これは、なにをしたくて書いたもの?
前にPydanticを少し見てみました。
Pythonのデータバリデーションライブラリー、Pydanticを試す - CLOVER🍀
この時はGet Startedにほぼ習った感じでしたが、今回は扱える型について見ていこうと思います。
Types
Pydanticで扱える型については、こちらのページに書かれています。
方針としては、Pythonの標準ライブラリーの型を使ってフィールド定義するのが基本です。ただ、それだけでは足りないのでPydanticが
追加で型を実装しています。
Where possible Pydantic uses standard library types to define fields, thus smoothing the learning curve. For many useful applications, however, no standard library type exists, so Pydantic implements many commonly used types.
Python標準ライブラリーの型についてはこちらですね。
Standard Library Types - Pydantic
厳密な型もあり、独自の型も定義できるようです。
このページには見当たらないのですが、Pydanticが提供している型がこちらのようですね。
たとえば、こちらの例で書かれているPositiveIntですが
PositiveInt = Annotated[int, Field(gt=0)]
Types / Custom Types / Using the annotated pattern
まさしくPydanticのPositiveIntの定義になっています。
Pydantic Types / types / PositiveInt
こんな感じで独自の型を作るんですね。
これにはFieldというものが基盤になり、ここで様々な制約を定義できるようです。
さらに複雑な型を扱う場合として、Pydantic Extra Typesが紹介されているのですが。
GitHub - pydantic/pydantic-extra-types: Extra Pydantic types.
この他にPydanticもNetwork Typesというものを提供しているようですね。
データ型の変換規則…フィールドの型に対して、どのようなデータ型を(変換して)受け付けることができるのかは
こちらのページに表としてまとめられているので見ておくとよいでしょう。
Strictモードだと、ほとんど変換されなくなることがわかります。
Fields
こう見ていくと、Fieldについても見ておいた方がよさそうな気がしてきますね。
こういう感じでフィールドに代入して使うようです。
class Model(BaseModel): name: str = Field(frozen=True)
Fieldを使うことで、こんなことができるみたいです。
- デフォルト値を設定する
- フィールドのエイリアスを設定する
- 制約を設定する
- 数値制約(大小など)
- 文字列制約(文字列長、正規表現)
- 小数制約(最大桁数、最大小数点桁数)
- 対象のフィールドをモデルの文字列表現に含めるかどうか
- union型の識別
- Strictモードの設定
- 不変性
- モデルをダンプする時に除外対象とするかどうか
- フィールドを非推奨に設定する
- JSON Schemaのカスタマイズ
APIドキュメントはこちら。
こう見ると、制約の定義の方法もだいたいわかる感じですね。
では、少し試してみましょう。
環境
今回の環境はこちら。
$ python3 --version Python 3.12.3 $ uv --version uv 0.7.20
プロジェクトを作成する
最初にuvでプロジェクトを作成します。
$ uv init --vcs none pydantic-types-fields $ cd pydantic-types-fields $ rm main.py
不要なファイルは削除。
ライブラリーのインストール。
$ uv add pydantic
テストなどに必要なライブラリーをインストール。
$ uv add --dev pytest mypy ruff
確認はテストコードで行います。
pyproject.toml
[project]
name = "pydantic-types-fields"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.11.7",
]
[dependency-groups]
dev = [
"mypy>=1.16.1",
"pytest>=8.4.1",
"ruff>=0.12.3",
]
[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true
plugins = ["pydantic.mypy"]
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
PydanticのMypyプラグインも追加しています。
インストールされたライブラリーの一覧。
$ uv pip list Package Version ----------------- ------- annotated-types 0.7.0 iniconfig 2.1.0 mypy 1.16.1 mypy-extensions 1.1.0 packaging 25.0 pathspec 0.12.1 pluggy 1.6.0 pydantic 2.11.7 pydantic-core 2.33.2 pygments 2.19.2 pytest 8.4.1 ruff 0.12.3 typing-extensions 4.14.1 typing-inspection 0.4.1
雛形を作成する
モデルとテストコードの雛形を作成します。
models.py
from pydantic import BaseModel, Field, HttpUrl, PositiveInt ## ここにモデルを書く!
テストコードはこちら。
test_models.py
from pydantic import HttpUrl, ValidationError from models import User, User2 ## ここにテストを書く!
こちらをもとに作成していきます。
Typesを試す
まずはTypesを使ってみます。
class User(BaseModel): first_name: str last_name: str age: PositiveInt web: HttpUrl
Pythonの標準ライブラリーの型(str)、Pydanticの型(PositiveInt)、PydanticのNetworkの型(HttpUrl)からそれぞれ
選んでいます。
テストコードで確認します。
def test_valid_user() -> None: user = User.model_validate( { "first_name": "カツオ", "last_name": "磯野", "age": 11, "web": "https://katsuo.isono.example.com", } ) assert user.first_name == "カツオ" assert user.last_name == "磯野" assert user.age == 11 assert user.web == HttpUrl("https://katsuo.isono.example.com")
strからHttpUrlに変換してくれるようですね。
自分で書いてもOKですが。
def test_valid_user_2() -> None: user = User.model_validate( { "first_name": "カツオ", "last_name": "磯野", "age": 11, "web": HttpUrl("https://katsuo.isono.example.com"), } ) assert user.first_name == "カツオ" assert user.last_name == "磯野" assert user.age == 11 assert user.web == HttpUrl("https://katsuo.isono.example.com")
バリデーションエラーとなる場合。
def test_invalid_user() -> None: try: User.model_validate({"first_name": 1, "age": -5, "web": "ftp://example.com"}) except ValidationError as e: assert e.errors() == [ { "type": "string_type", "loc": ("first_name",), "msg": "Input should be a valid string", "input": 1, "url": "https://errors.pydantic.dev/2.11/v/string_type", }, { "type": "missing", "loc": ("last_name",), "msg": "Field required", "input": {"first_name": 1, "age": -5, "web": "ftp://example.com"}, "url": "https://errors.pydantic.dev/2.11/v/missing", }, { "type": "greater_than", "loc": ("age",), "msg": "Input should be greater than 0", "input": -5, "ctx": {"gt": 0}, "url": "https://errors.pydantic.dev/2.11/v/greater_than", }, { "type": "url_scheme", "loc": ("web",), "msg": "URL scheme should be 'http' or 'https'", "input": "ftp://example.com", "ctx": {"expected_schemes": "'http' or 'https'"}, "url": "https://errors.pydantic.dev/2.11/v/url_scheme", }, ]
型だけでもある程度バリデーションが可能ですね。
Fieldsを試す
次はFieldsを使ってみます。
先ほどのクラスを少しカスタマイズしたものを用意。最大長(最大値)や正規表現を使っています。
class User2(BaseModel): first_name: str = Field(min_length=1, max_length=10) last_name: str = Field(default="磯野", min_length=1, max_length=10) age: PositiveInt = Field(lt=100) web: str = Field(pattern=r"^https?://.+", max_length=35)
HttpUrlは文字列の最大長を制御できないようだったので、今回はFieldを直接扱うことにしました。
バリデーションOKとなる場合。
def test_valid_user2() -> None: user = User2.model_validate( { "first_name": "カツオ", "age": 11, "web": "https://katsuo.isono.example.com", } ) assert user.first_name == "カツオ" assert user.last_name == "磯野" assert user.age == 11 assert user.web == "https://katsuo.isono.example.com"
バリデーションNGとなる場合。
def test_invalid_user2() -> None: try: User2.model_validate( { "first_name": "あいうえおかきくけこさ", "last_name": "", "age": 100, "web": "ftp://katsuo.isono.example.com/foobarfuga", } ) except ValidationError as e: assert e.errors() == [ { "type": "string_too_long", "loc": ("first_name",), "msg": "String should have at most 10 characters", "input": "あいうえおかきくけこさ", "ctx": {"max_length": 10}, "url": "https://errors.pydantic.dev/2.11/v/string_too_long", }, { "type": "string_too_short", "loc": ("last_name",), "msg": "String should have at least 1 character", "input": "", "ctx": {"min_length": 1}, "url": "https://errors.pydantic.dev/2.11/v/string_too_short", }, { "ctx": { "lt": 100, }, "input": 100, "loc": ("age",), "msg": "Input should be less than 100", "type": "less_than", "url": "https://errors.pydantic.dev/2.11/v/less_than", }, { "ctx": { "max_length": 35, }, "input": "ftp://katsuo.isono.example.com/foobarfuga", "loc": ("web",), "msg": "String should have at most 35 characters", "type": "string_too_long", "url": "https://errors.pydantic.dev/2.11/v/string_too_long", }, ]
パッと見では期待通り動作しているように見えるのですが、web(URL)に関しては定義した正規表現と文字列長の両方に違反して
いるのですが、文字列長のみが検出されていますね。
もうひとつテストを用意。
def test_invalid_user2_2() -> None: try: User2.model_validate( { "first_name": "あいうえおかきくけこさ", "last_name": "", "age": 100, "web": "ftp://katsuo.isono.example.com", } ) except ValidationError as e: assert e.errors() == [ { "type": "string_too_long", "loc": ("first_name",), "msg": "String should have at most 10 characters", "input": "あいうえおかきくけこさ", "ctx": {"max_length": 10}, "url": "https://errors.pydantic.dev/2.11/v/string_too_long", }, { "type": "string_too_short", "loc": ("last_name",), "msg": "String should have at least 1 character", "input": "", "ctx": {"min_length": 1}, "url": "https://errors.pydantic.dev/2.11/v/string_too_short", }, { "ctx": { "lt": 100, }, "input": 100, "loc": ("age",), "msg": "Input should be less than 100", "type": "less_than", "url": "https://errors.pydantic.dev/2.11/v/less_than", }, { "ctx": { "pattern": "^https?://.+", }, "input": "ftp://katsuo.isono.example.com", "loc": ("web",), "msg": "String should match pattern '^https?://.+'", "type": "string_pattern_mismatch", "url": "https://errors.pydantic.dev/2.11/v/string_pattern_mismatch", }, ]
こちらはURLの文字列長をオーバーしていないので、正規表現でNGになったことが検出されました。
ということは、定義されているバリデーションルールのどれかひとつを満たさなかった時点で処理が打ち切られるということ
ですね。
おわりに
Pydanticで扱える型、フィールドの使い方を確認してみました。
使い方自体は特に難しくない気がするのですが、ちゃんとドキュメントを読まないと機能に気づかないですね…。
雰囲気で使うのではなく、やっぱりドキュメントを読むことが大事だなと思いました。