本文 AI 產出,尚未審核

Python – 型別提示與靜態分析(Type Hints & Static Analysis)

主題:pydantic 型別驗證


簡介

在 Python 的動態型別系統中,型別錯誤往往只有在程式執行時才會被發現,這對大型專案的維護與除錯造成極大挑戰。隨著 Python 3.5 以後逐步引入型別提示(type hints),開發者可以在編寫程式時即表達變數、函式參數與回傳值的預期型別,配合 mypypyright 等靜態分析工具,提前捕捉潛在的型別問題。

然而,僅靠型別提示只能驗證「結構」是否符合預期,資料本身的合法性(例如字串長度、數值範圍、Email 格式)仍需額外檢查。此時,pydantic 這個以 型別註解為核心、提供自動資料驗證與轉換的套件,成為 Python 生態系統中最受歡迎的資料模型工具之一。本文將從概念、範例、常見陷阱與最佳實踐,帶你一步步掌握 pydantic 的型別驗證能力,並說明它在真實專案中的應用方式。


核心概念

1. pydantic 的設計哲學

  • 以型別提示為唯一來源:所有欄位的驗證規則皆由 typing 模組的型別提示(如 intList[str]Literal)自動推導。
  • 資料自動轉換(coercion):傳入的資料會盡可能被轉成目標型別,例如字串 "123" 會被轉成 int
  • 錯誤資訊友好:驗證失敗時會產生結構化的 ValidationError,包含錯誤位置、預期型別與實際值,方便除錯與 API 回傳。

2. 基本模型(BaseModel)

pydantic 透過繼承 pydantic.BaseModel 來建立資料模型。模型的每個屬性即是要驗證的欄位,型別提示決定驗證行為。

from pydantic import BaseModel

class User(BaseModel):
    id: int                 # 必填,會自動轉成 int
    name: str               # 必填,必須是字串
    email: str | None = None   # 可選欄位,預設為 None

3. 欄位驗證與預設值

  • 預設值:若欄位提供預設值,該欄位變為可選。
  • Field 進階設定:使用 pydantic.Field 可加入描述、別名、範圍限制等額外資訊。
from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(..., description="商品名稱")
    price: float = Field(..., gt=0, description="商品價格,必須大於 0")
    tags: list[str] = Field(default_factory=list, description="標籤列表")
  • ... 表示「必填」且無預設值。

4. 內建驗證器與自訂驗證器

4.1 內建驗證器

pydantic 內建多種型別(如 EmailStr, IPv4Address, HttpUrl)會自動執行格式驗證。

from pydantic import BaseModel, EmailStr

class Contact(BaseModel):
    email: EmailStr          # 嚴格檢查 Email 格式
    website: HttpUrl | None = None

4.2 自訂驗證器

使用 @validator 裝飾器可針對單一或多個欄位寫自訂驗證邏輯。

from pydantic import BaseModel, validator

class Order(BaseModel):
    quantity: int
    price_per_item: float

    @validator('quantity')
    def quantity_must_be_positive(cls, v):
        if v <= 0:
            raise ValueError('quantity 必須大於 0')
        return v

    @validator('price_per_item')
    def price_positive(cls, v):
        if v <= 0:
            raise ValueError('price_per_item 必須大於 0')
        return v

5. 產生模型實例的流程

  1. 傳入任意資料(字典、JSON、ORM 物件等)。
  2. pydantic 依序執行型別轉換(coercion)。
  3. 驗證器(內建或自訂)檢查資料合法性。
  4. 若全部通過,返回 模型實例;否則拋出 ValidationError
raw_data = {"id": "42", "name": "Alice", "email": "alice@example.com"}
user = User(**raw_data)          # id 會被轉為 int,驗證成功
print(user)                      # User id=42 name='Alice' email='alice@example.com'

程式碼範例

以下提供 5 個實用範例,涵蓋基礎模型、嵌套模型、列表驗證、JSON 解析與自訂驗證。

範例 1:基礎模型與預設值

from pydantic import BaseModel, Field

class BlogPost(BaseModel):
    title: str = Field(..., min_length=5, description="文章標題,至少 5 個字")
    content: str
    published: bool = False          # 預設為未發佈

post = BlogPost(title="Hello", content="第一篇部落格")
print(post)
# > BlogPost title='Hello' content='第一篇部落格' published=False

範例 2:嵌套模型(Nested Model)

from pydantic import BaseModel, EmailStr

class Author(BaseModel):
    name: str
    email: EmailStr

class Article(BaseModel):
    author: Author
    tags: list[str] = []

article = Article(
    author={"name": "Bob", "email": "bob@example.com"},
    tags=["python", "pydantic"]
)
print(article.author.email)   # bob@example.com

範例 3:列表與集合驗證

from pydantic import BaseModel, conint, validator

class ScoreBoard(BaseModel):
    scores: list[conint(gt=0, lt=101)]   # 每個分數必須在 1~100 之間

    @validator('scores')
    def no_duplicate_scores(cls, v):
        if len(v) != len(set(v)):
            raise ValueError('分數不可重複')
        return v

board = ScoreBoard(scores=[95, 88, 76])
print(board)
# > ScoreBoard scores=[95, 88, 76]

範例 4:從 JSON 字串建立模型

import json
from pydantic import BaseModel, HttpUrl

class WebHook(BaseModel):
    url: HttpUrl
    secret: str | None = None

json_str = '{"url": "https://example.com/hook", "secret": "s3cr3t"}'
data = json.loads(json_str)
hook = WebHook(**data)
print(hook)
# > WebHook url=HttpUrl('https://example.com/hook', ...) secret='s3cr3t'

範例 5:自訂驗證器檢查相依欄位

from pydantic import BaseModel, validator, root_validator

class PasswordChange(BaseModel):
    old_password: str
    new_password: str
    confirm_password: str

    @validator('new_password')
    def password_strength(cls, v):
        if len(v) < 8:
            raise ValueError('新密碼至少 8 個字元')
        return v

    @root_validator
    def passwords_match(cls, values):
        if values.get('new_password') != values.get('confirm_password'):
            raise ValueError('新密碼與確認密碼不相符')
        return values

# 下面會拋出 ValidationError
# PasswordChange(old_password='old', new_password='short', confirm_password='short')

常見陷阱與最佳實踐

陷阱 說明 解決方式
1. 忽略 Field(... ) 的必填語意 ... 表示「必填且無預設值」,若寫成 field: int = 0 會把欄位變成可選,導致意外的資料遺漏。 明確使用 Field(... )Annotated 來標示必填欄位。
2. 過度依賴自動型別轉換 pydantic 會把 "123" 轉成 int,但在某些情境(例如 API 輸入)可能不希望自動 coercion。 使用 StrictInt, StrictStr 等「嚴格」型別,或設定 Config.validate_assignment = True
3. 大型模型的效能問題 每次建立模型都會跑完整驗證,對高頻率的資料流(如訊息佇列)可能造成瓶頸。 1) 只在入口層做驗證,之後傳遞已驗證的模型。2) 使用 model_copy(update=...) 以避免重複驗證。
4. 嵌套模型的循環引用 兩個模型互相引用會導致遞迴錯誤。 使用 ForwardRef 或在 Python 3.11+ 直接使用字串型別提示("ModelA")。
5. 忽視 Config 設定 默認情況下,模型允許額外欄位被忽略,可能掩蓋錯誤。 設定 class Config: extra = "forbid" 讓未定義欄位直接拋錯。

最佳實踐

  1. 在 API 輸入層使用 pydantic,返回層使用原始 dict:利用 model.dict(by_alias=True) 產生符合外部規範的資料。
  2. 配合靜態分析工具:在 IDE 中啟用 pyrightmypy,讓型別提示與 pydantic 的驗證相輔相成。
  3. 分層驗證:將「結構驗證」交給 pydantic,將「業務規則」寫在服務層或自訂 validator 中,保持模型的單一職責。
  4. 使用 Union / Literal 明確列舉可接受的值:提升錯誤訊息可讀性。

實際應用場景

場景 為何適合使用 pydantic 範例
FastAPI REST API FastAPI 原生支援 pydantic 作為請求/回應模型,提供自動文件與驗證。 def create_user(user: UserCreate): ...
設定檔讀取 透過 BaseSettings 可直接從環境變數、.env 或 JSON 檔載入設定,並自動驗證型別。 class Settings(BaseSettings): db_url: PostgresDsn
資料庫 ORM 轉換 pydantic 可與 SQLModel、Tortoise-ORM 等結合,將 ORM 物件轉成乾淨的 DTO。 UserDTO.from_orm(user_orm)
訊息佇列(Kafka / RabbitMQ) 消費者在收到訊息後先用 pydantic 解析,確保訊息格式正確再進行業務處理。 event = EventMessage.parse_raw(message_body)
資料分析前的清理 大批 CSV/Excel 資料匯入時,用 pydantic 檢查每列資料是否合法,避免後續分析錯誤。 RowModel(**row_dict)

總結

pydantic 以 型別提示為核心,結合自動型別轉換、豐富的內建驗證器與可自訂的驗證邏輯,為 Python 開發者提供了一套既簡潔又強大的資料驗證解決方案。在日常開發中,將 pydantic 作為 入口層的防護牆(API 請求、設定檔、訊息佇列),配合靜態分析工具與良好的模型設計原則,可大幅降低因資料不正確而導致的錯誤與除錯成本。

關鍵要點

  • 使用 BaseModel 定義資料結構,型別提示即自動產生驗證。
  • 透過 Field、內建型別(如 EmailStr)與 @validator 提供細緻的限制。
  • 設定 Config.extra = "forbid"Strict* 型別,避免隱藏錯誤。
  • 結合 FastAPI、SQLModel、BaseSettings 等生態系工具,讓驗證成為整體架構的一部份。

掌握上述概念與實務技巧後,你將能在 Python 專案中以最少的程式碼,達到最高的資料安全與可維護性。祝你在開發旅程中玩得開心、寫得更安心!