Convert Markdown to Notion API block objects
Project description
notion-markdown
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 |
 |
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
941b3a5b04eee991d971114a9edc09b0935c6ab79f987a6f4999774d643fe362
|
|
| MD5 |
9b67fd12048e63140a6d465a3baf6f5a
|
|
| BLAKE2b-256 |
89c17614cd3ce5df401209051808e3c59c4959b52641a2e6c86c64184d754a49
|
Provenance
The following attestation bundles were made for notion_markdown-0.7.0.tar.gz:
Publisher:
publish.yml on surepub/notion-markdown
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_markdown-0.7.0.tar.gz -
Subject digest:
941b3a5b04eee991d971114a9edc09b0935c6ab79f987a6f4999774d643fe362 - Sigstore transparency entry: 947070696
- Sigstore integration time:
-
Permalink:
surepub/notion-markdown@d3d66260c01d28ab70a9a376ae6d21d1de9d5158 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/surepub
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d3d66260c01d28ab70a9a376ae6d21d1de9d5158 -
Trigger Event:
push
-
Statement type:
File details
Details for the file notion_markdown-0.7.0-py3-none-any.whl.
File metadata
- Download URL: notion_markdown-0.7.0-py3-none-any.whl
- Upload date:
- Size: 23.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae0c38ec73e114f8b6a367bd81eca0640b2af366ac2ea117028080c713ea6ea9
|
|
| MD5 |
61683aaf9ee4b1f9de5800f0555061e9
|
|
| BLAKE2b-256 |
5d856e0f302211bd615b16bd8431d2855225b9b2901b84b6d6785c730a9a4a91
|
Provenance
The following attestation bundles were made for notion_markdown-0.7.0-py3-none-any.whl:
Publisher:
publish.yml on surepub/notion-markdown
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_markdown-0.7.0-py3-none-any.whl -
Subject digest:
ae0c38ec73e114f8b6a367bd81eca0640b2af366ac2ea117028080c713ea6ea9 - Sigstore transparency entry: 947070698
- Sigstore integration time:
-
Permalink:
surepub/notion-markdown@d3d66260c01d28ab70a9a376ae6d21d1de9d5158 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/surepub
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d3d66260c01d28ab70a9a376ae6d21d1de9d5158 -
Trigger Event:
push
-
Statement type: