Pythonの標準ライブラリだけでNotion APIに1行書き込む
「処理が終わったら、Notionのデータベースに1行だけ記録したい」。それだけのために、仮想環境(venv)を作って pip install requests をするのは、少し大げさです。しかも自動実行のスクリプトでは、ライブラリが入っていないPCで動かなくなる、という失敗の種にもなります。
私は、自動実行が失敗したときにNotionへ確認用のタスクを1件書き残すスクリプトを、標準ライブラリの urllib だけで作りました。この記事では、その作り方を、家の用事の例(買い物メモ)に置き換えてまとめます。
なぜ標準ライブラリにしたか
Pythonの公式ドキュメントでは、より高機能なHTTPクライアントとして requests パッケージが勧められています。ただ、Notion APIに送るのは「JSONを1回POSTする」だけです。urllib.request で足りるので、私は次の理由で標準ライブラリを選びました。
- インストールが要らない。Pythonさえ入っていれば、どのPCでもそのまま動く
- 無人実行で壊れにくい。依存するものが少ないほど、「ある日ライブラリが無くて動かない」が起きにくい
- 呼び出しのたびにAIを起動しない。失敗の通知のように何度も走る処理は、決まった形のAPI呼び出しにしておくと軽く、費用も増えない
書き込みまでの流れ
- 1. インテグレーション作ってトークンを受け取る
- 2. 共有書き込み先に「コネクトを追加」する
- 3. トークンを置く環境変数か.envに。コードには書かない
- 4. POSTurllibでページを作る
トークンを用意する
Notionの公式ドキュメントでは、内部インテグレーションの作り方はこう説明されています。開発者ポータルで新しいコネクションを作り、「Configuration」タブでAPIトークンを取得します(ワークスペースのオーナーである必要があります)。なお、公式には、自分の権限で動く「個人アクセストークン」も別に用意されています。
次に、書き込み先のページかデータベースを開き、右上のメニューから「Add connections(コネクトを追加)」で作ったコネクションを選びます。公式ドキュメントでは、この共有をしないとコネクションはページを操作できないと説明されています。
私が一度はまったのは、ここです。すでに動いているトークンを別の用途に使い回したところ、新しく書き込み先にしたページには共有が済んでおらず、404で静かに失敗し続けました。詳しくはNotion APIが「object_not_found(404)」を返すときの確認手順にまとめています。
トークンの扱いについて、公式ドキュメントは「ソースコードやバージョン管理に入れず、環境変数から読む」ように勧めています。私は、スクリプトと同じフォルダの .env に NOTION_TOKEN=… と1行だけ書き、スクリプトがそれを読む形にしています。なお、Pythonの os.environ は .env を自動では読みません。下のコードでは、環境変数に無ければ .env を数行で読むようにしています。同期フォルダやGitに入れる場合は、.env を除外するのを忘れないでください。
ページを1つ作る最小のコード
ページを作るエンドポイントは POST /v1/pages です。リクエストには、Authorization: Bearer のトークンと、Notion-Version ヘッダーが要ります。公式ドキュメントでは、Notion-Version はすべてのリクエストに必須で、2026年10月時点の最新は 2026-03-11 と書かれています。
次の例は、「名前」「日付」という2つのプロパティを持つデータベースに、買い物メモを1行足します(プロパティ名は例です。自分のデータベースの名前に合わせます)。
import json
import os
import urllib.error
import urllib.request
from pathlib import Path
NOTION_VERSION = "2026-03-11"
DATA_SOURCE_ID = "ここにデータソースのID"
def load_token():
"""環境変数 NOTION_TOKEN か、スクリプトと同じフォルダの .env から読む"""
token = os.environ.get("NOTION_TOKEN")
if token:
return token
env_file = Path(__file__).with_name(".env")
if env_file.exists():
for line in env_file.read_text(encoding="utf-8").splitlines():
if line.startswith("NOTION_TOKEN="):
return line.split("=", 1)[1].strip()
raise SystemExit("NOTION_TOKEN が見つかりません")
TOKEN = load_token()
payload = {
"parent": {"type": "data_source_id", "data_source_id": DATA_SOURCE_ID},
"properties": {
"名前": {"title": [{"type": "text", "text": {"content": "牛乳を買う"}}]},
"日付": {"date": {"start": "2026-10-20"}},
},
}
req = urllib.request.Request(
"https://api.notion.com/v1/pages",
data=json.dumps(payload).encode("utf-8"),
method="POST",
headers={
"Authorization": f"Bearer {TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
print("作成できました:", resp.status)
except urllib.error.HTTPError as e:
print("失敗: HTTP", e.code, e.read().decode("utf-8", errors="replace"))
raise SystemExit(1)
except OSError as e:
print("通信できませんでした:", e)
raise SystemExit(1)
ポイントは3つです。
- bodyはbytesにする。Pythonの公式ドキュメントでは、
Requestのdataはバイト列である必要があります。json.dumps(...).encode("utf-8")で変換します - methodを明示する。省略した場合、dataがあればPOSTになりますが、明示しておくと読みやすくなります
- timeoutを付ける。無人実行では、通信が止まったまま待ち続けるのを避けられます
データベースに行を足すときの「親」の指定
Notionの公式のアップグレードガイドでは、APIバージョン 2025-09-03 からデータソースという考え方が入り、データベースに行を足すときの親は data_source_id に切り替えるよう案内されています。ページ作成のリファレンスには親の型として database_id もまだ載っていますが、ガイドの案内に沿って data_source_id を使うのが確実です。旧バージョン(2022-06-28)では database_id で指定する形でした。
| Notion-Version | データベースに行を足すときの親 |
|---|---|
| 2022-06-28 | {"database_id": "…"} |
| 2025-09-03 以降 | {"data_source_id": "…"}(公式ガイドの案内) |
データソースのIDは、データベースの取得APIが返す data_sources の一覧から、またはNotionの画面のデータベースの設定にある「Manage data sources」の「Copy data source ID」で取れます。URLから取れるIDはデータベースのIDなので、混同しないでください。URLの読み分けにはNotionのID取り出しツールが使えます。
通常のページの子としてページを作るなら、親は page_id で、プロパティに指定できるのは title だけです(公式のリファレンスの説明)。
エラーの中身を読む
失敗したときは、HTTPError が起きます。Pythonの公式ドキュメントでは、この例外はレスポンスとしても使え、read() で本文を読めると説明されています。Notionの公式ドキュメントでは、エラーの本文には code と message が入ります。HTTPのステータス(404など)は e.code で取れます。
except urllib.error.HTTPError as e:
body = json.loads(e.read().decode("utf-8", errors="replace"))
print(e.code, body.get("code"), body.get("message"))
| 状態 | code | 公式の説明 |
|---|---|---|
| 400 | validation_error | リクエストの本文が想定の形と合わない。messageを見る |
| 401 | unauthorized | トークンが正しくない |
| 404 | object_not_found | そのトークンからは存在しない。共有されていない場合もある |
| 429 | rate_limited | リクエストが多すぎる。間を空けて再試行する |
上の例では失敗したら終了コード1で止めていますが、私の実際のスクリプトは、自動実行の失敗を知らせるための「おまけ」の処理なので、書き込みに失敗しても止めず、ステータスと本文を標準エラー(sys.stderr)に出すだけにしています。ただし、書き込みの失敗を黙って流すと、何日も気づけません。自動実行の失敗に気づく仕組みはタスクスケジューラの失敗に気づく方法 ― 自動実行を毎朝一覧で確かめるで扱っています。
まとめ
- Notion APIへの書き込みは、JSONを1回POSTするだけ。
urllib.requestで足り、pipもvenvも要らない - 先にインテグレーションを作り、書き込み先に「コネクトを追加」する。トークンは環境変数か.envに置き、コードに直書きしない
- 必須ヘッダーは
Authorization: BearerとNotion-Version。データベースに行を足すときは、2025-09-03以降は公式ガイドの案内どおりdata_source_idで指定する - 失敗は
HTTPErrorの本文(codeとmessage)を読めば、原因が分かる