在 Day 2–9,我們把「能跑且一致」的地基(環境、依賴、可重現、工作流、風格)架好了。接下來要把正確性前移到更前面:
pyproject.toml
一次設定好工具行為;本機以 Hatch 指令快速執行。typecheck
與 contracts
工作流,接到 CI。pyproject.toml
設定dev
類別)[project.optional-dependencies]
dev = [
"pyright>=1.1.375",
"mypy>=1.10",
"pydantic>=2.8",
"pydantic-settings>=2.3", # 若需讀環境變數
"pytest>=8.0"
]
[tool.pyright] # 若改用 basedpyright,區塊名為 [tool.basedpyright]
pythonVersion = "3.11"
typeCheckingMode = "strict"
reportMissingTypeStubs = "warning"
[tool.mypy]
python_version = "3.11"
strict = true
warn_unused_ignores = true
warn_redundant_casts = true
no_implicit_optional = true
disallow_any_generics = true
# 視需要:Pydantic mypy 外掛
plugins = ["pydantic.mypy"]
口訣:先嚴格再標註。先把規則拉高,用型別註記與局部忽略(# type: ignore[code])收斂歷史區塊。
from typing import Annotated, Literal
from pydantic import BaseModel, Field, field_validator, ValidationError
Username = Annotated[str, Field(min_length=3, max_length=20)]
Role = Literal["admin", "staff", "guest"]
class User(BaseModel):
id: int
username: Username
email: Annotated[str, Field(pattern=r"^[^@]+@[^@]+\.[^@]+$")]
role: Role = "guest"
@field_validator("username")
@classmethod
def no_spaces(cls, v: str) -> str:
if " " in v:
raise ValueError("username must not contain spaces")
return v
try:
User(id=1, username="a b", email="x@example.com")
except ValidationError as e:
# e.errors() 為結構化列表,方便記錄或回應 API 錯誤
print(e.errors())
from pydantic import BaseModel, model_validator
class PasswordPair(BaseModel):
password1: str
password2: str
@model_validator(mode="after")
def passwords_match(self):
if self.password1 != self.password2:
raise ValueError("passwords do not match")
return self
TypeAdapter
from typing import List
from pydantic import TypeAdapter
ta = TypeAdapter(List[int])
assert ta.validate_python(["1","2","3"]) == [1,2,3]
argparse
)import argparse
from pydantic import BaseModel, TypeAdapter
class Args(BaseModel):
host: str
port: int
parser = argparse.ArgumentParser()
parser.add_argument("--host", required=True)
parser.add_argument("--port", required=True)
ns = parser.parse_args()
args = TypeAdapter(Args).validate_python(vars(ns))
# 從這刻起,args 就是型別安全+驗證過的契約資料
pydantic-settings
)from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="APP_")
host: str = "127.0.0.1"
port: int = 8000
debug: bool = False
settings = AppSettings() # 自動讀取 APP_HOST, APP_PORT, APP_DEBUG
from fastapi import FastAPI
from pydantic import BaseModel
class CreateUser(BaseModel):
username: str
email: str
app = FastAPI()
@app.post("/users")
def create_user(payload: CreateUser):
# payload 已驗證並轉型完成
return {"ok": True}
[tool.hatch.envs.default.scripts]
start = "python -m demo_app.main"
test = "pytest -q"
lint = "ruff check ."
format = "black ."
type-pyright = "pyright ."
type-mypy = "mypy ."
typecheck = [
"pyright .",
"mypy src/",
]
contracts = "pytest -q tests/contracts"
# noxfile.py
import nox
@nox.session(name="typecheck", venv_backend="none")
def typecheck(session: nox.Session) -> None:
"""Run static type checks (pyright + mypy) using current Hatch env."""
session.run("pyright", ".")
session.run("mypy", "src/")
@nox.session(name="contracts", venv_backend="none")
def contracts(session: nox.Session) -> None:
"""Run contract tests (Pydantic models)."""
session.run("pytest", "-q", "tests/contracts")
@nox.session(name="tests-3.11", venv_backend="none")
def tests_311(session: nox.Session) -> None:
"""
Run tests (labelled 3.11) using the CURRENT env.
建議在 CI job 裡用 setup-python matrix 指定 3.11 再呼叫此 session。
"""
session.run("pytest", "-q")
# .pre-commit-config.yaml 片段
- repo: local
hooks:
- id: typecheck-pyright
name: pyright
entry: pyright
language: system
pass_filenames: false
- id: typecheck-mypy
name: mypy
entry: mypy
language: system
pass_filenames: false
上述會是直接使用pyright和mypy,但我們使用了hatch我們只要定義在pyproject.toml然後在pre-commit中設定呼叫即可,就像下面範例。
repos:
- repo: local
hooks:
- id: hatch-fmt
name: hatch fmt
entry: hatch
language: system
args: ["run", "fmt"]
types: [python]
pass_filenames: false
- id: hatch-check
name: hatch check
entry: hatch
language: system
args: ["run", "check"]
types: [python]
pass_filenames: false
- id: hatch-typecheck
name: hatch typecheck
entry: hatch
language: system
args: ["run", "typecheck"]
types: [python]
pass_filenames: false
這些設定都完成後,我們在git commit時,就會幫我們的團隊所撰寫的程式碼進行檢查,確保我們程式碼是有符合規定的。
# type: ignore[code]
擋住,逐步還債。Any
擴散 → 在 mypy 啟用 disallow_any_generics
、在 pyright 觀察 reportAny*
系列診斷。tests/contracts/
)。pyproject.toml
為單一事實來源,Hatch/Nox/CI 全吃同一份設定。今天我們加入了更多的檢查,這為了確保我們未來在團隊合作的時候,大家寫出來的程式碼可以較為相近,不能保證百分之百,但可以提可閱讀性。在未來讓不同的人接手的時候都能有一個脈絡的快速理解我們這些程式碼到底在幹什麼。
其實到了今天第十天前置作業才算真正結束,這些前置作業的過程中我們其實針對每一個工具都還能更深入的剖析,但我們做為一個較為全局觀來看待的系列文章,要讓大家可以先有全局,再來後續逐步深入,也就是這只是一把鑰匙,在未來還有更多的領域要探索。
而第十一天開始我們先進入道測試領域的部分,讓我們明天繼續吧。
TypeAdapter
、mypy 外掛與遷移要點typeCheckingMode
、report*
規則與 pyproject.toml
設定strict
常見旗標、逐步嚴格化實務