Skip to main content

Convert Markdown to Notion API block objects

Project description

notion-markdown

CI codecov PyPI Python License: MIT

Bidirectional conversion between Markdown and Notion API block objects. Fully typed, zero dependencies beyond mistune.

from notion_markdown import to_notion, to_markdown

# Markdown → Notion blocks
blocks = to_notion("# Hello\n\nSome **bold** text.")

# Notion blocks → Markdown
md = to_markdown(blocks)
# "# Hello\n\nSome **bold** text.\n"

Installation

pip install notion-markdown

CLI

Markdown to Notion blocks

# File to stdout
notion-markdown to-notion README.md

# Pipe from stdin
cat README.md | notion-markdown to-notion

# Write to a file
notion-markdown to-notion README.md -o blocks.json

# Compact JSON (no indentation)
notion-markdown to-notion README.md --indent 0

Notion blocks to Markdown

# JSON file to stdout
notion-markdown to-markdown blocks.json

# Pipe from stdin
cat blocks.json | notion-markdown to-markdown

# Write to a file
notion-markdown to-markdown blocks.json -o output.md

Python API

to_notion() — Markdown to Notion blocks

Takes a Markdown string and returns a list of Notion API block objects. Pass them directly to notion-client:

from notion_client import Client
from notion_markdown import to_notion

notion = Client(auth="secret_...")
blocks = to_notion(open("README.md").read())

notion.pages.create(
    parent={"page_id": "..."},
    properties={"title": [{"text": {"content": "My Page"}}]},
    children=blocks,
)

to_markdown() — Notion blocks to Markdown

Takes a list of Notion API block objects and returns a Markdown string. Works with blocks from to_notion() or directly from the Notion API:

from notion_markdown import to_notion, to_markdown

# From to_notion() output
blocks = to_notion("# Hello\n\nWorld")
md = to_markdown(blocks)

# From the Notion API
page_blocks = notion.blocks.children.list(block_id="...")["results"]
md = to_markdown(page_blocks)

Roundtrip guarantee

The two functions are inverses — converting in either direction and back produces identical output:

from notion_markdown import to_notion, to_markdown

md = "# Title\n\nSome **bold** text.\n"
assert to_markdown(to_notion(md)) == md

blocks = to_notion(md)
assert to_notion(to_markdown(blocks)) == blocks

Migration from convert()

In v0.7.0, convert() was renamed to to_notion() for consistency with to_markdown(). The old convert() function still works but emits a DeprecationWarning:

# Old (deprecated)
from notion_markdown import convert
blocks = convert("# Hello")  # ⚠️ DeprecationWarning

# New
from notion_markdown import to_notion
blocks = to_notion("# Hello")

Handling large documents (> 100 blocks)

The Notion API limits each request to 100 blocks. Split the list and append:

from itertools import batched  # Python 3.12+

blocks = to_notion(long_markdown)

for i, chunk in enumerate(batched(blocks, 100)):
    if i == 0:
        page = notion.pages.create(
            parent={"page_id": "..."},
            properties={"title": [{"text": {"content": "Page"}}]},
            children=list(chunk),
        )
    else:
        notion.blocks.children.append(block_id=page["id"], children=list(chunk))

Supported Markdown Elements

Block-level

Markdown Notion block type
# Heading heading_1
## Heading heading_2
### Heading heading_3
Paragraphs paragraph
- item / * item bulleted_list_item
1. item numbered_list_item
- [ ] / - [x] to_do
```lang code fences code (with language)
--- divider
> quote quote
| table | table + table_row
![alt](url) image
$$ expr $$ equation
<aside> callout
<details><summary> toggle

Inline formatting

Markdown Notion annotation
**bold** / __bold__ bold: true
*italic* / _italic_ italic: true
~~strike~~ strikethrough: true
`code` code: true
[text](url) text.link.url
$expr$ inline equation
<span underline="true"> underline: true
<span color="red"> color: "red"

Nested formatting is fully supported — **bold *and italic* text** produces the correct flat list of rich-text items with accumulated annotations.

All conversions work in both directions: to_notion() and to_markdown() handle every block and inline type listed above.

Type Safety

Every return type is a TypedDict, giving you full IDE autocomplete and mypy --strict compatibility. No dict[str, Any] in the public API.

from notion_markdown import to_notion, to_markdown, NotionBlock, ParagraphBlock

blocks: list[NotionBlock] = to_notion("Hello **world**")
md: str = to_markdown(blocks)

# IDE knows blocks[0] could be ParagraphBlock, HeadingOneBlock, etc.
# and gives autocomplete for block["paragraph"]["rich_text"]

Available types

All block types and rich-text types are exported:

from notion_markdown import (
    # Block types
    ParagraphBlock, HeadingOneBlock, HeadingTwoBlock, HeadingThreeBlock,
    BulletedListItemBlock, NumberedListItemBlock, ToDoBlock,
    CodeBlock, QuoteBlock, CalloutBlock, ToggleBlock,
    DividerBlock, TableBlock, ImageBlock,
    EquationBlock, BookmarkBlock, EmbedBlock, VideoBlock,
    # Rich-text types
    RichText, RichTextText, RichTextEquation, RichTextAnnotations,
    # Union of all blocks
    NotionBlock,
)

Code block language mapping

Common language aliases are automatically mapped to Notion's language identifiers:

Input Notion language
py python
js, jsx javascript
ts, tsx typescript
sh, zsh shell
yml yaml
rb ruby
rs rust
cpp, cc c++
cs c#
(empty) plain text

Development

# Clone and install
git clone https://github.com/surepub/notion-markdown.git
cd notion-markdown
uv venv && uv pip install -e . && uv pip install pytest pytest-cov ruff mypy

# Run tests
pytest tests/ -v

# Run tests with coverage
pytest tests/ --cov=notion_markdown --cov-report=term-missing

# Lint and format
ruff check src/ tests/
ruff format src/ tests/

# Type check
mypy src/ --strict

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

notion_markdown-0.7.0.tar.gz (42.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

notion_markdown-0.7.0-py3-none-any.whl (23.4 kB view details)

Uploaded Python 3

File details

Details for the file notion_markdown-0.7.0.tar.gz.

File metadata

  • Download URL: notion_markdown-0.7.0.tar.gz
  • Upload date:
  • Size: 42.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for notion_markdown-0.7.0.tar.gz
Algorithm Hash digest
SHA256 941b3a5b04eee991d971114a9edc09b0935c6ab79f987a6f4999774d643fe362
MD5 9b67fd12048e63140a6d465a3baf6f5a
BLAKE2b-256 89c17614cd3ce5df401209051808e3c59c4959b52641a2e6c86c64184d754a49

See more details on using hashes here.

Provenance

The following attestation bundles were made for notion_markdown-0.7.0.tar.gz:

Publisher: publish.yml on surepub/notion-markdown

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file notion_markdown-0.7.0-py3-none-any.whl.

File metadata

File hashes

Hashes for notion_markdown-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ae0c38ec73e114f8b6a367bd81eca0640b2af366ac2ea117028080c713ea6ea9
MD5 61683aaf9ee4b1f9de5800f0555061e9
BLAKE2b-256 5d856e0f302211bd615b16bd8431d2855225b9b2901b84b6d6785c730a9a4a91

See more details on using hashes here.

Provenance

The following attestation bundles were made for notion_markdown-0.7.0-py3-none-any.whl:

Publisher: publish.yml on surepub/notion-markdown

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page