在 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
TypeAdapterfrom 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 常見旗標、逐步嚴格化實務