TOML 是 Python 生態系越來越常見的設定檔格式。
pyproject.toml、Ruff、Mypy、pytest、uv、Poetry、Hatch……好多工具都用它。
以前在 Python 裡讀 TOML,通常要另外安裝 tomli 或 toml。
但從 Python 3.11 開始,標準函式庫內建了 tomllib。
這篇拍拍君要帶你把 tomllib 用起來:從讀設定檔、處理型別,到解析 pyproject.toml,最後做一個實用的設定載入器。
一. 前言:為什麼是 TOML? #
TOML 很適合「人類要手寫、程式要讀取」的設定檔。 它比 JSON 更適合寫註解,比 YAML 規則更明確,也比 INI 更適合巢狀結構。
[app]
name = "拍拍記帳本"
debug = true
workers = 4
[database]
url = "sqlite:///data.db"
timeout = 30.0
[features]
flags = ["export", "dark-mode", "sync"]
對人類來說,它看起來像設定檔。 對程式來說,它解析後就是普通的 Python dict。 這就是 TOML 的甜蜜點。
二. 安裝與版本 #
tomllib 是 Python 3.11 加入標準函式庫的模組。
如果你用的是 Python 3.11+:
import tomllib
就可以直接使用,不需要安裝任何套件。
如果你還在 Python 3.10 或更舊版本,可以用相容套件 tomli:
python -m pip install tomli
然後寫成這種相容匯入:
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
拍拍君建議:
- 新專案若可以,直接使用 Python 3.11+
- 函式庫若需要支援舊版 Python,再加
tomlifallback - 讀取 TOML 時優先使用這組標準介面
三. 第一個 tomllib 範例 #
假設我們有一個 config.toml:
[app]
name = "daily-pypy"
debug = false
port = 8000
[logging]
level = "INFO"
json = true
用 tomllib.load() 讀取:
from pathlib import Path
import tomllib
config_path = Path("config.toml")
with config_path.open("rb") as f:
config = tomllib.load(f)
print(config["app"]["name"])
print(config["logging"]["level"])
注意一個很重要的小細節:
tomllib.load()需要 binary file object,所以要用"rb"。
不是 "r",也不是 encoding="utf-8"。
解析結果會是巢狀 dict,TOML 的布林、整數、浮點數也會轉成對應的 Python 型別。
四. loads():從字串解析 TOML
#
如果 TOML 內容不是來自檔案,而是測試資料或動態字串,可以用 tomllib.loads()。
import tomllib
raw = """
[server]
host = "127.0.0.1"
port = 8080
reload = true
"""
config = tomllib.loads(raw)
print(config["server"]["host"])
print(config["server"]["port"])
print(config["server"]["reload"])
輸出:
127.0.0.1
8080
True
load() 和 loads() 的差別很單純:
| 函式 | 輸入 | 常見用途 |
|---|---|---|
tomllib.load(f) |
binary file object | 讀設定檔 |
tomllib.loads(s) |
str |
測試、字串內容 |
跟 json.load() / json.loads() 的命名邏輯很像。
五. TOML 型別會變成什麼? #
TOML 支援的型別比 INI 豐富很多。
tomllib 會幫你轉成合理的 Python 型別。
title = "拍拍任務"
enabled = true
retry = 3
timeout = 2.5
tags = ["python", "tooling", "config"]
released = 2026-04-24
started_at = 2026-04-24T12:08:00+08:00
常見對照如下:
| TOML | Python |
|---|---|
| string | str |
| integer | int |
| float | float |
| boolean | bool |
| array | list |
| table | dict |
| date | datetime.date |
| time | datetime.time |
| datetime | datetime.datetime |
這也是 TOML 適合設定檔的原因:不用自己把 "true" 轉成 True,不用自己把 "8000" 轉成 8000。
六. 讀取 pyproject.toml #
Python 專案最常見的 TOML 檔,大概就是 pyproject.toml。
[project]
name = "pypy-tools"
version = "0.1.0"
description = "Small tools for Daily Pypy examples"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27",
"rich>=13.0",
]
[tool.ruff]
line-length = 100
我們可以寫一個小工具讀出專案資訊:
from pathlib import Path
import tomllib
def read_project_metadata(path: Path = Path("pyproject.toml")) -> dict:
with path.open("rb") as f:
data = tomllib.load(f)
project = data.get("project", {})
return {
"name": project.get("name", "unknown"),
"version": project.get("version", "0.0.0"),
"requires_python": project.get("requires-python"),
"dependencies": project.get("dependencies", []),
}
這種小工具很適合放在 release script、文件產生器、CI 檢查流程裡。
拍拍君自己很喜歡用它來做「不要重複寫設定」。
版本號、專案名稱、支援 Python 版本都從 pyproject.toml 讀。
單一來源,少一點複製貼上,少一點凌晨三點的 bug。
七. 不要讓 dict 到處亂跑 #
tomllib 解析出來的是 dict。
這很方便,但如果整個專案到處都傳 dict,很快會遇到拼字錯誤和預設值散落各地。
if config["servre"]["prot"] == 8000:
...
這段看起來像程式碼,但其實充滿 typo。 比較好的做法是:讀取 TOML 後,轉成明確的設定物件。
from dataclasses import dataclass
from pathlib import Path
import tomllib
@dataclass(frozen=True)
class ServerConfig:
host: str
port: int
reload: bool = False
@dataclass(frozen=True)
class AppConfig:
name: str
server: ServerConfig
class ConfigError(ValueError):
pass
def load_config(path: Path) -> AppConfig:
with path.open("rb") as f:
data = tomllib.load(f)
try:
app = data["app"]
server = data["server"]
except KeyError as e:
raise ConfigError(f"missing section: {e.args[0]}") from e
return AppConfig(
name=str(app["name"]),
server=ServerConfig(
host=str(server.get("host", "127.0.0.1")),
port=int(server["port"]),
reload=bool(server.get("reload", False)),
),
)
使用時:
config = load_config(Path("config.toml"))
print(config.name)
print(config.server.host)
print(config.server.port)
這樣的好處是:
- 設定格式集中在一個地方處理
- 呼叫端拿到的是有結構的物件
- IDE 比較容易補完欄位
- 測試也比較好寫
tomllib 負責解析。
你的設定類別負責讓資料變可靠。
八. 加上基本驗證 #
上一節的範例已經比裸 dict 好,但還可以更穩。 例如 port 應該在 1 到 65535 之間,logging level 應該是固定幾種值。
VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
def parse_port(value: object) -> int:
port = int(value)
if not (1 <= port <= 65535):
raise ValueError(f"invalid server.port: {port}")
return port
def parse_log_level(value: object) -> str:
level = str(value).upper()
if level not in VALID_LOG_LEVELS:
raise ValueError(f"invalid logging.level: {level}")
return level
重點不是把驗證寫得多華麗。 重點是讓「設定檔錯了」這件事在程式啟動時就爆炸,而不是跑到一半才發現。 設定檔錯誤越早失敗,越便宜。
九. 巢狀 table 與 array of tables #
TOML 支援兩種很常用的結構:table 和 array of tables。
普通 table:
[database]
host = "localhost"
port = 5432
[database.pool]
min_size = 1
max_size = 10
解析後,database.pool.max_size 會變成:
data["database"]["pool"]["max_size"]
array of tables 則適合「多個同型物件」:
[[tasks]]
name = "lint"
command = "ruff check ."
[[tasks]]
name = "test"
command = "pytest"
解析後會得到 list:
[
{"name": "lint", "command": "ruff check ."},
{"name": "test", "command": "pytest"},
]
這種格式很適合寫內部工具的 task runner。 比起自己 invent 一個奇怪格式,TOML 省心很多。
十. tomllib 只讀不寫
#
這點非常重要:
tomllib只負責讀取 TOML,不負責寫出 TOML。
它沒有 dump(),也沒有 dumps()。
如果你需要把 Python 資料寫回 TOML,可以考慮:
tomli-w:輕量寫出 TOMLtomlkit:保留註解與格式,適合編輯既有 TOML
例如使用 tomli-w:
python -m pip install tomli-w
import tomli_w
config = {
"app": {"name": "拍拍服務", "debug": False},
"server": {"host": "127.0.0.1", "port": 8000},
}
print(tomli_w.dumps(config))
但如果你的需求是「修改 pyproject.toml 並保留原本註解」,拍拍君會偏向 tomlkit。
因為 tomllib 解析後只給你資料,不保留格式細節。
這是設計取捨,不是 bug。
十一. 實戰:小型 CLI 設定讀取器 #
假設我們要寫一個 CLI 工具,預設讀 pypy.toml:
[app]
name = "daily-pypy-cli"
[server]
host = "127.0.0.1"
port = 8000
Python 程式可以這樣組:
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import argparse
import tomllib
@dataclass(frozen=True)
class Settings:
app_name: str
host: str
port: int
class ConfigError(ValueError):
pass
def load_toml(path: Path) -> dict:
try:
with path.open("rb") as f:
return tomllib.load(f)
except FileNotFoundError as e:
raise ConfigError(f"config file not found: {path}") from e
except tomllib.TOMLDecodeError as e:
raise ConfigError(f"invalid TOML in {path}: {e}") from e
def parse_settings(data: dict) -> Settings:
app = data.get("app", {})
server = data.get("server", {})
port = int(server.get("port", 8000))
if not (1 <= port <= 65535):
raise ConfigError(f"server.port must be 1..65535, got {port}")
return Settings(
app_name=str(app.get("name", "pypy-app")),
host=str(server.get("host", "127.0.0.1")),
port=port,
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path, default=Path("pypy.toml"))
args = parser.parse_args()
try:
settings = parse_settings(load_toml(args.config))
except ConfigError as e:
parser.error(str(e))
print(f"app: {settings.app_name}")
print(f"server: {settings.host}:{settings.port}")
這個範例雖然小,但已經有正式專案需要的基本骨架:
- 檔案不存在時有友善錯誤
- TOML 格式錯誤時有清楚訊息
- 設定解析集中在一處
- CLI 可以指定不同 config path
你可以把這個模式拿去改成自己的工具。
十二. 什麼時候該用 Pydantic? #
如果設定結構很大,或需要比較複雜的驗證,手寫 dataclass 驗證會開始變累。 這時候可以考慮 Pydantic:
from pathlib import Path
import tomllib
from pydantic import BaseModel, Field
class ServerConfig(BaseModel):
host: str = "127.0.0.1"
port: int = Field(default=8000, ge=1, le=65535)
class Settings(BaseModel):
server: ServerConfig = ServerConfig()
with Path("config.toml").open("rb") as f:
data = tomllib.load(f)
settings = Settings.model_validate(data)
print(settings.server.port)
拍拍君的粗略建議是:
- 小工具:
tomllib+ dataclass 就很好 - 中大型服務:
tomllib+ Pydantic 會省很多驗證成本 - 需要環境變數、
.env、多層設定來源:考慮專門設定管理工具
工具不要過度,但也不要硬撐。
十三. 常見錯誤整理 #
1. 用文字模式開檔 #
錯誤:
with open("config.toml", "r", encoding="utf-8") as f:
data = tomllib.load(f)
正確:
with open("config.toml", "rb") as f:
data = tomllib.load(f)
2. 期待 tomllib 可以寫檔
#
tomllib.dumps(config) # 不存在
要寫 TOML 請用 tomli-w 或 tomlkit。
3. 忘記處理缺少 section 的情況 #
使用者的設定檔常常不會跟你想像的一樣完整。
所以請提供合理預設值、清楚錯誤訊息、啟動時檢查。
不要讓使用者看到一串神秘的 KeyError: 'server' 然後開始懷疑人生。
十四. 跟 JSON / YAML / INI 怎麼選? #
簡單比較一下:
| 格式 | 優點 | 缺點 | 適合 |
|---|---|---|---|
| JSON | 標準、跨語言、機器友善 | 不能註解、手寫不舒服 | API 資料交換 |
| YAML | 表達力強、人類可讀 | 規則複雜、縮排易踩雷 | Kubernetes、CI 設定 |
| INI | 簡單、歷史悠久 | 型別弱、巢狀不自然 | 很簡單的設定 |
| TOML | 明確、可註解、型別友善 | 不適合超複雜資料 | Python 專案設定 |
如果是 Python 專案設定檔,尤其跟工具鏈相關,TOML 幾乎是現在的預設答案。
pyproject.toml 已經把整個生態系推向這個方向了。
結語 #
tomllib 是一個很小但很實用的標準函式庫模組。
它不花俏,沒有寫檔功能,也不幫你做完整 schema validation。
但它把「讀 TOML」這件事做得乾淨、穩定、標準化。
拍拍君會這樣記:
- Python 3.11+ 直接
import tomllib - 用
open("rb")搭配tomllib.load() - 字串內容用
tomllib.loads() - 讀完後轉成 dataclass 或 Pydantic model
- 寫 TOML 請另外用
tomli-w或tomlkit
設定檔是專案的入口之一。 入口整理好,後面就不容易踩到自己的鞋帶。 今天也把工具箱補上一個小而鋒利的工具吧。拍拍!