Python 3.12~3.13の型ヒント革命:Pydantic v2.7~v2.10のアップデート情報
はじめに 📘
この記事は ラクス Advent Calendar 2024 の 20 日目の記事になります!!
ラクスパートナーズ Advent Calendar 2024 の 23 日目にも参加してますのでそちらもよろしくお願い致します 🥳
昨年のアドベントカレンダーでは Python3.7~3.11 の型ヒントやコード品質向上のテクニックについてまとめました。
今年は引き続き Python3.12~3.13 の型ヒントについてまとめるとともに、 Pydantic のバージョンアップ情報[1]についてもまとめていきたいと思います。
また、最後にバージョンアップによる Pydantic の高速化検証も行いたいと思います。
この記事の対象者 🎯
- Python3.12~3.13 の型ヒントについて理解を深めたい方
- Pydantic の最新情報(3.7~3.10)を知りたい方
第 1 部:Python 3.12~3.13 の型ヒントの進化 👆
Python3.12 で追加された型 ⤴️
type
type
文を用いて型エイリアスを定義できるようになりました。
以下の例では、静的型検査器は Vector
と list[float]
を等しいものとして扱います
type Vector = list[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
new_vector = scale(2.0, [1.0, -4.2, 5.4]) # Passes type checker
another_vector = scale(2.0, ["1.0", "-4.2", "5.4"]) # Error in type checker
型 "list[str]" の引数を、関数 "scale" の型 "Vector" のパラメーター "vector" に割り当てることはできません
"Literal['1.0']" は "float" に割り当てできません
"Literal['-4.2']" は "float" に割り当てできません
"Literal['5.4']" は "float" に割り当てできませんPylancereportArgumentType
型エイリアスを定義する方法は type
以外に2つあります。
Vector: TypeAlias = list[float]
Vector = list[float]
どのように使い分けるのが良いか煩雑なので、それぞれの記載方法の違いについて以下にまとめます。
特徴 | type Vector = list[float] |
Vector: TypeAlias = list[float] |
Vector = list[float] |
---|---|---|---|
Python バージョン | Python 3.12 以降で利用可能 | Python 3.9 以降で利用可能(3.12 で非推奨) | 全ての Python バージョンで利用可能 |
構文の簡潔さ | 最も簡潔 | やや冗長 | 簡潔だが意図が明確ではない |
互換性 | 3.12 以前ではサポートされない | 3.12 未満のコードでも使用可能 | 互換性の心配なし |
型チェッカーの動作 | 型エイリアスとして認識 | 型エイリアスとして認識 | 型エイリアスとして認識されるが曖昧さあり |
意図の明確さ | 明確 | 明確 | 型エイリアスか通常の変数か不明確 |
推奨度 | 推奨される | 非推奨(3.12 以降) | 推奨されない(型エイリアスとしての意図が曖昧) |
TypeAliasType
type
を用いて作成された type alias を表現するタイプです。
type Vector = list[float]
print(type(Vector))
<class 'typing.TypeAliasType'>
type_check_only
@type_check_only
の主な目的は、特定のクラスや関数に紐づくプライベートなクラスを隠蔽しつつ、型チェッカーを通じてそのクラスの構造を利用者に提供することです。
from typing import type_check_only
@type_check_only
class Response: # 内部クラス
code: int
def get_header(self, name: str) -> str: ...
def fetch_response() -> Response:
# 実際にはプライベートなクラスを使う
class _PrivateResponse:
def __init__(self, code: int):
self.code = code
def get_header(self, name: str) -> str:
return f"Header-{name}"
return _PrivateResponse(200)
response = fetch_response()
response.code # 型チェッカーがコード補完で 'code: int' を認識
response.get_header("Content-Type") # 'get_header' メソッドも認識
response = Response(200) # Error in type checker
"Response" は@type_check_onlyとしてマークされており、型注釈でのみ使用できます
Python3.12 でアップデートされた型 🔄
TypeVar
Python 3.12 では、新しい構文でジェネリック型を直接定義できるようになりました。
def identity[T](value: T) -> T:
return value
from typing import TypeVar
T = TypeVar('T')
def identity(value: T) -> T:
return value
TypeVar:`__infer_variance__`(分散推論の自動化)
- 分散は型チェッカーによって自動で推論されるようになりました。
- 明示的に指定したい場合は従来どおり
covariant
やcontravariant
を使うことも可能です。
共変(Covariant):サブクラスの型がスーパークラスの型として扱える性質。
反変(Contravariant):スーパークラスの型がサブクラスの型として扱える性質。
T = TypeVar("T") # 推論により適切な分散が決定される
from typing import TypeVar, Generic
# 型変数を共変(covariant=True)として定義
T_co = TypeVar("T_co", covariant=True)
class Box(Generic[T_co]):
def __init__(self, value: T_co):
self.value = value
class Animal:
pass
class Dog(Animal):
pass
# Box[Dog]はBox[Animal]として扱える
def handle_animal_box(box: Box[Animal]):
print("Handling a box of animals.")
# Dogを格納したBoxを作成
dog_box = Box(Dog()) # Box[Dog]
handle_animal_box(dog_box) # 共変なので問題なく動作
from typing import Generic, TypeVar
# 型変数を反変(contravariant=True)として定義
T_contra = TypeVar("T_contra", contravariant=True)
class Handler(Generic[T_contra]):
def handle(self, value: T_contra):
print(f"Handling {value}")
class Animal:
pass
class Dog(Animal):
pass
# Handler[Animal]はHandler[Dog]として扱える
def register_handler(handler: Handler[Dog]):
print("Registering a handler for dogs.")
animal_handler = Handler[Animal]() # Handler[Animal]
# Handler[Animal]が反変なので、Handler[Dog]として扱える
register_handler(animal_handler) # 反変なので問題なく動作
TypeVar:`__bound__`(制約の遅延評価)
-
bound
はアクセス時に評価(遅延評価)されるようになりました。 - 未定義の型や再帰的な型にも柔軟に対応可能です。
from typing import TypeVar
# 3.11以前: 定義時に SomeClass が存在していないとエラー
# 3.12以降: 遅延評価のため SomeClass が未定義でもOK
T = TypeVar("T", bound="SomeClass")
class SomeClass:
pass
TypeVar:`__constraints__`(制約の遅延評価)
-
constraints
はアクセス時に評価(遅延評価)されるようになりました。 - 未定義の型を柔軟に扱うことが可能です。
from typing import TypeVar
# 3.11以前: 制約に未定義の型 SomeClass を指定した場合、即時評価のためエラー
# 3.12以降: 遅延評価のため、SomeClass が未定義でも問題なし
T = TypeVar("T", int, str, "SomeClass")
class SomeClass:
pass
TypeVarTuple
型パラメータ構文を用いることで、タプルの型変数(TypeVarTuple
)をより簡潔に定義できるようになリました。
TypeVarTuple
は、可変長の型パラメータを扱うための機能で、Python 3.11 で導入されました。例えば、タプルの要素が異なる型を持つ場合や、複数の型引数を受け取るクラスや関数を定義したい場合に使います。
from typing import TypeVarTuple, Generic
# 可変長の型引数を定義
Ts = TypeVarTuple("Ts")
class MyClass(Generic[*Ts]): # Ts を展開して複数の型引数を受け取る
def __init__(self, *args: *Ts):
self.values = args
# 使用例
instance = MyClass[int, str, float](42, "Hello", 3.14)
print(instance.values) # 出力: (42, 'Hello', 3.14)
Python 3.12 では、以下のように型パラメータ構文で可変長型を直接記述できます:
class MyClass[*Ts]: # 可変長型を直接記述
def __init__(self, *args: *Ts):
self.values = args
# 使用例
instance = MyClass[int, str, float](42, "Hello", 3.14)
print(instance.values) # 出力: (42, 'Hello', 3.14)
ParamSpec
冗長なParamSpec
の定義が不要になり、コードが簡潔になりました。
from typing import ParamSpec, TypeVar, Callable
P = ParamSpec("P")
R = TypeVar("R")
def my_function(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("Before the function call")
result = func(*args, **kwargs)
print("After the function call")
return result
return wrapper
def my_function[P, R](func: Callable[P, R]) -> Callable[P, R]: # 型パラメータ構文
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("Before the function call")
result = func(*args, **kwargs)
print("After the function call")
return result
protocol, runtime_checkable
isinstance()
でプロトコルの属性を確認する際に、hasattr()
を使用していましたが、inspect.getattr_static()
を使用するようになりました。これにより属性が動的に生成される場合に対応できなくなりました。
(hasattr()
はオブジェクトの属性を確認するために__getattr__
や__getattribute__
を呼び出すため、属性が動的に生成される場合でも対応できました。)
コードの補足[2]
from typing import Protocol, runtime_checkable
@runtime_checkable
class MyProtocol(Protocol):
def some_method(self) -> None: ...
class DynamicClass:
def __getattr__(self, name):
if name == "some_method":
return lambda: print("Dynamically generated method")
raise AttributeError
obj = DynamicClass()
# Python 3.11以前: True(動的に生成されたメソッドが存在と見なされる)
# Python 3.12以降: False(静的に属性を確認するため、メソッドが見つからない)
print(isinstance(obj, MyProtocol)) # 3.11以前: True / 3.12以降: False
dataclass_transform
Python 3.12 から、@dataclass_transform
に frozen_default
オプションが追加され、デフォルトで属性を不変(frozen=True)として扱えるようになりました。
そもそも dataclass_transform
とは dataclass
のように振る舞うことを型チェッカーに知らせるためのものです。
class ImmutableBase:
def __init__(self, **kwargs):
for key, value in kwargs.items():
object.__setattr__(self, key, value)
class Point(ImmutableBase):
x: int
y: int
point = Point(x="1", y="2") # 型チェッカーはエラーを報告しない
point = Point(x="1") # 型チェッカーはエラーを報告しない
from typing import dataclass_transform
@dataclass_transform(frozen_default=True)
class ImmutableBase:
def __init__(self, **kwargs):
for key, value in kwargs.items():
object.__setattr__(self, key, value)
class Point(ImmutableBase):
x: int
y: int
point = Point(x=1, y=2)
print(point.x) # 1
point.x = 3 # 型チェッカーがエラーを報告
クラス "Point" の属性 "x" に割り当てることはできません
属性 "x" は読み取り専用です
Python3.12 で非推奨になった型 ⤵️
-
Hashable
- 代わりに collections.abc.Hashbale を使用
-
Sized
- 代わりに collections.abc.Sized を使用
-
TypeAlias
- 代わりに type を利用
Python3.13 で追加された型 ⤴️
ReadOnly(TypedDict)
TypedDict
のアイテムが読み取り専用であることを示します。
from typing import ReadOnly, TypedDict
class Movie(TypedDict):
title: ReadOnly[str]
year: int
rate: float
print(Movie.__readonly_keys__) # 出力: frozenset({'title'})
print(Movie.__mutable_keys__) # 出力: frozenset({'year', 'rate'})
def mutate_movie(m: Movie) -> None:
m["year"] = 1999 # allowed
m["title"] = "The Matrix" # typechecker error
TypedDict で項目を割り当てることができませんでした
"title" は "Movie" の読み取り専用キーです
get_protocol_members
指定した Protocol
型に定義されているメンバー(メソッドや属性)の名前をセットとして返します。
from typing import Protocol, get_protocol_members
class P(Protocol):
def a(self) -> str: ...
b: int
print(get_protocol_members(P)) # 出力: frozenset({'a', 'b'})
is_protocol
指定した型が Protocol
かどうかを判定します。
from typing import Protocol, is_protocol
class P(Protocol):
def a(self) -> str: ...
b: int
print(is_protocol(P)) # 出力: True
print(is_protocol(int)) # 出力: False
TypeIs
ユーザー定義の型述語関数(type predicate function) を記述するための特殊な型コンストラクトです。これは、型チェッカーがコードのフローに基づいて式の型を狭める(narrowing) ことをサポートします。
from typing import TypeIs
class Parent: pass
class Child(Parent): pass
class Unrelated: pass
def is_parent(val: object) -> TypeIs[Parent]:
return isinstance(val, Parent)
def process(value: Child | Unrelated):
if is_parent(value):
# 型チェッカーはここで value を Child と推論
print(f"{value} is a Child!")
else:
# 型チェッカーはここで value を Unrelated と推論
print(f"{value} is Unrelated!")
from typing import TypeIs
def is_str_or_bytes(val: object) -> TypeIs[str | bytes]:
return isinstance(val, (str, bytes))
def handle_data(data: int | str | bytes):
if is_str_or_bytes(data):
# data は str | bytes と推論
result = data + 1 # Type check error
else:
# data は int と推論
result = data + 1 # Pass type checker
演算子 "+" は型 "str | bytes | bytearray | memoryview[_I@memoryview]" と "Literal[1]" ではサポートされていません
TypeGuard
との違いがわかりずらいと感じた為、テーブル形式でまとめてみます。
特性 | TypeIs | TypeGuard |
---|---|---|
適用範囲 | 入力型と出力型が同じ型階層または交差型である場合 | 入力型と出力型が異なる場合にも使用可能 |
型の交差(Intersection) | サポートあり | サポートなし |
型の補集合(Negation) | サポートあり | サポートなし |
主な用途 | 型階層内の型の絞り込み | 入力型を別の型に変換する場合 |
from typing import TypeIs
class Parent:
pass
class Child(Parent):
pass
class Unrelated:
pass
def is_parent(val: object) -> TypeIs[Parent]:
# Parent 型かどうかを判定
return isinstance(val, Parent)
def example_type_is(arg: Child | Unrelated):
if is_parent(arg):
# TypeIs により、arg の型が Parent に絞り込まれる
print(f"{arg} is a Parent or subclass (e.g., Child)")
else:
print(f"{arg} is not a Parent (e.g., Unrelated)")
from typing import TypeGuard
def is_list_of_ints(val: list[object]) -> TypeGuard[list[int]]:
# リストの全要素が int 型であるかを判定
return all(isinstance(item, int) for item in val)
def example_type_guard(data: list[object]):
if is_list_of_ints(data):
# TypeGuard により、data の型が list[int] に絞り込まれる
print(f"{data} is a list of integers")
else:
# data は元の型 list[object] のまま
print(f"{data} is not a list of integers")
NoDefault
TypeVar
でデフォルト値を持たないことを示す為に使用されます。
from typing import NoDefault, TypeVar
T = TypeVar("T")
print(T.__default__ is NoDefault) # True
S = TypeVar("S", default=None)
print(S.__default__ is None) # True
Python3.13 でアップデートされた型 🔄
ClassVar && Final
ClassVar
と Final
を合わせて利用することができるようになりました。
from typing import ClassVar
class Example:
shared_value: ClassVar[int] = 42 # クラス全体で共有される属性
a = Example()
a.shared_value = 100 # Error, setting class variable on instance
Example.shared_value = 100 # This is OK
クラス "Example" の属性 "shared_value" に割り当てることはできません
属性 "shared_value" は ClassVar であるため、クラス インスタンスを介して割り当てることはできません
from typing import Final
class Example:
MAX_ITEMS: Final[int] = 100 # 再代入禁止
Example.MAX_ITEMS = 10 # 型チェックエラー: Finalで再代入禁止
クラス "type[Example]" の属性 "MAX_ITEMS" に割り当てることはできません
"MAX_ITEMS" は Final として宣言されており、再割り当てできません
from typing import ClassVar, Final
class Example:
DEFAULT_VALUE: ClassVar[Final[int]] = 42 # クラス属性で再代入不可
Example.DEFAULT_VALUE = 99 # 型チェックエラー: Finalで再代入禁止
クラス "type[Example]" の属性 "DEFAULT_VALUE" に割り当てることはできません
"Literal[99]" は "Final" に割り当てできません
TypeVar: default
TypeVar
にデフォルト値を指定できるようになりました。
from typing import TypeVar
T = TypeVar("T", default=int) # デフォルト値として int を指定
U = TypeVar("U") # デフォルト値なし
print(T.__default__) # 出力: <class 'int'>
print(U.__default__) # 出力: <typing.NoDefault object at ...>
うーん。ただ基本的に default
を指定しなくても型推論してくれるし、そもそもこれってテンプレート関数を作る為なので最初から型を指定する意味とは?。。。 使用する際に明示的に型を指定してあげれば良いので default
を使うタイミングがいまいちわからないな。。。
TypeVar: has_default()
デフォルト値が設定されているかを確認するメソッドも追加されました。
from typing import TypeVar
T = TypeVar("T", default=int) # デフォルト値として int を指定
U = TypeVar("U") # デフォルト値なし
print(T.has_default()) # 出力: True
print(U.has_default()) # 出力: False
TypeVarTuple: default/has_default()
Typevar と同様
ParamSpec: default/has_default()
Typevar と同様
NamedTuple
from typing import NamedTuple
# クラスベース構文
class Point(NamedTuple):
x: int
y: int
p = Point(1, 2)
print(p.x) # 出力: 1
print(p.y) # 出力: 2
# 関数ベース構文
Point = NamedTuple("Point", [("x", int), ("y", int)])
p = Point(1, 2)
print(p.x) # 出力: 1
print(p.y) # 出力: 2
以下は非推奨になった使い方です。
from typing import NamedTuple
# 非推奨(Python 3.13以降で警告)
Point = NamedTuple("Point", x=int, y=int)
# 非推奨(Python 3.13以降で警告)
Empty = NamedTuple("Empty")
EmptyNone = NamedTuple("Empty", None)
色々複雑と思われるかもしれませんが、NamedTuple
については クラスベース構文を使用しておけば問題ないと考えて良さそう。
そもそも、NamedTuple
じゃなくて pydantic(frozen=True)
使えば良くないか?って思ったので下記に比較表作ってみました。
特性 | NamedTuple |
Pydantic モデル(frozen=True) |
---|---|---|
不変性(immutable) | デフォルトで不変 |
frozen=True またはallow_mutation=False で指定 |
型チェック | 静的型チェック(型注釈が必須) | 実行時の型チェックと値の検証が可能 |
デフォルト値 | クラスベース構文でのみ可能 | 簡単に設定可能 |
シリアル化/デシリアル化 | 非対応 | JSON や辞書への変換が簡単 |
カスタムバリデーション | 不可能 | 豊富なバリデーション機能をサポート |
メソッドの追加 | 基本的に不可能 | メソッドの追加が可能 |
パフォーマンス | 非常に軽量で高速 | 若干重い(通常は無視できる程度) |
用途 | 軽量な固定データ構造に適している | 不変性に加えて検証やシリアル化が必要な場合 |
ContextManager/AbstractContextManager
AbstractContextManager
を使用することで __exit__
のデフォルト実装を継承できるようになりました。
class AbstractContextManager(abc.ABC):
...
@abc.abstractmethod
def __exit__(self, exc_type, exc_value, traceback):
"""Raise any exception triggered within the runtime context."""
return None
from typing import ContextManager
def get_context() -> ContextManager[str]:
class MyContext:
def __enter__(self) -> str:
return "Hello"
# NOTE: __exit__ の実装が必須
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
return MyContext()
from contextlib import AbstractContextManager
def get_context() -> AbstractContextManager[str]:
class MyContext(AbstractContextManager[str]):
def __enter__(self) -> str:
return "Hello"
return MyContext() # NOTE: 型チェッカーによっては警告が出るみたいです。
AsyncGenerator/Generator(collections.abc)
デフォルト値が追加されたため、SendType
と ReturnType
を省略可能になりました。
これが地味に便利
from typing import Generator
def example() -> Generator[int, None, None]:
yield 1
from collections.abc import Generator
def example() -> Generator[int]:
yield 1
Python3.13 で非推奨になった型 ⤵️
AnyStr
Python 3.18 で削除される予定となりました。代わりに [T: (str, bytes)]
を使うことが推奨されてます。
from typing import AnyStr
def concat(a: AnyStr, b: AnyStr) -> AnyStr:
return a + b
concat("hello", "world") # OK
concat(b"hello", b"world") # OK
concat("hello", b"world") # 型エラー
型 "Literal[b"world"]" の引数を、関数 "concat" の型 "AnyStr@concat" のパラメーター "b" に割り当てることはできません
"Literal[b"world"]" は "str" に割り当てできません
def concat[T: (str, bytes)](a: T, b: T) -> T:
return a + b
TypedDict
フィールドをキーワード引数として指定する構文は非推奨。
# 非推奨(Python 3.13 以降で警告、3.15 で削除予定)
Config = TypedDict("Config", mutable_key=int, readonly_key=str)
@no_type_check_decorator
代わりに @no_type_check
を使用することが推奨されます。
from typing import no_type_check
@no_type_check
def my_function(arg: int) -> None:
# 型チェッカーはこの関数を無視
print(arg + "This is invalid") # 実行時エラーになるが、型チェッカーは検出しない
第 2 部:2024 年の Pydantic のバージョンアップ情報
全てのアップデート情報を網羅できているわけではありません。詳細は各自で公式ドキュメントを参照してください 🙏
v2.7の主な新機能 🎉
1. 部分的な JSON パースのサポート:
from_json
メソッドを使用して、無効な構文に遭遇するまでの入力を読み取り、有効な部分を可能な限り JSON オブジェクトとして返すことができます。これは、LLM(大規模言語モデル)のストリーミング出力など、従来のパーサーではエラーとなる部分的な JSON オブジェクトの処理に特に有用です。
from pydantic_core import from_json
partial_json_data = '["aa", "bb", "c' # 不完全なJSONリスト
try:
result = from_json(partial_json_data, allow_partial=False)
except ValueError as e:
print(e) # エラー: EOF while parsing a string at line 1 column 15
result = from_json(partial_json_data, allow_partial=True)
print(result) # 出力: ['aa', 'bb']
私も LLM サービスを以前に構築した際にこの機能にはお世話になりました。
基本的には pydantic_core.from_json
と pydantic.BaseModel
の組み合わせで使用するケースになると思います。
from pydantic_core import from_json
from pydantic import BaseModel
class Dog(BaseModel):
breed: str
name: str
friends: list
partial_dog_json = '{"breed": "lab", "name": "fluffy", "friends": ["buddy", "spot", "rufus"], "age'
dog = Dog.model_validate(from_json(partial_dog_json, allow_partial=True))
print(repr(dog))
#> Dog(breed='lab', name='fluffy', friends=['buddy', 'spot', 'rufus'])
以下は、デフォルト値を使用する例になります。
from typing import Any, Optional, Tuple
import pydantic_core
from typing_extensions import Annotated
from pydantic import BaseModel, ValidationError, WrapValidator
def default_on_error(v, handler) -> Any:
"""
Raise a PydanticUseDefault exception if the value is missing.
This is useful for avoiding errors from partial
JSON preventing successful validation.
"""
try:
return handler(v)
except ValidationError as exc:
# there might be other types of errors resulting from partial JSON parsing
# that you allow here, feel free to customize as needed
if all(e['type'] == 'missing' for e in exc.errors()):
raise pydantic_core.PydanticUseDefault()
else:
raise
class NestedModel(BaseModel):
x: int
y: str
class MyModel(BaseModel):
foo: Optional[str] = None
bar: Annotated[
Optional[Tuple[str, int]], WrapValidator(default_on_error)
] = None
nested: Annotated[
Optional[NestedModel], WrapValidator(default_on_error)
] = None
m = MyModel.model_validate(
pydantic_core.from_json('{"foo": "x", "bar": ["world",', allow_partial=True)
)
print(repr(m))
#> MyModel(foo='x', bar=None, nested=None)
m = MyModel.model_validate(
pydantic_core.from_json(
'{"foo": "x", "bar": ["world", 1], "nested": {"x":', allow_partial=True
)
)
print(repr(m))
#> MyModel(foo='x', bar=('world', 1), nested=None)
2. 汎用的な`Secret`基本型の導入:
SecretStr
やSecretBytes
に加えて、任意の型をラップするカスタムシークレット型を作成できる汎用的なSecret
基本型が追加されました。
from pydantic import BaseModel, Secret
class SecretSalary(Secret[int]):
def _display(self) -> str:
return '$******'
class Employee(BaseModel):
name: str
salary: SecretSalary
employee = Employee(name='John Doe', salary=100_000)
print(repr(employee)) # 出力: Employee(name='John Doe', salary=SecretSalary('$******'))
print(employee.salary) # 出力: $******
print(employee.salary.get_secret_value()) # 出力: 100000
print(employee.model_dump())
#> {'sensitive_int': Secret('**********')}
ちなみに、SecretStr
, SecretBytes
の使用例は下記のような感じです。
from pydantic import BaseModel, SecretStr
class User(BaseModel):
username: str
password: SecretStr
# 使用例
user = User(username='alice', password="supersecretpassword")
print(user) # User(username='alice', password=SecretStr('**********'))
print(user.password) # **********
print(user.password.get_secret_value()) # supersecretpassword
from pydantic import BaseModel, SecretBytes
class Config(BaseModel):
api_key: SecretBytes
# 使用例
config = Config(api_key=b"secret_api_key")
print(config) # Config(api_key=SecretBytes('**********'))
print(config.api_key) # **********
print(config.api_key.get_secret_value()) # b'secret_api_key'
Secret
の使い所としては、機密情報を含む API レスポンスやログ出力に使用するケースでしょうか。
こうすることで機密情報を誤ってログに流したりレスポンスに含めたりすることを防げます。
from typing_extensions import Annotated
from pydantic import BaseModel, Field, Secret
# Overwriting the representation
class SecretSalary(Secret[float]):
# NOTE: str 関数を呼び出した時の表示を制御
def _display(self) -> str:
return '$****.**'
class Model(BaseModel):
salary: SecretSalary
# Configでjson_encodersを設定
class Config:
json_encoders = {
Secret: lambda v: str(v) # Secret型を安全にマスクした文字列へ変換
}
# モデルインスタンスの作成
m = Model(salary=42)
# JSONへの変換
json_data = m.model_dump_json() # Response として返す Json データを作成
print(json_data) # {"sensitive_int": "**********"} ログに出力
3. フィールドの`deprecated`属性:
フィールドを非推奨としてマークする機能が追加されました。これにより、フィールドにアクセスするとランタイムで警告が発せられ、生成される JSON スキーマ内でdeprecated
パラメータがtrue
に設定されます。
from pydantic import BaseModel, Field
class User(BaseModel):
username: str
old_field: str = Field(..., deprecated=True)
user = User(username='alice', old_field='deprecated_value')
print(user.old_field) # アクセス時に非推奨警告が表示される
4. `serialize_as_any`
Duck-typing serialization is the behavior of serializing an object based on the fields present in the object itself, rather than the fields present in the schema of the object. This means that when an object is serialized, fields present in a subclass, but not in the original schema, will be included in the serialized output.
簡単に言うと、モデルのスキーマ(型定義)に基づくか、モデルが持つフィールド(実際の属性値)に基づくかを選択可能にしたと言うことみたいです。
from pydantic import BaseModel, TypeAdapter
class User(BaseModel):
name: str
class UserLogin(User):
password: str
user_login = UserLogin(name="John Doe", password="secret_password")
ta = TypeAdapter(User)
# モデルスキーマ(型定義)に基づくシリアライズ(通常のケース)
print(ta.dump_python(user_login, serialize_as_any=False))
# 出力: {'name': 'John Doe'}
# 実際のデータに基づく柔軟なシリアライズ
print(ta.dump_python(user_login, serialize_as_any=True))
# 出力: {'name': 'John Doe', 'password': 'secret_password'}
model_dump/model_dump_json
でもシリアライズ化できるので、基本的にモデルスキーマに基づくシリアライズ化を行いたい場合は TypeAdapter(serialize_asy_any=False)
を使う感じでしょうか。以下、モデルスキーマとモデルフィールドの違いをまとめておきます。
基準 | モデルスキーマに基づく | モデルフィールドに基づく |
---|---|---|
動作の基準 | 型定義(親クラスの定義など)に基づく | 実際のインスタンスが持つフィールドに基づく |
Pydantic v2.x デフォルト |
serialize_as_any=False (型スキーマ優先) |
model_dump() や model_dump_json , serialize_as_any=True (フィールド優先) |
柔軟性 | スキーマに明示されたもののみをシリアライズ | 実際のデータを柔軟に扱う |
セキュリティ | 型スキーマ外の予期しないデータを除外 | サブクラスで追加されたフィールドも含まれる |
5. `pass context to serialization`
シリアライズ時にもコンテキスト情報を渡せるようになりました。
元々、バリデーション時には以下のようにコンテキスト情報を渡すことができました。
from pydantic import BaseModel, root_validator, ValidationError
class Measurement(BaseModel):
distance: float # in meters
@root_validator(pre=True)
def validate_distance(cls, values, context):
distance = values.get("distance")
if context and "unit" in context:
if context["unit"] == "km" and distance > 100:
raise ValueError("Distance cannot exceed 100 km")
return values
# バリデーション実行
try:
Measurement(distance=101, context={"unit": "km"})
except ValidationError as e:
print(e)
# Distance cannot exceed 100 km
これが以下のように、シリアライズ時に可能になりました。
from pydantic import BaseModel, SerializationInfo, field_serializer
class Measurement(BaseModel):
distance: float # in meters
@field_serializer('distance')
def convert_units(self, v: float, info: SerializationInfo):
context = info.context
if context and 'unit' in context:
if context['unit'] == 'km':
v /= 1000 # convert to kilometers
elif context['unit'] == 'cm':
v *= 100 # convert to centimeters
return v
measurement = Measurement(distance=500)
print(measurement.model_dump()) # no context
#> {'distance': 500.0}
print(measurement.model_dump(context={'unit': 'km'})) # with context
#> {'distance': 0.5}
print(measurement.model_dump(context={'unit': 'cm'})) # with context
#> {'distance': 50000.0}
- パフォーマンス向上
詳細はこちら
v2.8の主な新機能 🎉
1. Fail-Fast Validation
list
, tuple
, set
と frozenset
のタイプにおいて要素のバリデーションにおいて失敗が生じた時点でバリデーションを終えられるようになりました。データの正確性を重視する場合や、パフォーマンスを重視する場合に役立ちます。
from typing import List
from typing_extensions import Annotated
from pydantic import BaseModel, FailFast, Field, ValidationError
class Model(BaseModel):
x: Annotated[List[int], FailFast()]
y: List[int] = Field(..., fail_fast=True)
# This will raise a single error for the first invalid value in each list
# At which point, validation for said field will stop
try:
obj = Model(x=[1, 2, 'a', 4, 5, 'b', 7, 8, 9, 'c'], y=[1, 2, 'a', 4, 5, 'b', 7, 8, 9, 'c'])
except ValidationError as e:
print(e)
"""
2 validation errors for Model
x.2
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/int_parsing
y.2
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/int_parsing
"""
2. Model/field deprecation in JSON schema
JSON schema に deprecation 情報が格納されるようになりました。
これによって、FastAPI の自動生成する OpenAPI ドキュメントなどに deprecated 情報が表示されるようなるそうです。
from typing_extensions import deprecated
from pydantic import BaseModel, Field
@deprecated('DeprecatedModel is... sadly deprecated')
class DeprecatedModel(BaseModel):
deprecated_field: str = Field(..., deprecated=True)
json_schema = DeprecatedModel.schema()
assert json_schema['deprecated'] is True
assert json_schema['properties']['deprecated_field']['deprecated'] is True
3. Programmatic Title Generation
model や fileds のタイトルをアトリビュート情報を使用して動的に生成できるようになりました。
import json
from pydantic import BaseModel, ConfigDict, Field
class MyModel(BaseModel):
foo: str = Field(..., field_title_generator=lambda name, field_info: f'title-{name}-from-field')
bar: str
model_config = ConfigDict(
field_title_generator=lambda name, field_info: f'title-{name}-from-config',
model_title_generator=lambda cls: f'title-{cls.__name__}-from-config',
)
print(json.dumps(MyModel.model_json_schema(), indent=2))
"""
{
"properties": {
"foo": {
"title": "title-foo-from-field",
"type": "string"
},
"bar": {
"title": "title-bar-from-config",
"type": "string"
},
},
"required": [
"foo",
"bar",
],
"title": "title-MyModel-from-config",
"type": "object"
}
"""
4. Serialization Context for TypeAdapter
TypeAdapter
のシリアライゼーションにもコンテキスト情報を渡せるようになりました。
from typing_extensions import Annotated
from pydantic import SerializationInfo, TypeAdapter, PlainSerializer
def serialize_distance(v: float, info: SerializationInfo) -> float:
"""We assume a distance is provided in meters, but we can convert it to other units if a context is provided."""
context = info.context
if context and 'unit' in context:
if context['unit'] == 'km':
v /= 1000 # convert to kilometers
elif context['unit'] == 'cm':
v *= 100 # convert to centimeters
return v
distance_adapter = TypeAdapter(Annotated[float, PlainSerializer(serialize_distance)])
print(distance_adapter.dump_python(500)) # no context, dumps in meters
# > 500.0
print(distance_adapter.dump_python(500, context={'unit': 'km'})) # with context, dumps in kilometers
# > 0.5
print(distance_adapter.dump_python(500, context={'unit': 'cm'})) # with context, dumps in centimeters
# > 50000
補足
そもそも、 TypeAdapter
っていつ使うのか?
以下に、Pydantic モデルを使用する場合との比較表を記載しておきます。
特徴 | TypeAdapter | Pydantic モデル |
---|---|---|
適用範囲 | 単一型や複雑な型アノテーション | モデルスキーマ全体の設計 |
モデル定義の必要性 | 不要 | 必要 |
柔軟性 | 型アノテーションに直接対応 | モデル内のフィールドに依存 |
単一型のバリデーション | 適切 | 対応可能だがモデル作成が必要 |
スキーマ生成 | 型アノテーションに基づいて直接生成可能 | モデルに基づくスキーマ生成 |
複雑な型(Union、List 等) | 直接対応 | モデルに依存した構造で対応 |
5. Experimental Features
実験的機能が導入されました。詳細はこちら
- パフォーマンス向上
詳細はこちら
v2.9の主な新機能 🎉
1. complex number support
Python の標準ライブラリが提供する 複素数(complex)型
がサポートされました。
from pydantic import TypeAdapter
ta = TypeAdapter(complex)
complex_number = ta.validate_python('1+2j')
assert complex_number == complex(1, 2)
assert ta.dump_json(complex_number) == b'"1+2j"'
from pydantic import BaseModel
class ComplexModel(BaseModel):
value: complex
# バリデーション
model = ComplexModel(value="1+2j")
assert model.value == complex(1, 2)
# JSON ダンプ
json_data = model.model_dump_json()
assert json_data == b'{"value":"1+2j"}'
2. Explicit ZoneInfo support
Python の標準ライブラリが提供する タイムゾーン情報(Zoneinfo型)
がサポートされました。
from pydantic import TypeAdapter
from zoneinfo import ZoneInfo
ta = TypeAdapter(ZoneInfo)
tz = ta.validate_python('America/Los_Angeles')
assert tz == ZoneInfo('America/Los_Angeles')
assert ta.dump_json(tz) == b'"America/Los_Angeles"'
3. val_json_bytes setting
JSON からバイトデータをデコードする際に使用するエンコーディングを指定できるようになりました。
Pydantic v2.7
以前では、バイトデータのデコード時にデフォルトで UTF-8 を使用していました。このため、JSON に格納されたバイトデータが他のエンコーディング(例: Base64)を使用している場合、ラウンドトリップ(エンコード -> デコードの往復)が失敗することがありました。
from pydantic import TypeAdapter, ConfigDict
ta = TypeAdapter(bytes, config=ConfigDict(ser_json_bytes='base64', val_json_bytes='base64'))
some_bytes = b'hello'
validated_bytes = ta.validate_python(some_bytes)
encoded_bytes = b'"aGVsbG8="'
assert ta.dump_json(validated_bytes) == encoded_bytes
# verifying round trip
# before we added support for val_json_bytes, the default encoding was 'utf-8' for validation, so this would fail
assert ta.validate_json(encoded_bytes) == validated_bytes
4. Support for JSON schema with custom validators
Pydantic では、フィールドのカスタムバリデーション(例: BeforeValidator
や PlainValidator
)を使用する場合、JSON Schema の定義がバリデーションロジックに対応していませんでした。例えば、数値と文字列の両方を受け付けて内部で整数に変換するカスタムバリデーションがあった場合でも、生成される JSON Schema には整数型しか記載されない、という問題がありました。この新機能により、カスタムバリデータを使用したフィールドでも、json_schema_input_type を指定することで、入力形式に応じた JSON Schema を正しく生成できるようになりました。
from typing import Any, Union
from pydantic_core import PydanticKnownError
from typing_extensions import Annotated
from pydantic import PlainValidator, TypeAdapter
# カスタムバリデータ: int または str を受け付けて int に変換
def validate_maybe_int(v: Any) -> int:
if isinstance(v, int):
return v
elif isinstance(v, str):
try:
return int(v)
except ValueError:
...
# Pydantic の既知のエラーをスロー
raise PydanticKnownError('int_parsing')
# TypeAdapter にカスタムバリデータと JSON Schema 情報を指定
ta = TypeAdapter(
Annotated[
int,
PlainValidator(validate_maybe_int, json_schema_input_type=Union[int, str])
]
)
# JSON Schema の生成(mode='validation')
print(ta.json_schema(mode='validation'))
# {
# "anyOf": [
# {"type": "integer"},
# {"type": "string"}
# ]
# }
from typing import Any, Union
from pydantic import BaseModel, field_validator, Field
from typing_extensions import Annotated
# カスタムバリデータ: int または str を受け付けて int に変換
def validate_maybe_int(v: Any) -> int:
if isinstance(v, int):
return v
elif isinstance(v, str):
try:
return int(v)
except ValueError:
...
raise ValueError("Invalid value: must be int or string representing an int.")
# モデル定義
class MyModel(BaseModel):
value: Annotated[
int,
Field(json_schema_input_type=Union[int, str]) # JSON Schema 情報の指定
]
@field_validator("value", mode="before")
def validate_value(cls, v):
return validate_maybe_int(v)
# JSON Schema の生成
print(MyModel.model_json_schema(mode='validation'))
# {
# "properties": {
# "value": {
# "anyOf": [
# {"type": "integer"},
# {"type": "string"}
# ]
# }
# },
# "type": "object",
# "required": ["value"]
# }
# バリデーション例
model = MyModel(value="123")
assert model.value == 123
model = MyModel(value=456)
assert model.value == 456
try:
MyModel(value="invalid")
except ValueError as e:
print(e) # 出力: Invalid value: must be int or string representing an int.
補足
そもそも、mode
はどう言う意味を持つのか?
Pydantic では、JSON Schema の生成時に mode
パラメータを指定することで、どのタイミングに基づいたスキーマを生成するかを制御できます。
-
mode='validation'
:- 「入力時の検証」用のスキーマを生成します。
- バリデーションで「受け入れられる形式」に基づいて JSON Schema を定義します。
- 用途
- 主に 入力時のデータ検証 を意図してスキーマを定義する場合。
- 例:
- API リクエストやフロントエンドでのフォーム入力時に、どの形式のデータを受け入れるかを明示する。
-
mode='after'
:- 「検証後(バリデーション後)の状態」に基づくスキーマを生成します。
- 実際にモデル内で保持されるデータ型や構造を反映したスキーマを定義します。
- 用途
- 主に モデル内部の状態や出力形式 を意図してスキーマを定義する場合
- 例
- API レスポンスで、モデル内部に格納されたデータ構造を正確に反映する。
比較表
mode |
説明 | 主な用途 |
---|---|---|
validation |
バリデーション時に受け入れる入力形式を基にスキーマを生成。 | 入力データの検証。ユーザーからのリクエストやデータ入力に適用。 |
after |
バリデーション後の内部状態(変換後の型や値)を基にスキーマを生成。 | 出力データの検証。API レスポンスやデータ保存に適用。 |
- パフォーマンス向上
詳細はこちら
v2.10の主な新機能 🎉
1. experimental_allow_partial
不完全な JSON 文字列や、Python Objects のバリデーションが可能となりました。LLM の出力を処理する際により便利になりました。
現在は、TypeAdapter
インスタンスのみサポートしてます。今後 BaseModel
, Pydantic dataclass
でも対応予定とのことです。
この機能もっと早く欲しかったなー
from pydantic import TypeAdapter, BaseModel
from typing import Literal
class UserRecord(BaseModel):
id: int
name: str
role: Literal['admin', 'user']
ta = TypeAdapter(list[UserRecord])
# allow_partial if the input is a python object
d = ta.validate_python(
[
{'id': '1', 'name': 'Alice', 'role': 'user'},
{'id': '1', 'name': 'Ben', 'role': 'user'},
{'id': '1', 'name': 'Char'},
],
experimental_allow_partial=True,
)
print(d)
#> [UserRecord(id=1, name='Alice', role='user'), UserRecord(id=1, name='Ben', role='user')]
# allow_partial if the input is a json string
d = ta.validate_json(
'[{"id":"1","name":"Alice","role":"user"},{"id":"1","name":"Ben","role":"user"},{"id":"1","name":"Char"}]',
experimental_allow_partial=True,
)
print(d)
#> [UserRecord(id=1, name='Alice', role='user'), UserRecord(id=1, name='Ben', role='user')]
2. Support default factories taking validated data as an argument
default_factory ですでにバリデーション済みのデータを使用することが可能となりました。
Pydantic のフィールドバリデーションでは、モデル定義ないのフィールドが上から順に処理されるのが基本です。
from pydantic import BaseModel
class Model(BaseModel):
a: int = 1
b: int = Field(default_factory=lambda data: data['a'] * 2)
model = Model()
assert model.b == 2
3. Support for typing.Unpack with @validate_call
Pydantic は、@validate_call
デコレーターが付与された関数で、可変長のキーワード引数を指定するために typing.Unpack
をサポートするようになりました。
「TypedDict をベースにして関数型チェックを行いたい」という目的がある場合に使う?っぽいのですが、そう言うケースってあまりな気がするが。。。そもそも型チェックをしたいのであれば初めから Pydantic
を使えば良いって話なので。おそらく、 TypedDict
の型をすでに利用しているプロジェクトで、バリデーションを導入したいケースとかを想定しているのかな。
from typing import Required, TypedDict, Unpack
from pydantic import ValidationError, validate_call, with_config
@with_config({'strict': True})
class TD(TypedDict, total=False):
a: int
b: Required[str]
@validate_call
def foo(**kwargs: Unpack[TD]):
pass
foo(a=1, b='test')
foo(b='test')
try:
foo(a='1')
except ValidationError as e:
print(e)
"""
2 validation errors for foo
a
Input should be a valid integer [type=int_type, input_value='1', input_type=str]
For further information visit https://errors.pydantic.dev/2.10/v/int_type
b
Field required [type=missing, input_value={'a': '1'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.10/v/missing
"""
4. Support compiled patterns in protected_namespaces
re.compile を使った正規表現で保護する名前空間を定義できるようになりました。
import re
import warnings
from pydantic import BaseModel, ConfigDict
with warnings.catch_warnings(record=True) as caught_warnings:
warnings.simplefilter('always') # 全ての警告をキャッチ
class Model(BaseModel):
safe_field: str # 保護対象外
also_protect_field: str # 保護対象に該当
protect_this: str # 正規表現により保護対象に該当
# カスタムの protected_namespaces を設定
model_config = ConfigDict(
protected_namespaces=(
'protect_me_', # 接頭辞に基づく保護
'also_protect_', # 別の接頭辞
re.compile('^protect_this$'), # 正規表現で特定のフィールド名を保護
)
)
# キャッチした警告を出力
for warning in caught_warnings:
print(f'{warning.message}')
# Field "also_protect_field" in Model has conflict with protected namespace "also_protect_".
# You may be able to resolve this warning by setting `model_config['protected_namespaces'] = ('protect_me_', re.compile('^protect_this$'))`.
# Field "protect_this" in Model has conflict with protected namespace "re.compile('^protect_this$')".
# You may be able to resolve this warning by setting `model_config['protected_namespaces'] = ('protect_me_', 'also_protect_')`.
protected_namespaces の役割
Pydantic の protected_namespaces は、Pydantic モデル内のフィールド名が特定の規則(接頭辞や正規表現)に該当しないように制約を加える仕組みです。
protected_namespaces を利用すると、特定の名前(または名前パターン)を 予約語として保護 できます。
これにより、特定の名前空間を避けるべきフィールド名として警告を発することができます。
5. Support defer_build for TypeAdapter and Pydantic dataclasses
defer_build
の設定が Pydantic dataclasses
, TypeAdapter
にも適用されるようになりました。
defer 設定はスキーマ構築、バリデーター、シリアライザーの構築を最初に実行されるタイミングまで遅延させることでアプリケーションの起動時間を短縮できます。大規模アプリケーションでは起動時間の大幅な改善が見込まれるとのことです。
from pydantic import ConfigDict, TypeAdapter
# TypeAdapter インスタンスを作成
# スキーマ構築を遅延させる設定を適用
ta = TypeAdapter('MyInt', config=ConfigDict(defer_build=True))
# 後で前方参照(forward reference)を定義
MyInt = int
# スキーマ構築を手動でトリガー
ta.rebuild()
# バリデーションが正しく機能することを確認
assert ta.validate_python(1) == 1
from pydantic import ConfigDict
from pydantic.dataclasses import dataclass
@dataclass(config=ConfigDict(defer_build=True))
class MyDataClass:
x: int
y: str
# データクラスのインスタンスを作成
# スキーマ構築は遅延
data = MyDataClass(x=1, y="test")
from pydantic import BaseModel, ConfigDict
class MyModel(BaseModel, config=ConfigDict(defer_build=True)):
x: int
y: str
# インスタンス化時にはスキーマは構築されていない
model = MyModel(x=1, y="test")
# 初回利用時にスキーマ構築がトリガーされる
print(model) # MyModel(x=1, y='test')
6. Support for fractions.Fraction
Python 標準ライブラリの fractions.Fraction
型をネイティブにサポートするようになりました。
from pydantic import TypeAdapter
from fractions import Fraction
from decimal import Decimal
# TypeAdapter を使った Fraction サポート
fraction_adapter = TypeAdapter(Fraction)
# バリデーション例
assert fraction_adapter.validate_python('3/2') == Fraction(3, 2) # 文字列形式
assert fraction_adapter.validate_python(Fraction(3, 2)) == Fraction(3, 2) # Fraction インスタンス
assert fraction_adapter.validate_python(Decimal('1.5')) == Fraction(3, 2) # Decimal 型
assert fraction_adapter.validate_python(1.5) == Fraction(3, 2) # 浮動小数点数
# シリアライゼーション例
assert fraction_adapter.dump_python(Fraction(3, 2)) == '3/2'
7. Incompatibility warnings for mixing v1 and v2 models
v1, v2 のモデルを混ぜて使用すると warging が出力されるようになりました。
from pydantic import BaseModel as BaseModelV2
from pydantic.v1 import BaseModel as BaseModelV1
class V1Model(BaseModelV1):
...
class V2Model(BaseModelV2):
inner: V1Model
"""
UserWarning: Nesting V1 models inside V2 models is not supported. Please upgrade V1Model to V2.
"""
8. Expose public sort method for JSON schema generation
GenerateJsonSchema
クラスに sort メソッドが追加され利用可能となりました。GenerateJsonSchema
を継承して、sort メソッドをオーバーライドすることで、キーの順序を自由にカスタマイズできます。
import json
from typing import Optional
from pydantic import BaseModel, Field
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
class MyGenerateJsonSchema(GenerateJsonSchema):
def sort(
self, value: JsonSchemaValue, parent_key: Optional[str] = None
) -> JsonSchemaValue:
"""No-op, we don't want to sort schema values at all."""
return value
class Bar(BaseModel):
c: str
b: str
a: str = Field(json_schema_extra={'c': 'hi', 'b': 'hello', 'a': 'world'})
json_schema = Bar.model_json_schema(schema_generator=MyGenerateJsonSchema)
print(json.dumps(json_schema, indent=2))
"""
{
"type": "object",
"properties": {
"c": {
"type": "string",
"title": "C"
},
"b": {
"type": "string",
"title": "B"
},
"a": {
"type": "string",
"c": "hi",
"b": "hello",
"a": "world",
"title": "A"
}
},
"required": [
"c",
"b",
"a"
],
"title": "Bar"
}
"""
import json
from typing import Optional
from pydantic import BaseModel, Field
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
class MyCustomSortJsonSchema(GenerateJsonSchema):
def sort(
self, value: JsonSchemaValue, parent_key: Optional[str] = None
) -> JsonSchemaValue:
"""カスタムソート: キーをアルファベット順にソート"""
if isinstance(value, dict):
return {key: self.sort(val) for key, val in sorted(value.items())}
elif isinstance(value, list):
return [self.sort(item) for item in value]
return value
class ExampleModel(BaseModel):
z_field: str
a_field: int
b_field: float = Field(json_schema_extra={'example': 'value'})
# カスタムソートを適用して JSON Schema を生成
json_schema = ExampleModel.model_json_schema(schema_generator=MyCustomSortJsonSchema)
print(json.dumps(json_schema, indent=2))
"""
{
"properties": {
"a_field": {
"title": "A Field",
"type": "integer"
},
"b_field": {
"example": "value",
"title": "B Field",
"type": "number"
},
"z_field": {
"title": "Z Field",
"type": "string"
}
},
"required": [
"a_field",
"z_field"
],
"title": "ExampleModel",
"type": "object"
}
"""
9. Allow for subclassing of ValidationError and PydanticCustomError
Pydantic の ValidationError
と PydanticCustomError
をサブクラス化(継承)して、独自のエラーメッセージや振る舞いを持つカスタム例外を作成できるようになりました。
この辺り、ちゃんと調べてませんが、以前は ValidationError
や PydanticCustomError
の内部動作(例えばエラーメッセージの生成や構造化されたデータの処理)は、固定的で拡張性が低かったみたいですね。
from pydantic import BaseModel, ValidationError
class CustomValidationError(ValidationError):
"""カスタム ValidationError"""
def __init__(self, message: str, field_name: str):
self.message = message
self.field_name = field_name
super().__init__(errors=[{'msg': message, 'loc': (field_name,)}])
def __str__(self):
return f"Validation Error in field '{self.field_name}': {self.message}"
class MyModel(BaseModel):
x: int
@classmethod
def validate_x(cls, value):
if value < 0:
raise CustomValidationError("Value must be positive", field_name="x")
return value
try:
MyModel(x=-1)
except CustomValidationError as e:
print(e)
# 出力: Validation Error in field 'x': Value must be positive
from pydantic_core import PydanticCustomError
from pydantic import TypeAdapter
class CustomError(PydanticCustomError):
"""カスタム PydanticCustomError"""
def __init__(self, error_type: str, message: str):
super().__init__(type=error_type, message=message)
def validate_positive(value):
if value < 0:
raise CustomError('negative_value', 'Value must be positive')
return value
adapter = TypeAdapter(int)
try:
adapter.validate_python(-1, validators=[validate_positive])
except CustomError as e:
print(f"Error Type: {e.type}, Message: {e.message}")
# 出力: Error Type: negative_value, Message: Value must be positive
- パフォーマンス向上
詳細はこちら
第3部: Pydantic の高速化検証 ⏫
前回の記事でも行ったパフォーマンス比較を行いたいと思います。
実行環境
- M1 Mac mini 16GB
- python 3.11.5
検証コード
import random
import time
from datetime import datetime
from pydantic import BaseModel
random.seed(314) # seed値を固定
class Prediction(BaseModel):
score: float
version: str
predicted_at: datetime
def measure_performance():
# 固定の日時を用意
fixed_datetime = datetime(2023, 1, 1, 12, 0, 0)
# テストデータを事前生成
test_data = [
{
"score": random.random(),
"version": "v1",
"predicted_at": fixed_datetime,
}
for _ in range(1_000_000)
]
start = time.time()
for data in test_data:
prediction_result = Prediction.model_validate(data)
end = time.time()
return end - start
# 10回繰り返し計測
iterations = 10
results = [measure_performance() for _ in range(iterations)]
average_time = sum(results) / iterations
print(f"Average time over {iterations} iterations: {average_time:.2f} seconds")
import random
import time
from datetime import datetime
from pydantic import BaseModel
random.seed(314) # seed値を固定
class Whatever(BaseModel):
hoge: str
hoge2: str
hoge3: str
class User(BaseModel):
name: str
age: int
whatever: Whatever
class Prediction(BaseModel):
score: float
version: str
predicted_at: datetime
user: User
def measure_performance():
# 固定日時を使用
fixed_datetime = datetime(2023, 1, 1, 12, 0, 0)
# テストデータを事前生成
test_data = [
{
"score": random.random(),
"version": "v1",
"predicted_at": fixed_datetime,
"user": {
"name": "test",
"age": random.randint(0, 150),
"whatever": {"hoge": "hoge", "hoge2": "hoge2", "hoge3": "hoge3"},
},
}
for _ in range(1_000_000)
]
start = time.time()
# モデル検証をループ
for data in test_data:
prediction_result = Prediction.model_validate(data)
end = time.time()
return end - start
# 10回繰り返して計測
iterations = 10
results = [measure_performance() for _ in range(iterations)]
average_time = sum(results) / iterations
print(f"Average time over {iterations} iterations: {average_time:.2f} seconds")
結果は下記の表のようになりました。[3]
ライブラリ | ネストなし処理時間(s) | ネストあり処理時間(s) |
---|---|---|
pydantic 2.5.1 | 0.712 | 1.675 |
pydantic 2.6.4 | 0.677 | 1.714 |
pydantic 2.7.4 | 0.629 | 1.576 |
pydantic 2.8.2 | 0.621 | 1.595 |
pydantic 2.9.2 | 0.611 | 1.612 |
pydantic 2.10.2 | 0.618 | 1.580 |
Pydantic のバージョンが上がるにつれて、パフォーマンスがわずかに向上している?? と言えなくもなさそうです。
リリース情報を見る限り、主に バリデーションの最適化 や Rust ベースの処理最適化 がパフォーマンス向上に寄与していると考えられそうです。
まとめ
今年も引き続き Python の型ヒントの進化についてまとめながらキャッチアップを行いました。色々便利になりましたが、覚えることも多くなりキャッチアップが大変ですね。。。
Pydantic は特に LLM
に特化した機能リリースが個人的に大きかったのではないかと感じています。次の v2.11
のリリースでは パフォーマンス向上にフォーカスするとの記載 [4]もあり今後のアップデートが楽しみです。
参考文献
https://docs.pydantic.dev/latest/
https://docs.python.org/ja/3/library/typing.html
-
3.14 についてはまだ dev 状態のため今回は取り上げません。 ↩︎
-
通常の Protocol は、静的型チェック専用であり、isinstance()や issubclass()では使用できません。@runtime_checkable を追加すると、isinstance()を使って実行時に型がプロトコルを満たしているかを確認できるようになります。 ↩︎
-
厳密な調査ではありません。参考の数値としてご覧ください。 ↩︎
-
For this release, we focused on adding a variety of new features and bug fixes. We'll be pivoting to a focus on performance improvements in the next release, v2.11. ↩︎
Discussion