pydantic

How to propertly check the type of a Pydantic FieldInfo?


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:


Solution

  • Two of the checks in the program from the question produce apparently illogical results because:

    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.)

    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}")