包名是 notion-client 而非 notion-sdk-py,导入名是 notion_client;需先在 Notion 后台创建 integration 并拿到 token 才能调用;500/503 仅在 GET、DELETE 等幂等方法上自动重试,写操作需自行处理重试
notion-sdk-py
ramnesMIT活跃维护Notion API 的 Python 客户端 SDK,同时支持同步和异步调用,用 dataclasses 和 httpx 封装接口,省去手写请求的麻烦。
Notion API client SDK, rewritten in Python! (sync + async)
pip install notion-clientREADME 未说明,纯 HTTP 客户端库,无 GPU 或特殊硬件要求
Notion 官方 API 的 Python 客户端 SDK,同步异步双支持,写法对齐 JS 参考实现
Notion API 的 Python 客户端 SDK,同时支持同步和异步调用,用 dataclasses 和 httpx 封装接口,省去手写请求的麻烦。
核心能力
- 提供 Client 与 AsyncClient 两套同步/异步客户端
- 所有端点参数合并为单个对象,无需区分 path/query/body
- 内置 429/529/500/503 自动重试与指数退避抖动
- 抛出 APIResponseError 并用 APIErrorCode 枚举比对错误码
- 提供 is_full_page 等辅助函数区分完整与部分响应对象
容易踩的坑
- 3.1.0最新正式版2026-05-12 · 2026-05-12 08:58 UTC
This version adds support for a whole bunch of new endpoints!
It also brings automatic retries on rate limits and transient server errors (configurable at the
ClientOptionslevel).A few new helpers and constants have been added as well:
is_full_view,is_http_response_error,DEFAULT_TIMEOUT_MSandMIN_VIEW_COLUMN_WIDTH.Thanks to @tsinglinrain for the help.
What's Changed
- Enhance request handling in
client.pyfollowing notion-sdk-js by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/314 - Add
pages.retrieve_markdown()andpages.update_markdown()endpoints by @ramnes in https://github.com/ramnes/notion-sdk-py/pull/317 - Add automatic retry support with exponential backoff by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/319
- Extract constants by @ramnes in https://github.com/ramnes/notion-sdk-py/pull/321
- Add
gateway_timeouttoAPIErrorCodeenum by @ramnes in https://github.com/ramnes/notion-sdk-py/pull/322 - Add
custom_emojis.list()endpoint by @ramnes in https://github.com/ramnes/notion-sdk-py/pull/323 - Add view endpoints by @ramnes in https://github.com/ramnes/notion-sdk-py/pull/324
Full Changelog: https://github.com/ramnes/notion-sdk-py/compare/3.0.0...3.1.0
- Enhance request handling in
- 3.0.02026-02-16 · 2026-02-16 11:23 UTC
查看更新说明
Thanks to all the contributors for this new major version!
⚠️ Beware of the following breaking changes:
is_full_page_or_databasehas been removed. Useis_full_page_or_data_sourceinstead.is_api_error_codehas been made private.
What's Changed
- Added
intro-to-notion-apiexample for Python SDK by @Sumeet213 in https://github.com/ramnes/notion-sdk-py/pull/297 - Added helper functions related to
data sourcesinhelpers.pyand more namespace exports by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/299 - Fixed mypy compatibility with Python 3.14 by @Sumeet213 in https://github.com/ramnes/notion-sdk-py/pull/300
- Added OAuth token, introspect, and revoke endpoints by @Sumeet213 in https://github.com/ramnes/notion-sdk-py/pull/301
- Added move method to pages endpoint by @JJocelynH in https://github.com/ramnes/notion-sdk-py/pull/307
- Added error namespace and refactored
errors.pyfollowing JS SDK implementation by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/308 - Added path traversal validation in
error.pyfollowing notion-sdk-js by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/312 - Added support for Python 3.14 by @ramnes in https://github.com/ramnes/notion-sdk-py/pull/315
New Contributors
- @JJocelynH made their first contribution in https://github.com/ramnes/notion-sdk-py/pull/307
Full Changelog: https://github.com/ramnes/notion-sdk-py/compare/2.7.0...3.0.0
- 2.7.02025-10-31 · 2025-10-31 12:11 UTC
查看更新说明
What's Changed
- Add generate random data example by @Sumeet213 in https://github.com/ramnes/notion-sdk-py/pull/292
- Prevent resource leaks in tests when failures occur by @Sumeet213 in https://github.com/ramnes/notion-sdk-py/pull/291
- Add support for
additional_dataandrequest_idfields in theAPIResponseErrorclass by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/293 - Add missing VCR cassettes for
additional_dataandrequest_idtests by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/295 - Refactor file upload tests to remove hardcoded start_cursor and improve maintainability by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/294
- Add missing features to match notion-sdk-js generate_random_data by @Sumeet213 in https://github.com/ramnes/notion-sdk-py/pull/296
New Contributors
- @Sumeet213 made their first contribution in https://github.com/ramnes/notion-sdk-py/pull/292
Full Changelog: https://github.com/ramnes/notion-sdk-py/compare/2.6.0...2.7.0
- 2.6.02025-10-23 · 2025-10-23 14:50 UTC
查看更新说明
⚠️ Beware,
Notion-Versionhas been upgraded to2025-09-03. This version of the SDK is not compatible with older versions of the API. See Notion's upgrade guide to learn more.What's Changed
- Add
attachementsanddisplay_nameto body by @zubu007 in https://github.com/ramnes/notion-sdk-py/pull/281 - Add id extraction utilities for Notion URLs by @riwaht in https://github.com/ramnes/notion-sdk-py/pull/282
- Added data source endpoints and refactored database endpoints by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/286
New Contributors
- @zubu007 made their first contribution in https://github.com/ramnes/notion-sdk-py/pull/281
- @riwaht made their first contribution in https://github.com/ramnes/notion-sdk-py/pull/282
Full Changelog: https://github.com/ramnes/notion-sdk-py/compare/2.5.0...2.6.0
- Add
- 2.5.02025-08-26 · 2025-08-26 10:35 UTC
查看更新说明
What's Changed
- Support file uploads by @tsinglinrain in https://github.com/ramnes/notion-sdk-py/pull/270
New Contributors
- @tsinglinrain made their first contribution in https://github.com/ramnes/notion-sdk-py/pull/270
Full Changelog: https://github.com/ramnes/notion-sdk-py/compare/2.4.0...2.5.0
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。

notion-sdk-py is a simple and easy to use client library for the official Notion API.
It is meant to be a Python version of the reference JavaScript SDK, so usage should be very similar between both. 😊 (If not, please open an issue or PR!)
Installation
pip install notion-client
Usage
Use Notion's Getting Started Guide to get set up to use Notion's API.
Import and initialize a client using an integration token or an OAuth access token.
import os
from notion_client import Client
notion = Client(auth=os.environ["NOTION_TOKEN"])
In an asyncio environment, use the asynchronous client instead:
from notion_client import AsyncClient
notion = AsyncClient(auth=os.environ["NOTION_TOKEN"])
Make a request to any Notion API endpoint.
from pprint import pprint
list_users_response = notion.users.list()
pprint(list_users_response)
[!NOTE] See the complete list of endpoints in the API reference.
or with the asynchronous client:
list_users_response = await notion.users.list()
pprint(list_users_response)
This would output something like:
{'results': [{'avatar_url': 'https://secure.notion-static.com/e6a352a8-8381-44d0-a1dc-9ed80e62b53d.jpg',
'id': 'd40e767c-d7af-4b18-a86d-55c61f1e39a4',
'name': 'Avocado Lovelace',
'object': 'user',
'person': {'email': '[email protected]'},
'type': 'person'},
...]}
All API endpoints are available in both the synchronous and asynchronous clients.
Endpoint parameters are grouped into a single object. You don't need to remember which parameters go in the path, query, or body.
my_page = notion.data_sources.query(
**{
"data_source_id": "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
"filter": {
"property": "Landmark",
"rich_text": {
"contains": "Bridge",
},
},
}
)
Handling errors
If the API returns an unsuccessful response, an APIResponseError will be raised.
The error contains properties from the response, and the most helpful is code.
You can compare code to the values in the APIErrorCode object to avoid
misspelling error codes.
import logging
from notion_client import APIErrorCode, APIResponseError, Client
try:
notion = Client(auth=os.environ["NOTION_TOKEN"])
my_page = notion.data_sources.query(
**{
"data_source_id": "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
"filter": {
"property": "Landmark",
"rich_text": {
"contains": "Bridge",
},
},
}
)
except APIResponseError as error:
if error.code == APIErrorCode.ObjectNotFound:
#
# For example: handle by asking the user to select a different data source
#
...
else:
# Other error handling code
print(error)
Logging
The client emits useful information to a logger. By default, it only emits warnings and errors.
If you're debugging an application, and would like the client to log request & response
bodies, set the log_level option to logging.DEBUG.
import logging
from notion_client import Client
notion = Client(
auth=os.environ["NOTION_TOKEN"],
log_level=logging.DEBUG,
)
You may also set a custom logger to emit logs to a destination other than stdout.
Have a look at Python's logging cookbook
if you want to create your own logger.
Client options
Client and AsyncClient both support the following options on initialization.
These options are all keys in the single constructor parameter.
| Option | Default value | Type | Description |
|---|---|---|---|
auth | None | string | Bearer token for authentication. If left undefined, the auth parameter should be set on each request. |
log_level | logging.WARNING | int | Verbosity of logs the instance will produce. By default, logs are written to stdout. |
timeout_ms | DEFAULT_TIMEOUT_MS | int | Number of milliseconds to wait before emitting a RequestTimeoutError |
base_url | DEFAULT_BASE_URL | string | The root URL for sending API requests. This can be changed to test with a mock server. |
logger | Log to console | logging.Logger | A custom logger. |
retry | See constants | RetryOptions | Configuration for automatic retries on rate limits (429), service overloads (529), and server errors (500, 503). See Automatic retries below. |
Automatic retries
The client automatically retries requests that fail due to rate limiting or transient server errors. By default, it will retry up to 2 times using exponential back-off with jitter.
Retryable errors:
rate_limited(HTTP 429) - Too many requests; retried for all HTTP methodsservice_overload(HTTP 529) - Service overloaded; retried for all HTTP methodsinternal_server_error(HTTP 500) - Server error; retried only for GET and DELETEservice_unavailable(HTTP 503) - Service temporarily unavailable; retried only for GET and DELETE
Server errors (500, 503) are only retried for idempotent HTTP methods (GET, DELETE) to avoid duplicate side effects. Rate limits (429) and service overloads (529) are retried for all methods since the server explicitly asks clients to retry.
Configuration:
from notion_client import Client, RetryOptions
notion = Client(
auth="secret_...",
retry=RetryOptions(
max_retries=5, # Maximum retry attempts (default: 2)
initial_retry_delay_ms=500, # Initial delay in ms (default: 1000)
max_retry_delay_ms=60000, # Maximum delay in ms (default: 60000)
),
)
To disable automatic retries:
notion = Client(auth="secret_...", retry=False)
Constants
The SDK exports named constants for all default values used by the client, as well as useful Notion-specific values. You can import them directly:
from notion_client import (
DEFAULT_BASE_URL, # "https://api.notion.com"
DEFAULT_TIMEOUT_MS, # 60_000
DEFAULT_MAX_RETRIES, # 2
DEFAULT_INITIAL_RETRY_DELAY_MS, # 1_000
DEFAULT_MAX_RETRY_DELAY_MS, # 60_000
MIN_VIEW_COLUMN_WIDTH, # 32
)
MIN_VIEW_COLUMN_WIDTH is the minimum width (in pixels) that a table column can
have in the Notion UI. Set a property's width to this value when creating or
updating a view to make a column appear collapsed -- useful for checkbox or
status-as-checkbox columns:
await notion.views.create(
database_id=database_id,
name="My view",
type="table",
configuration={
"table": {
"properties": [
{
"property_id": checkbox_prop_id,
"visible": True,
"width": MIN_VIEW_COLUMN_WIDTH,
},
],
},
},
)
Full API responses
The following functions can distinguish between full and partial API responses.
| Function | Purpose |
|---|---|
is_full_page | Determine whether an object is a full Page object |
is_full_block | Determine whether an object is a full Block object |
is_full_data_source | Determine whether an object is a full Data source object |
is_full_page_or_data_source | Determine whether an object is a full Page object or Data source object |
is_full_user | Determine whether an object is a full User object |
is_full_comment | Determine whether an object is a full Comment object |
from notion_client.helpers import is_full_page
full_or_partial_pages = notion.data_sources.query(
data_source_id="897e5a76-ae52-4b48-9fdf-e71f5945d1af"
)
for page in full_or_partial_pages["results"]:
if not is_full_page_or_data_source(page):
continue
print(f"Created at: {page['created_time']}")
Utility functions
This package also exports a few utility functions that are helpful for dealing with any of the paginated APIs.
iterate_paginated_api(function, **kwargs)
This utility turns any paginated API into a generator.
Parameters:
function: Any function on the Notion client that represents a paginated API (i.e. acceptsstart_cursor.) Example:notion.blocks.children.list.**kwargs: Arguments that should be passed to the API on the first and subsequent calls to the API, for example ablock_id.
Returns:
A generator over results from the API.
Example:
from notion_client.helpers import iterate_paginated_api
for block in iterate_paginated_api(
notion.blocks.children.list, block_id=parent_block_id
):
# Do something with block.
...
collect_paginated_api(function, **kwargs)
This utility accepts the same arguments as iterate_paginated_api, but collects
the results into an in-memory array.
Before using this utility, check that the data you are dealing with is small enough to fit in memory.
Parameters:
function: Any function on the Notion client that represents a paginated API (i.e. acceptsstart_cursor.) Example:notion.blocks.children.list.**kwargs: Arguments that should be passed to the API on the first and subsequent calls to the API, for example ablock_id.
Returns:
An array with results from the API.
Example:
from notion_client.helpers import collect_paginated_api
blocks = collect_paginated_api(
notion.blocks.children.list, block_id=parent_block_id
)
# Do something with blocks.
Both utilities also have async versions: async_iterate_paginated_api and
async_collect_paginated_api.
Custom requests
To make requests directly to a Notion API endpoint instead of using the
pre-built families of methods, call the request() method. For example:
import json
# POST /v1/comments
response = notion.request(
path="comments",
method="post",
body={
"parent": {"page_id": "5c6a28216bb14a7eb6e1c50111515c3d"},
"rich_text": [{"text": {"content": "Hello, world!"}}],
},
# No `query` params in this example, only `body`.
)
print(json.dumps(response, indent=2))
[!TIP] Usually, making custom requests with
notion.request()isn't necessary, but can be helpful in some cases, e.g. when upgrading your Notion API version incrementally before upgrading your SDK version. For example, if there's a new or renamed endpoint in the new API version that isn't yet available to call via a dedicated method onClient.In the above example, the simpler approach is to use
notion.comments.create().
Another customization you can make is to pass your own httpx.Client or
httpx.AsyncClient to the Client or AsyncClient constructor. This might be
helpful for some execution environments where the default HTTPX client isn't
suitable.
Verifying webhook signatures
If your integration receives Notion webhook deliveries,
use verify_webhook_signature to confirm each request was sent by Notion and
was not tampered with in transit. Notion signs every delivery with HMAC-SHA256
over the raw request body using the subscription's verification token, and
places the result in the X-Notion-Signature header as sha256=<hex>.
import json
import os
from fastapi import FastAPI, HTTPException, Request
from notion_client import verify_webhook_signature
app = FastAPI()
# The body must be read as the raw bytes that arrived over the wire —
# JSON-parsed and re-serialized bodies will not verify, because
# re-serialization changes whitespace and key order.
@app.post("/notion-webhook")
async def notion_webhook(request: Request):
body = await request.body()
if not verify_webhook_signature(
body=body,
signature=request.headers.get("X-Notion-Signature"),
verification_token=os.environ["NOTION_WEBHOOK_VERIFICATION_TOKEN"],
):
raise HTTPException(status_code=401, detail="invalid signature")
event = json.loads(body)
# ...handle the event
return "ok"
The same helper handles the initial verification handshake that Notion sends
when you first register a webhook URL — the handshake body
({"verification_token": "..."}) is signed with the same token, so a single
code path covers both cases.
sign_webhook_payload(body, verification_token) produces the same
sha256=<hex> header value, which is useful for unit-testing your webhook
handler without standing up a real subscription.
Testing
Run the tests with the pytest command. If you want to test against all Python
versions, you can run tox instead.
The tests are using pytest-vcr's cassettes for simulating requests to the
Notion API. To create new tests or run them without cassettes, you need to set
up the environment variables NOTION_TOKEN and NOTION_TEST_PAGE_ID (a page
where your integration has all the capabilities enabled).
The code will use the page at NOTION_TEST_PAGE_ID to generate a temporary
environment with the Notion objects to be tested, which will be deleted
at the end of the session.
Requirements and compatibility
This package supports the following minimum versions:
- Runtime: Python >= 3.8
- httpx >= 0.23.0
Earlier versions may still work, but we encourage people building new applications to upgrade to the current stable.
Getting help
If you want to submit a feature request for Notion's API, or are experiencing
any issues with the API platform, please email [email protected].
If you found a bug with the library, please submit an issue.