故事要從 Python 的語言特性說起,因為 Python 是動態型別的,所以一個在同一個區塊(命名空間)內的變數 x,可以一下是字串,一下是整數:

x = 4
print(type(4)) # at this moment, x points to an integer

x = "Hello, world"
print(type(x)) # and at this moment, x points to a string

這樣的特性降低了 Python 的學習曲線,但在程式專案變複雜後,對於「值」與「型態」的掌握會越來越難以駕馭。

大約在 Python 3.5 起引入了 type hints,華文叫類型提示或型態提示,它讓我們得以聲明變數的型態,也可以聲明函式回傳值的型態。

有了 type hints,IDE 或是靜態分析工具就可以幫助我們偵測型態上的錯誤,也因為它只是 hints,所以它終究無法讓 Python 變成靜態型別語言,只是一種輔助而已。

下面是最基本的 type hints 範例:

name: str = "world"

def greeting(name: str) -> str:
    return "Hello " + name

greeting(name)

上面我們用 : str 聲明 name 為字串,以及在函式我們用 -> str 聲明 greeting() 回傳的也是字串,當然還有更複雜的用法,可以參見〈Python Type Hints 從入門到實踐〉。

以上為前情提要,下面開始談本文的主角 Pydantic

Pydantic

Pydantic 是以 type hints 為基礎,幫我們做資料型態驗證的套件,我們可以用它定義「Model」:

from pydantic import BaseModel

class EmployeeModel(BaseModel):
    name: str
    salary: int

在這個 EmployeeModel 中,我們聲明了 name 應為字串、salary 應為整數,透過繼承自 BaseModel 的特性,pydantic 會自動幫我們驗證型態的正確性。

如果試圖建立一個實例,並提供正確的型態,那不會有任何問題(也不會有任何提示說我們好棒棒):

employee = EmployeeModel(
    name='Bar',
    salary=1000,
)

但如果給了一個錯誤的型態,那麼 pydantic 會引發錯誤:

employee = EmployeeModel(
    name='Bar',
    salary='secret',
)

應該要是整數的 salary,卻被塞了字串,引發 validation error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/leon/Projects/pydantic/.venv/lib/python3.10/site-packages/pydantic/main.py", line 406, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for EmployeeModel
salary
  value is not a valid integer (type=type_error.integer)

Pydantic + FastAPI

在應用上,新興的 Python 框架 FastAPI 深度整合了 pydantic 的類型驗證特性,只要在程式碼內事先聲明類型,所有的 API 端點都自動的具備型態驗證機制,我輩開發者再也不用自己刻 e-mail、URL 等等繁瑣的驗證邏輯,香!

下面是個極簡的 FastAPI 範例:

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    return item

在 Item model 中,我們定義了四個屬性,其中的 descriptiontax 的 type hints 是 Optional[str],而預設值為 NoneOptional[str] 意味著此屬性是選填的,如果建立實例時未填,則屬性值為 None

最後一個區塊定義了一個接受 POST 的 API 端點 /items/,此端點的處理函式 create_item(item: Item) 接受一個參數 item,並且該參數之型態應為前面定義的 Item model,在這樣的的定義與聲明下,前端送過來的 request body 會自動的被 pydantic 解析與驗證,並且那 FastAPI 自動產生的 OpenAPI 文件也會載明該端點的 JSON schema:

FastAPI

FastAPI

Pydantic 與 .env 環境變數管理

在開發環境(development)與生產環境(production)採用的參數可能不同,例如連接 Twitter API 的帳密、連接 MongoDB 的帳密等等,這類參數習慣上會把它與程式碼分開,另外放在 .env 檔案內,再透過像 python-dotenv 這樣的套件把 .env 讀入成為可調用的變數。Pydantic 也整合了 python-dotenv,可以幫我們做到上述工作,這部份的用法請參閱下面兩篇:

說個題外話,關於 .env 檔案,有一條常見的鐵律是不要把 .env 提交到版控系統,個人是不太認同,只要我的 Git repository 是私有的,自架的,本機的,提交到 Git repository 又何妨?版控≠開源,當我的專案是閉源的,.env 的地位就與任何一份原碼一樣隱私,不具有特殊地位,我們要守護的也不僅是 .env,而是整個專案。

結語

綜觀以上,只要在程式碼內定義好型態,就可以享有:

  • IDE 或靜態分析工具幫我們揪錯,以及程式碼的可讀性更高,大型專案更好維護
  • Pydantic 幫我們檢查打來 API 的值的型態正確與否,省去自己寫驗證邏輯的功夫,time to market 時間再省一半
  • FastAPI 自動根據我們定義的型態產生 OpenAPI 文件與規格,再也不用寫 API 規格書,再也不用回覆前端同事的私訊

參考資料