快轉到主要內容
  1. 教學文章/

Python tomllib 實戰:內建 TOML 解析、設定檔管理與 pyproject.toml 完全攻略

·7 分鐘· loading · loading · ·
Python Tomllib TOML 設定檔 Pyproject.toml
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 50: 本文

TOML 是 Python 生態系越來越常見的設定檔格式。 pyproject.toml、Ruff、Mypy、pytest、uv、Poetry、Hatch……好多工具都用它。 以前在 Python 裡讀 TOML,通常要另外安裝 tomlitoml。 但從 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,再加 tomli fallback
  • 讀取 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:輕量寫出 TOML
  • tomlkit:保留註解與格式,適合編輯既有 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-wtomlkit

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-wtomlkit

設定檔是專案的入口之一。 入口整理好,後面就不容易踩到自己的鞋帶。 今天也把工具箱補上一個小而鋒利的工具吧。拍拍!

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 50: 本文

相關文章

Python tenacity 實戰:重試、退避與容錯機制完全攻略
·9 分鐘· loading · loading
Python Tenacity Retry Backoff 容錯
Python loguru 實戰:告別複雜的 logging 設定,寫出漂亮的日誌
·6 分鐘· loading · loading
Python Logging Loguru 除錯 工具
Python Packaging:從 pyproject.toml 到發佈 PyPI 全攻略
·7 分鐘· loading · loading
Python Packaging Pyproject.toml Pypi Pip Uv Setuptools
Python match/case:結構化模式匹配完全攻略
·6 分鐘· loading · loading
Python Match Pattern-Matching Python 3.10
Python enum:打造型別安全的常數管理
·5 分鐘· loading · loading
Python Enum 型別安全 設計模式
Python contextlib:掌握 Context Manager 的進階魔法
·7 分鐘· loading · loading
Python Contextlib Context Manager With 資源管理