Python client for the Notion API
Project description
[!IMPORTANT] Upgrading from v1.4? Version 2.0 is a full redesign with a new API —
NotionClientreplaces the oldPage,Database, andSearchclasses. v1.4.x will continue to receive security fixes only. See the migration table and CHANGELOG for details.
notion-database
Python client for the Notion API — easy to use, 1-to-1 API mapping, AI/MCP friendly
Notion API version supported: 2026-03-11
What's new in 2.0
| Feature | Description |
|---|---|
client.pages.retrieve_markdown() |
Retrieve a page as enhanced Markdown (GET /pages/{id}/markdown) |
client.pages.update_markdown() |
Replace page content with Markdown (PATCH /pages/{id}/markdown) |
client.pages.append_markdown() |
Append Markdown to the bottom of a page (PATCH /pages/{id}/markdown) |
client.pages.create(timezone=...) |
IANA timezone for template variables (@now, @today) |
client.blocks.append_children(position=...) |
Insert blocks at start, end, or after_block |
client.databases.query(in_trash=...) |
Filter trashed / non-trashed rows |
client.databases.update(is_inline=..., in_trash=..., is_locked=...) |
Toggle inline layout, trash, and lock state |
client.databases.create(initial_data_source=...) |
Pre-populate a database from a data source on creation |
PropertySchema.button() |
Automation button column |
PropertySchema.location() |
Geographic location column |
PropertySchema.last_visited_time() |
Last visited time column (read-only) |
PropertySchema.rollup(relation_property_id=..., rollup_property_id=...) |
ID-based rollup lookup params |
PropertySchema.verification() |
Wiki page verification column |
PropertyValue.verification() |
Set wiki page verification state |
BlockContent.tab() / tab_group() |
Tab layout blocks |
Filter.created_by() / last_edited_by() |
Filter by creator or last editor |
Filter.formula(name, value_type) |
Filter on formula property results |
Filter.rollup(name, aggregate, value_type) |
Filter on rollup aggregates |
Filter.verification() |
Filter on wiki verification state |
Install
pip install notion-database==2.0.0rc1
Quick start
from notion_database import (
NotionClient,
PropertyValue,
PropertySchema,
BlockContent,
RichText,
Filter,
Sort,
Icon,
Cover,
)
client = NotionClient("secret_xxx")
Databases
# Retrieve
db = client.databases.retrieve("database-id")
# Create
db = client.databases.create(
parent={"type": "page_id", "page_id": "page-id"},
title=[RichText.text("My Database")],
properties={
"Name": PropertySchema.title(),
"Status": PropertySchema.select([
{"name": "Active", "color": "green"},
{"name": "Done", "color": "gray"},
]),
"Score": PropertySchema.number("number"),
"Due": PropertySchema.date(),
},
)
# Query with filter and sort
results = client.databases.query(
"database-id",
filter=Filter.select("Status").equals("Active"),
sorts=[Sort.descending("Score")],
)
pages = results["results"]
# Compound filter
results = client.databases.query(
"database-id",
filter=Filter.and_([
Filter.select("Status").equals("Active"),
Filter.number("Score").greater_than(80),
]),
)
# Auto-paginate (returns all pages at once)
all_pages = client.databases.query_all("database-id")
# Update schema — rename, lock, and set inline layout
client.databases.update(
"database-id",
title=[RichText.text("Renamed DB")],
is_inline=False, # full-page, not inline
is_locked=True, # prevent edits without unlocking
)
Pages
# Create
page = client.pages.create(
parent={"database_id": "database-id"},
properties={
"Name": PropertyValue.title("Hello, Notion 2.0!"),
"Status": PropertyValue.select("Active"),
"Score": PropertyValue.number(95),
"Due": PropertyValue.date("2024-12-31"),
"Done": PropertyValue.checkbox(False),
},
icon=Icon.emoji("🚀"),
cover=Cover.external("https://example.com/cover.jpg"),
children=[
BlockContent.heading_1("Introduction"),
BlockContent.paragraph("This page was created via notion-database 2.0."),
],
)
# Retrieve
page = client.pages.retrieve("page-id")
# Update properties
client.pages.update(
"page-id",
properties={
"Status": PropertyValue.select("Done"),
"Done": PropertyValue.checkbox(True),
},
)
# Archive / restore
client.pages.archive("page-id")
client.pages.archive("page-id", archived=False)
Blocks
# Append content to a page (inserted at end by default)
client.blocks.append_children(
"page-id",
children=[
BlockContent.heading_2("Section"),
BlockContent.paragraph([
RichText.text("Normal text, "),
RichText.text("bold", bold=True),
RichText.text(", and "),
RichText.text("italic", italic=True),
]),
BlockContent.bulleted_list_item("First item"),
BlockContent.bulleted_list_item("Second item"),
BlockContent.to_do("Finish docs", checked=False),
BlockContent.code("print('hello')", language="python"),
BlockContent.divider(),
BlockContent.image("https://example.com/image.png", caption="Fig 1"),
BlockContent.column_list([
[BlockContent.paragraph("Left column")],
[BlockContent.paragraph("Right column")],
]),
# Tab layout (Notion-Version: 2026-03-11)
BlockContent.tab_group([
BlockContent.tab("Overview", [BlockContent.paragraph("Overview content")]),
BlockContent.tab("Details", [BlockContent.paragraph("Details content")]),
]),
],
)
# Insert at a specific position (Notion-Version: 2026-03-11)
client.blocks.append_children("page-id", children=[
BlockContent.paragraph("Prepended to top"),
], position={"type": "start"})
client.blocks.append_children("page-id", children=[
BlockContent.paragraph("After a specific block"),
], position={"type": "after_block", "after_block": {"id": "block-id"}})
# Retrieve children (single page)
response = client.blocks.retrieve_children("page-id")
blocks = response["results"]
# Auto-paginate all children
all_blocks = client.blocks.retrieve_all_children("page-id")
# Delete a block
client.blocks.delete("block-id")
Search
# Search all
results = client.search.search("Project")
# Filter by type
db_results = client.search.search_databases("Project")
page_results = client.search.search_pages("Meeting notes")
# Auto-paginate all results
all_results = client.search.search_all("Q1")
Users
# Current bot
me = client.users.me()
# List all workspace users
all_users = client.users.list_all()
# Retrieve by ID
user = client.users.retrieve("user-id")
Comments
# Retrieve comments on a page
comments = client.comments.retrieve("page-id")
# Post a comment
client.comments.create(
parent={"page_id": "page-id"},
rich_text=[RichText.text("Great work!")],
)
Filters reference
# Text / title
Filter.text("Name").equals("Alice")
Filter.text("Name").contains("Al")
Filter.text("Name").starts_with("A")
Filter.text("Name").is_empty()
# Number
Filter.number("Score").greater_than(80)
Filter.number("Score").less_than_or_equal_to(100)
# Checkbox
Filter.checkbox("Done").equals(True)
# Select / status
Filter.select("Status").equals("Active")
Filter.status("Status").does_not_equal("Archived")
# Multi-select
Filter.multi_select("Tags").contains("python")
# Date
Filter.date("Due").before("2025-01-01")
Filter.date("Due").past_week()
Filter.date("Due").next_month()
# People-type columns
Filter.people("Assignee").contains("user-id")
Filter.created_by("Created By").contains("user-id")
Filter.last_edited_by("Last Edited By").does_not_contain("user-id")
# Timestamp columns
Filter.created_time().after("2024-01-01")
Filter.last_edited_time().past_week()
# Formula (value_type: "string" | "number" | "checkbox" | "date")
Filter.formula("Computed", "string").equals("ok")
Filter.formula("Score", "number").greater_than(50)
# Rollup (aggregate: "any" | "every" | "none" | "number"; value_type: property type)
Filter.rollup("Tasks", "any", "number").greater_than(0)
Filter.rollup("Tags", "every", "rich_text").contains("urgent")
# Verification (wiki pages)
Filter.verification("Verified").equals("verified")
# Compound
Filter.and_([
Filter.select("Status").equals("Active"),
Filter.number("Score").greater_than(80),
])
Filter.or_([
Filter.text("Name").contains("Alice"),
Filter.text("Name").contains("Bob"),
])
# Nested compound
Filter.and_([
Filter.checkbox("Done").equals(False),
Filter.or_([
Filter.select("Priority").equals("High"),
Filter.date("Due").before("2025-01-01"),
]),
])
# Raw (escape hatch)
Filter.raw({"property": "Formula", "formula": {"string": {"equals": "ok"}}})
Sorts reference
Sort.by_property("Name") # ascending by default
Sort.by_property("Score", "descending")
Sort.ascending("Name") # alias
Sort.descending("CreatedAt") # alias
Sort.by_timestamp("created_time", "descending")
Sort.by_timestamp("last_edited_time")
RichText reference
RichText.text("plain")
RichText.text("bold", bold=True)
RichText.text("italic", italic=True)
RichText.text("underline", underline=True)
RichText.text("strike", strikethrough=True)
RichText.text("code", code=True)
RichText.text("colored", color="red")
RichText.text("link", link="https://example.com")
RichText.mention_page("page-id")
RichText.mention_database("db-id")
RichText.mention_user("user-id")
RichText.mention_date("2024-01-01", end="2024-01-31")
RichText.equation("E=mc^2")
PropertySchema reference
from notion_database import PropertySchema
{
# Text
"Name": PropertySchema.title(),
"Notes": PropertySchema.rich_text(),
"Website": PropertySchema.url(),
"Email": PropertySchema.email(),
"Phone": PropertySchema.phone_number(),
# Numeric
"Score": PropertySchema.number("number"),
"Price": PropertySchema.number("dollar"),
# Selection
"Category": PropertySchema.select([{"name": "A", "color": "green"}]),
"Tags": PropertySchema.multi_select(),
"Status": PropertySchema.status(),
# Date / time (read-only system columns)
"Due": PropertySchema.date(),
"Created": PropertySchema.created_time(),
"CreatedBy": PropertySchema.created_by(),
"LastEdited": PropertySchema.last_edited_time(),
"LastEditedBy": PropertySchema.last_edited_by(),
# Other
"Done": PropertySchema.checkbox(),
"Files": PropertySchema.files(),
"People": PropertySchema.people(),
# Special (2026-03-11)
"Action": PropertySchema.button(), # automation trigger
"Location": PropertySchema.location(), # geographic location
"LastVisited": PropertySchema.last_visited_time(), # read-only
# Computed
"Formula": PropertySchema.formula("prop('Score') * 2"),
"UniqueID": PropertySchema.unique_id(prefix="ITEM"),
"Related": PropertySchema.relation("other-database-id"),
"Rollup": PropertySchema.rollup("Related", "Count", "count"),
# ID-based rollup (stable against column renames)
"RollupByID": PropertySchema.rollup(
"Related", "Count", "sum",
relation_property_id="rel-id",
rollup_property_id="prop-id",
),
# Verification (wiki databases only)
"Verified": PropertySchema.verification(),
}
Error handling
from notion_database import (
NotionAPIError,
NotionNotFoundError,
NotionRateLimitError,
NotionUnauthorizedError,
)
import time
try:
page = client.pages.retrieve("invalid-id")
except NotionNotFoundError:
print("Page not found")
except NotionRateLimitError:
time.sleep(1)
page = client.pages.retrieve("invalid-id")
except NotionUnauthorizedError:
print("Check your integration token")
except NotionAPIError as e:
print(f"[{e.status_code}] {e.code}: {e.message}")
Color constants
from notion_database.const import (
DEFAULT, GRAY, BROWN, ORANGE, YELLOW, GREEN, BLUE, PURPLE, PINK, RED,
GRAY_BACKGROUND, BROWN_BACKGROUND, ORANGE_BACKGROUND, YELLOW_BACKGROUND,
GREEN_BACKGROUND, BLUE_BACKGROUND, PURPLE_BACKGROUND, PINK_BACKGROUND,
RED_BACKGROUND,
)
BlockContent.paragraph("Highlighted", color=RED_BACKGROUND)
MCP integration
NotionClient is designed to map directly to Notion API endpoints, making it
straightforward to expose as MCP tools. Each sub-client (databases,
pages, blocks, search, users, comments) corresponds to one section
of the Notion API docs. All parameters are typed and documented, so an AI can
introspect the signatures without additional context.
License
LGPLv3
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_database-2.0.0rc1.tar.gz.
File metadata
- Download URL: notion_database-2.0.0rc1.tar.gz
- Upload date:
- Size: 37.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f3f86a2b7ee794a2938a682a76a03c73a164e6884d1c325d071e83e0a28ac2e8
|
|
| MD5 |
e680e6c513259727ac5b1cc90f169a35
|
|
| BLAKE2b-256 |
0e48d4d648bfb12da0a5bed5a2cb014006a9eb80a97ec155d0ff636a43cf1837
|
Provenance
The following attestation bundles were made for notion_database-2.0.0rc1.tar.gz:
Publisher:
publish-to-pypi.yml on minwook-shin/notion-database
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_database-2.0.0rc1.tar.gz -
Subject digest:
f3f86a2b7ee794a2938a682a76a03c73a164e6884d1c325d071e83e0a28ac2e8 - Sigstore transparency entry: 1238508464
- Sigstore integration time:
-
Permalink:
minwook-shin/notion-database@f632d453c70675c77b6d1132237700c5911de160 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/minwook-shin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@f632d453c70675c77b6d1132237700c5911de160 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file notion_database-2.0.0rc1-py3-none-any.whl.
File metadata
- Download URL: notion_database-2.0.0rc1-py3-none-any.whl
- Upload date:
- Size: 37.1 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 |
f6596b3a6935d3f8a1de23673a8d9c535aa2a449c9ec3e1e3cde9196f33d1e5e
|
|
| MD5 |
3b2ca349e2e0c1a4222481ce6c55c861
|
|
| BLAKE2b-256 |
874ac57615a792fe4386cffe4b09827d4f5e65b1e9063cb4f9afbb6398572134
|
Provenance
The following attestation bundles were made for notion_database-2.0.0rc1-py3-none-any.whl:
Publisher:
publish-to-pypi.yml on minwook-shin/notion-database
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_database-2.0.0rc1-py3-none-any.whl -
Subject digest:
f6596b3a6935d3f8a1de23673a8d9c535aa2a449c9ec3e1e3cde9196f33d1e5e - Sigstore transparency entry: 1238508465
- Sigstore integration time:
-
Permalink:
minwook-shin/notion-database@f632d453c70675c77b6d1132237700c5911de160 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/minwook-shin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@f632d453c70675c77b6d1132237700c5911de160 -
Trigger Event:
workflow_dispatch
-
Statement type: