Pydantic 小筆記
Tags: python, pydantic
Published: 2021-12-11 18:23:38

故事要從 Python 的語言特性說起，因為 Python 是[動態型別](https://zh.wikipedia.org/wiki/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B)的，所以一個在同一個區塊（命名空間）內的變數 `x`，可以一下是字串，一下是整數：

```py
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 範例：

```py
name: str = "world"

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

greeting(name)
```

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

以上為前情提要，下面開始談本文的主角 [Pydantic](https://pydantic-docs.helpmanual.io/) 。

## Pydantic

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

```py
from pydantic import BaseModel

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

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

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

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

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

```py
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 範例：

```py
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 中，我們定義了四個屬性，其中的 `description` 與 `tax` 的 type hints 是 `Optional[str]`，而預設值為 `None`，`Optional[str]` 意味著此屬性是選填的，如果建立實例時未填，則屬性值為 `None`。

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


![FastAPI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/317hr7lgpo1zvqnbjqjc.png)

![FastAPI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qrsvrxqrr7q54tl8tha8.png)

## Pydantic 與 .env 環境變數管理

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

- 〈[Pedantic Configuration Management with Pydantic](https://rednafi.github.io/digressions/python/2020/06/03/python-configs.html)〉
- 〈[用 pydantic 輕鬆進行設定管理（settings management）](https://myapollo.com.tw/zh-tw/python-pydantic/)〉


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

## 結語

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

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

## 參考資料

- 〈[Python is strongly, dynamically typed. What does that mean?](https://dev.to/icncsx/python-is-strongly-dynamically-typed-what-does-that-mean-5810)〉
- 〈[用 pydantic 輕鬆進行資料驗證](https://myapollo.com.tw/zh-tw/pydantic-validate-data/)〉