I want to check the type of a Pydantic FieldInfo:
from pydantic import BaseModel
from pydantic.fields import FieldInfo
class Address(BaseModel):
street: str
city: str
postcode: int
class Person(BaseModel):
name: str
address: Address
def is_string(field_info: FieldInfo):
print(f'checking field_info: {field_info}')
print(isinstance(field_info.annotation, str))
print(isinstance(field_info.annotation, type(str)))
print(field_info.annotation == str)
if __name__ == '__main__':
is_string(Person.model_fields['name'])
is_string(Person.model_fields['address'])
The output is:
checking field_info: annotation=str required=True
False
True
True
checking field_info: annotation=Address required=True
False
True
False
but I would expect:
checking field_info: annotation=str required=True
True
True
True
checking field_info: annotation=Address required=True
False
False
False
I do not understand why.
Notes:
Two of the checks in the program from the question produce apparently illogical results because:
str is not itself a str (that is, while isinstance("Hello, World", str) is True, isinstance(str, str) is False)
type(str) is just type , and Address is a type just like str is (that is, isinstance(Address, type(str)) is True because isinstance(Address, type) is True)
The correct way to compare types is == , as used in the program's third check. (For instance, field.annotation == str)
There are some caveats when it comes to type comparisons in Python. (In fact, there are many.)
If you'd like to admit subclasses, use the issubclass function (for instance; issubclass(field.annotation, (str,)) will catch subclasses of str)
If your type might be a composite expression, then the annotation may not be a true type. For instance, generic expressions (like dict[str, int]) optional types (like Optional[int]), union types (like int | str) and Annotated types (like Annotated[int, "coolInt"]) all fail to satisfy isinstance(expr, type).
All of these forms support == in intuitively reasonable ways
In isinstance(obj, y) ; y must be a Union or a type.
In issubclass(x, y) ; x must be a type, y must be a Union or a type.
The supported tools for getting a type out of a potential non-type are get_origin and get_args from the typing module.
from typing import Annotated, Any, Union, get_origin, get_args
from types import UnionType, NoneType
def extract_base_type(ty: Any) -> Any:
"""
Given a value representing some possibly-annotated Python type,
extract the "base" type -- (that is, that type with several common
forms of composite expression reduced away.)
"""
t = ty
while True:
# check if we received a type
if isinstance(t, type):
return t
# what we received wasn't a type -- so what was it?
origin, args = get_origin(t), get_args(t)
if isinstance(origin, type):
return t # generic expression -- note that `t` is not a type!
elif origin == Annotated:
assert len(args) == 2
t = args[0]
elif origin == Union or origin == UnionType:
non_null_args = [a for a in args if a != NoneType]
if len(non_null_args) == 1:
t = non_null_args[0]
continue
raise TypeError(f"no single base type exists for the Union in {ty!r}")
else:
raise TypeError(f"unexpected non-type in {ty!r}")