Skip to main content

Python client for the Notion API

Project description

Test Python Package

[!IMPORTANT] Upgrading from v1.4? Version 2.0 is a full redesign with a new API — NotionClient replaces the old Page, Database, and Search classes. 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

notion_database-2.0.0rc1.tar.gz (37.1 kB view details)

Uploaded Source

Built Distribution

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

notion_database-2.0.0rc1-py3-none-any.whl (37.1 kB view details)

Uploaded Python 3

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

Hashes for notion_database-2.0.0rc1.tar.gz
Algorithm Hash digest
SHA256 f3f86a2b7ee794a2938a682a76a03c73a164e6884d1c325d071e83e0a28ac2e8
MD5 e680e6c513259727ac5b1cc90f169a35
BLAKE2b-256 0e48d4d648bfb12da0a5bed5a2cb014006a9eb80a97ec155d0ff636a43cf1837

See more details on using hashes here.

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

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_database-2.0.0rc1-py3-none-any.whl.

File metadata

File hashes

Hashes for notion_database-2.0.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 f6596b3a6935d3f8a1de23673a8d9c535aa2a449c9ec3e1e3cde9196f33d1e5e
MD5 3b2ca349e2e0c1a4222481ce6c55c861
BLAKE2b-256 874ac57615a792fe4386cffe4b09827d4f5e65b1e9063cb4f9afbb6398572134

See more details on using hashes here.

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

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