先前 用 pydantic 輕鬆進行設定管理(Settings management) 一文曾提及利用 pydantic 管理設定,但其實也可以驗證資料,本文將介紹如何透過 pydantic 驗證資料,保證有效減少開發驗證各種資料的繁瑣。

本文環境

  • Python 3.7
  • pydantic 1.7.3
$ pip install pydantic==1.7.3

Hello, BaseModel

pydantic 提供 BaseModel 讓開發者能夠透過繼承該類別並且利用 typing 註記類別屬性的型別,就能夠擁有基本的驗證功能。

例如以下範例定義 EmployeeModel 類別,並分別為 name, username, salary, habits 註記(annotation)類別屬性 str, str, int, List[str], 最後實際初始化該類別後指定給 employee 變數:

from typing import List

from pydantic import BaseModel, ValidationError, validator


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: int
    habits: List[str]


employee = EmployeeModel(
    name='Bar',
    username='bar',
    salary=1000,
    habits=[]
)

上述範例執行成功的話,並不會有任何錯誤。但如果故意將 salary1000 改為 secret 的話,就會發現 pydantic 驗證 salary 不是合法的整數:

from typing import List

from pydantic import BaseModel, ValidationError, validator


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: int
    habits: List[str]


employee = EmployeeModel(
    name='Bar',
    username='bar',
    salary='secret',
    habits=[]
)

上述範例執行後將會出現以下錯誤,可以發現錯誤提示 salary 不是一個合法的整數,運用 pydantic 所提供的驗證功能,會更加方便開發:

Traceback (most recent call last):
  File "pydantic_test.py", line 17, in <module>
    habits=[]
  File "pydantic/main.py", line 338, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for EmployeeModel
salary
  value is not a valid integer (type=type_error.integer)

資料轉型

pydantic 在驗證 str, int 等型別時,將會試圖轉型,如果該值能夠被正常轉型,那麼也就不會出現任何錯誤,例如以下範例, salary 期望的是整數值,但我們故意傳入字串形態的 '1000' 也不會出現任何錯誤:

from typing import List

from pydantic import BaseModel, ValidationError, validator


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: int
    habits: List[str]


EmployeeModel(
    name='Bar',
    username='bar',
    salary='1000',
    habits=[]
)

原因在於 '1000' 是能夠被 int() 正確轉成整數 1000 的,因此並不會出現任何驗證錯誤,詳見 Data Conversion

但如果我們想得到與上述註記的型別符合的資料,可以呼叫 .dict() , .json() 等方法(詳見 Exporting models), pydantic 會確保輸出的資料符合註記的型別,例如以下範例:

from typing import List

from pydantic import BaseModel, ValidationError, validator


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: int
    habits: List[str]


employee = EmployeeModel(
    name='Bar',
    username='bar',
    salary='1000',
    habits=[]
)
print('output>', employee.dict())

上述範例輸出結果如下,可以看到我們傳入的字串 '1000' 被正確輸出為整數 1000:

output> {'name': 'Bar', 'username': 'bar', 'salary': 1000, 'habits': []}

Strict types

不過也由於資料轉型的特性,使得不合法型態的資料有機會被輸入並輸出,因此 pydantic 也提供Strict Types ,讓開發者能夠解決像前述在需要傳入 int 型別的地方,嚴格限制只有 int 型別才能傳入。

例如以下使用 StrictInt 註記 salary 屬性,如此一來就算是傳入字串 '1000' 也能夠正確驗證:

from typing import List

from pydantic import BaseModel, ValidationError, validator, StrictInt


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: StrictInt
    habits: List[str]


EmployeeModel(
    name='Bar',
    username='bar',
    salary='1000',
    habits=[]
)

上述範例故意傳入字串 '1000'salary ,在 StrictInt. 的作用下成功驗證出該型別不符:

Traceback (most recent call last):
  File "pydantic_test.py", line 17, in <module>
    habits=[]
  File "pydantic/main.py", line 362, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for EmployeeModel
salary
  value is not a valid integer (type=type_error.integer)

除了 StrictInt 之外, pydantic 也提供許多較嚴格的型別,如有需求可以翻閱 Strict Types

More pydantic types

Pydantic 也提供許多方便的型別,減少開發者一直重複開發驗證相關的程式,提高開發效率,例如以下範例運用 pydantic 的 HttpUrl 用以驗證 blog 屬性是 http 或者 https 開頭的合法 URL:

HttpUrl: schema http or https, TLD required, max length 2083

from typing import List, Optional

from pydantic import BaseModel, ValidationError, validator, StrictInt, HttpUrl


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: StrictInt
    habits: List[str]
    blog: Optional[HttpUrl]


EmployeeModel(
    name='Bar',
    username='bar',
    salary=1000,
    habits=[],
    blog="https://example.com"
)

更多 pydantic 提供的型別可參閱 Pydantic types ,其中包含 Email, Path, Json, IP 等多種實用的型別。

Model 設定

除了設定型別之外, pydantic 也提供一些設定能夠改變 Model 的行為。例如 pydantic BaseModel 預設忽略任意新增的屬性,也就是在下列範例中的 new_attribute 會被自動排除:

from typing import List, Optional

from pydantic import BaseModel, ValidationError, validator, StrictInt, HttpUrl


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: StrictInt
    habits: List[str]
    blog: Optional[HttpUrl]


e = EmployeeModel(
    name='Bar',
    username='bar',
    salary='1000',
    habits=[],
    blog="https://example.com",
    new_attribute='Haha',
)
print(e)

上述範例執行結果如下,可以發現 new_attribute 被自動排除:

{'name': 'Bar', 'username': 'bar', 'salary': 1000, 'habits': [], 'blog': HttpUrl('https://example.com', scheme='https', host='example.com', tld='com', host_type='domain')}

如果要改變上述行為,可以在 Model 內定義一個 Config 類別並設定相關屬性,例如以下範例將 extra 設定為 allow , 如此一來就能夠允許新增任意屬性:

from typing import List, Optional

from pydantic import BaseModel, ValidationError, validator, StrictInt, HttpUrl


class EmployeeModel(BaseModel):
    name: str
    username: str
    salary: StrictInt
    habits: List[str]
    blog: Optional[HttpUrl]

    class Config:
        extra = 'allow'


e = EmployeeModel(
    name='Bar',
    username='bar',
    salary='1000',
    habits=[],
    blog="https://example.com",
    new_attribute='Haha',
)
print(e)

上述範例執行結果如下,可以發現 new_attribute 出現了:

{'name': 'Bar', 'username': 'bar', 'salary': 1000, 'habits': [], 'blog': HttpUrl('https://example.com', scheme='https', host='example.com', tld='com', host_type='domain'), 'new_attribute': 'Haha'}

此外,如果不允許任意新增屬性的話,可以將 allow 改為 forbid 。屆時如果遇到額外的屬性,就會出現類似以下的錯誤,提示 extra fields not permitted 告訴我們 model 中含有不被允許的額外屬性:

Traceback (most recent call last):
  File "pydantic_test.py", line 23, in <module>
    new_attribute='Haha',
  File "pydantic/main.py", line 338, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 2 validation errors for EmployeeModel
salary
  value is not a valid integer (type=type_error.integer)
new_attribute
  extra fields not permitted (type=value_error.extra)

以上僅是如何設定 model 的 1 個範例,關於更多 model 的設定,可以參閱 Model Config

自製 Validator

除了透過型別註記(type annotation)驗證資料之外, pydantic 也允許開發者實作 validator 達到更細緻的驗證,例如下列範例利用裝飾子 validator 實作驗證 username, password 2 個欄位的方法,並在驗證未通過時 raise exception 代表資料未通過驗證,使用起來十分簡單易懂:

from pydantic import BaseModel, ValidationError, validator


class UserModel(BaseModel):
    username: str
    password: str

    @validator('password')
    def password_rules(cls, v, values, **kwargs):
        if len(v) < 8:
            raise ValueError('password too short')
        if v.isalnum() or len(set(v)) <= 3:
            raise ValueError('password too simple')
        return v

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v

當然 pydantic 也不只提供 validator 而已,也提供各種不同用途的 validator 可供使用,例如 root_validator 可一起驗證 model 內所有的屬性。

更多的 validator 使用方法請詳見 Validators .

以上就是如何使用 pydantic 驗證資料的簡短介紹。

Happy Coding!

References

https://pydantic-docs.helpmanual.io/