Skip to content

Models

The primary means of defining objects in pydantic is via models (models are simply classes which inherit from BaseModel).

You can think of models as similar to types in strictly typed languages, or as the requirements of a single endpoint in an API.

Untrusted data can be passed to a model, and after parsing and validation pydantic guarantees that the fields of the resultant model instance will conform to the field types defined on the model.

Note

pydantic is primarily a parsing library, not a validation library. Validation is a means to an end: building a model which conforms to the types and constraints provided.

In other words, pydantic guarantees the types and constraints of the output model, not the input data.

This might sound like an esoteric distinction, but it is not. If you're unsure what this means or how it might affect your usage you should read the section about Data Conversion below.

Although validation is not the main purpose of pydantic, you can use this library for custom validation.

Basic model usage

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'Jane Doe'
User here is a model with two fields id which is an integer and is required, and name which is a string and is not required (it has a default value). The type of name is inferred from the default value, and so a type annotation is not required (however note this warning about field order when some fields do not have type annotations).
user = User(id='123')
user_x = User(id='123.45')
user here is an instance of User. Initialisation of the object will perform all parsing and validation, if no ValidationError is raised, you know the resulting model instance is valid.
assert user.id == 123
assert user_x.id == 123
assert isinstance(user_x.id, int)  # Note that 123.45 was casted to an int and its value is 123
More details on the casting in the case of user_x can be found in Data Conversion. Fields of a model can be accessed as normal attributes of the user object. The string '123' has been cast to an int as per the field type
assert user.name == 'Jane Doe'
name wasn't set when user was initialised, so it has the default value
assert user.__fields_set__ == {'id'}
The fields which were supplied when user was initialised.
assert user.dict() == dict(user) == {'id': 123, 'name': 'Jane Doe'}
Either .dict() or dict(user) will provide a dict of fields, but .dict() can take numerous other arguments.
user.id = 321
assert user.id == 321
This model is mutable so field values can be changed.

Model properties

The example above only shows the tip of the iceberg of what models can do. Models possess the following methods and attributes:

dict()
returns a dictionary of the model's fields and values; cf. exporting models
json()
returns a JSON string representation dict(); cf. exporting models
copy()
returns a copy (by default, shallow copy) of the model; cf. exporting models
parse_obj()
a utility for loading any object into a model with error handling if the object is not a dictionary; cf. helper functions
parse_raw()
a utility for loading strings of numerous formats; cf. helper functions
parse_file()
like parse_raw() but for file paths; cf. helper functions
from_orm()
loads data into a model from an arbitrary class; cf. ORM mode
schema()
returns a dictionary representing the model as JSON Schema; cf. schema
schema_json()
returns a JSON string representation of schema(); cf. schema
construct()
a class method for creating models without running validation; cf. Creating models without validation
__fields_set__
Set of names of fields which were set when the model instance was initialised
__fields__
a dictionary of the model's fields
__config__
the configuration class for the model, cf. model config

Recursive Models

More complex hierarchical data structures can be defined using models themselves as types in annotations.

from typing import List, Optional
from pydantic import BaseModel


class Foo(BaseModel):
    count: int
    size: Optional[float] = None


class Bar(BaseModel):
    apple = 'x'
    banana = 'y'


class Spam(BaseModel):
    foo: Foo
    bars: List[Bar]


m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
print(m)
#> foo=Foo(count=4, size=None) bars=[Bar(apple='x1', banana='y'),
#> Bar(apple='x2', banana='y')]
print(m.dict())
"""
{
    'foo': {'count': 4, 'size': None},
    'bars': [
        {'apple': 'x1', 'banana': 'y'},
        {'apple': 'x2', 'banana': 'y'},
    ],
}
"""
from typing import Optional
from pydantic import BaseModel


class Foo(BaseModel):
    count: int
    size: Optional[float] = None


class Bar(BaseModel):
    apple = 'x'
    banana = 'y'


class Spam(BaseModel):
    foo: Foo
    bars: list[Bar]


m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
print(m)
#> foo=Foo(count=4, size=None) bars=[Bar(apple='x1', banana='y'),
#> Bar(apple='x2', banana='y')]
print(m.dict())
"""
{
    'foo': {'count': 4, 'size': None},
    'bars': [
        {'apple': 'x1', 'banana': 'y'},
        {'apple': 'x2', 'banana': 'y'},
    ],
}
"""
from pydantic import BaseModel


class Foo(BaseModel):
    count: int
    size: float | None = None


class Bar(BaseModel):
    apple = 'x'
    banana = 'y'


class Spam(BaseModel):
    foo: Foo
    bars: list[Bar]


m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
print(m)
#> foo=Foo(count=4, size=None) bars=[Bar(apple='x1', banana='y'),
#> Bar(apple='x2', banana='y')]
print(m.dict())
"""
{
    'foo': {'count': 4, 'size': None},
    'bars': [
        {'apple': 'x1', 'banana': 'y'},
        {'apple': 'x2', 'banana': 'y'},
    ],
}
"""

(This script is complete, it should run "as is")

For self-referencing models, see postponed annotations.

ORM Mode (aka Arbitrary Class Instances)

Pydantic models can be created from arbitrary class instances to support models that map to ORM objects.

To do this:

  1. The Config property orm_mode must be set to True.
  2. The special constructor from_orm must be used to create the model instance.

The example here uses SQLAlchemy, but the same approach should work for any ORM.

from typing import List
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, constr

Base = declarative_base()


class CompanyOrm(Base):
    __tablename__ = 'companies'
    id = Column(Integer, primary_key=True, nullable=False)
    public_key = Column(String(20), index=True, nullable=False, unique=True)
    name = Column(String(63), unique=True)
    domains = Column(ARRAY(String(255)))


class CompanyModel(BaseModel):
    id: int
    public_key: constr(max_length=20)
    name: constr(max_length=63)
    domains: List[constr(max_length=255)]

    class Config:
        orm_mode = True


co_orm = CompanyOrm(
    id=123,
    public_key='foobar',
    name='Testing',
    domains=['example.com', 'foobar.com'],
)
print(co_orm)
#> <models_orm_mode.CompanyOrm object at 0x7f6ad333c1c0>
co_model = CompanyModel.from_orm(co_orm)
print(co_model)
#> id=123 public_key='foobar' name='Testing' domains=['example.com',
#> 'foobar.com']
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, constr

Base = declarative_base()


class CompanyOrm(Base):
    __tablename__ = 'companies'
    id = Column(Integer, primary_key=True, nullable=False)
    public_key = Column(String(20), index=True, nullable=False, unique=True)
    name = Column(String(63), unique=True)
    domains = Column(ARRAY(String(255)))


class CompanyModel(BaseModel):
    id: int
    public_key: constr(max_length=20)
    name: constr(max_length=63)
    domains: list[constr(max_length=255)]

    class Config:
        orm_mode = True


co_orm = CompanyOrm(
    id=123,
    public_key='foobar',
    name='Testing',
    domains=['example.com', 'foobar.com'],
)
print(co_orm)
#> <models_orm_mode_3_9.CompanyOrm object at 0x7f6ad34e7ac0>
co_model = CompanyModel.from_orm(co_orm)
print(co_model)
#> id=123 public_key='foobar' name='Testing' domains=['example.com',
#> 'foobar.com']

(This script is complete, it should run "as is")

Reserved names

You may want to name a Column after a reserved SQLAlchemy field. In that case, Field aliases will be convenient:

import typing

from pydantic import BaseModel, Field
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base


class MyModel(BaseModel):
    metadata: typing.Dict[str, str] = Field(alias='metadata_')

    class Config:
        orm_mode = True


Base = declarative_base()


class SQLModel(Base):
    __tablename__ = 'my_table'
    id = sa.Column('id', sa.Integer, primary_key=True)
    # 'metadata' is reserved by SQLAlchemy, hence the '_'
    metadata_ = sa.Column('metadata', sa.JSON)


sql_model = SQLModel(metadata_={'key': 'val'}, id=1)

pydantic_model = MyModel.from_orm(sql_model)

print(pydantic_model.dict())
#> {'metadata': {'key': 'val'}}
print(pydantic_model.dict(by_alias=True))
#> {'metadata_': {'key': 'val'}}
from pydantic import BaseModel, Field
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base


class MyModel(BaseModel):
    metadata: dict[str, str] = Field(alias='metadata_')

    class Config:
        orm_mode = True


Base = declarative_base()


class SQLModel(Base):
    __tablename__ = 'my_table'
    id = sa.Column('id', sa.Integer, primary_key=True)
    # 'metadata' is reserved by SQLAlchemy, hence the '_'
    metadata_ = sa.Column('metadata', sa.JSON)


sql_model = SQLModel(metadata_={'key': 'val'}, id=1)

pydantic_model = MyModel.from_orm(sql_model)

print(pydantic_model.dict())
#> {'metadata': {'key': 'val'}}
print(pydantic_model.dict(by_alias=True))
#> {'metadata_': {'key': 'val'}}

(This script is complete, it should run "as is")

Note

The example above works because aliases have priority over field names for field population. Accessing SQLModel's metadata attribute would lead to a ValidationError.

Recursive ORM models

ORM instances will be parsed with from_orm recursively as well as at the top level.

Here a vanilla class is used to demonstrate the principle, but any ORM class could be used instead.

from typing import List
from pydantic import BaseModel


class PetCls:
    def __init__(self, *, name: str, species: str):
        self.name = name
        self.species = species


class PersonCls:
    def __init__(self, *, name: str, age: float = None, pets: List[PetCls]):
        self.name = name
        self.age = age
        self.pets = pets


class Pet(BaseModel):
    name: str
    species: str

    class Config:
        orm_mode = True


class Person(BaseModel):
    name: str
    age: float = None
    pets: List[Pet]

    class Config:
        orm_mode = True


bones = PetCls(name='Bones', species='dog')
orion = PetCls(name='Orion', species='cat')
anna = PersonCls(name='Anna', age=20, pets=[bones, orion])
anna_model = Person.from_orm(anna)
print(anna_model)
#> name='Anna' age=20.0 pets=[Pet(name='Bones', species='dog'),
#> Pet(name='Orion', species='cat')]
from pydantic import BaseModel


class PetCls:
    def __init__(self, *, name: str, species: str):
        self.name = name
        self.species = species


class PersonCls:
    def __init__(self, *, name: str, age: float = None, pets: list[PetCls]):
        self.name = name
        self.age = age
        self.pets = pets


class Pet(BaseModel):
    name: str
    species: str

    class Config:
        orm_mode = True


class Person(BaseModel):
    name: str
    age: float = None
    pets: list[Pet]

    class Config:
        orm_mode = True


bones = PetCls(name='Bones', species='dog')
orion = PetCls(name='Orion', species='cat')
anna = PersonCls(name='Anna', age=20, pets=[bones, orion])
anna_model = Person.from_orm(anna)
print(anna_model)
#> name='Anna' age=20.0 pets=[Pet(name='Bones', species='dog'),
#> Pet(name='Orion', species='cat')]

(This script is complete, it should run "as is")

Data binding

Arbitrary classes are processed by pydantic using the GetterDict class (see utils.py), which attempts to provide a dictionary-like interface to any class. You can customise how this works by setting your own sub-class of GetterDict as the value of Config.getter_dict (see config).

You can also customise class validation using root_validators with pre=True. In this case your validator function will be passed a GetterDict instance which you may copy and modify.

The GetterDict instance will be called for each field with a sentinel as a fallback (if no other default value is set). Returning this sentinel means that the field is missing. Any other value will be interpreted as the value of the field.

from pydantic import BaseModel
from typing import Any, Optional
from pydantic.utils import GetterDict
from xml.etree.ElementTree import fromstring


xmlstring = """
<User Id="2138">
    <FirstName />
    <LoggedIn Value="true" />
</User>
"""


class UserGetter(GetterDict):

    def get(self, key: str, default: Any) -> Any:

        # element attributes
        if key in {'Id', 'Status'}:
            return self._obj.attrib.get(key, default)

        # element children
        else:
            try:
                return self._obj.find(key).attrib['Value']
            except (AttributeError, KeyError):
                return default


class User(BaseModel):
    Id: int
    Status: Optional[str]
    FirstName: Optional[str]
    LastName: Optional[str]
    LoggedIn: bool

    class Config:
        orm_mode = True
        getter_dict = UserGetter


user = User.from_orm(fromstring(xmlstring))
from pydantic import BaseModel
from typing import Any
from pydantic.utils import GetterDict
from xml.etree.ElementTree import fromstring


xmlstring = """
<User Id="2138">
    <FirstName />
    <LoggedIn Value="true" />
</User>
"""


class UserGetter(GetterDict):

    def get(self, key: str, default: Any) -> Any:

        # element attributes
        if key in {'Id', 'Status'}:
            return self._obj.attrib.get(key, default)

        # element children
        else:
            try:
                return self._obj.find(key).attrib['Value']
            except (AttributeError, KeyError):
                return default


class User(BaseModel):
    Id: int
    Status: str | None
    FirstName: str | None
    LastName: str | None
    LoggedIn: bool

    class Config:
        orm_mode = True
        getter_dict = UserGetter


user = User.from_orm(fromstring(xmlstring))

(This script is complete, it should run "as is")

Error Handling

pydantic will raise ValidationError whenever it finds an error in the data it's validating.

Note

Validation code should not raise ValidationError itself, but rather raise ValueError, TypeError or AssertionError (or subclasses of ValueError or TypeError) which will be caught and used to populate ValidationError.

One exception will be raised regardless of the number of errors found, that ValidationError will contain information about all the errors and how they happened.

You can access these errors in several ways:

e.errors()
method will return list of errors found in the input data.
e.json()
method will return a JSON representation of errors.
str(e)
method will return a human readable representation of the errors.

Each error object contains:

loc
the error's location as a list. The first item in the list will be the field where the error occurred, and if the field is a sub-model, subsequent items will be present to indicate the nested location of the error.
type
a computer-readable identifier of the error type.
msg
a human readable explanation of the error.
ctx
an optional object which contains values required to render the error message.

As a demonstration:

from typing import List
from pydantic import BaseModel, ValidationError, conint


class Location(BaseModel):
    lat = 0.1
    lng = 10.1


class Model(BaseModel):
    is_required: float
    gt_int: conint(gt=42)
    list_of_ints: List[int] = None
    a_float: float = None
    recursive_model: Location = None


data = dict(
    list_of_ints=['1', 2, 'bad'],
    a_float='not a float',
    recursive_model={'lat': 4.2, 'lng': 'New York'},
    gt_int=21,
)

try:
    Model(**data)
except ValidationError as e:
    print(e)
    """
    5 validation errors for Model
    is_required
      field required (type=value_error.missing)
    gt_int
      ensure this value is greater than 42 (type=value_error.number.not_gt;
    limit_value=42)
    list_of_ints -> 2
      value is not a valid integer (type=type_error.integer)
    a_float
      value is not a valid float (type=type_error.float)
    recursive_model -> lng
      value is not a valid float (type=type_error.float)
    """

try:
    Model(**data)
except ValidationError as e:
    print(e.json())
    """
    [
      {
        "loc": [
          "is_required"
        ],
        "msg": "field required",
        "type": "value_error.missing"
      },
      {
        "loc": [
          "gt_int"
        ],
        "msg": "ensure this value is greater than 42",
        "type": "value_error.number.not_gt",
        "ctx": {
          "limit_value": 42
        }
      },
      {
        "loc": [
          "list_of_ints",
          2
        ],
        "msg": "value is not a valid integer",
        "type": "type_error.integer"
      },
      {
        "loc": [
          "a_float"
        ],
        "msg": "value is not a valid float",
        "type": "type_error.float"
      },
      {
        "loc": [
          "recursive_model",
          "lng"
        ],
        "msg": "value is not a valid float",
        "type": "type_error.float"
      }
    ]
    """
from pydantic import BaseModel, ValidationError, conint


class Location(BaseModel):
    lat = 0.1
    lng = 10.1


class Model(BaseModel):
    is_required: float
    gt_int: conint(gt=42)
    list_of_ints: list[int] = None
    a_float: float = None
    recursive_model: Location = None


data = dict(
    list_of_ints=['1', 2, 'bad'],
    a_float='not a float',
    recursive_model={'lat': 4.2, 'lng': 'New York'},
    gt_int=21,
)

try:
    Model(**data)
except ValidationError as e:
    print(e)
    """
    5 validation errors for Model
    is_required
      field required (type=value_error.missing)
    gt_int
      ensure this value is greater than 42 (type=value_error.number.not_gt;
    limit_value=42)
    list_of_ints -> 2
      value is not a valid integer (type=type_error.integer)
    a_float
      value is not a valid float (type=type_error.float)
    recursive_model -> lng
      value is not a valid float (type=type_error.float)
    """

try:
    Model(**data)
except ValidationError as e:
    print(e.json())
    """
    [
      {
        "loc": [
          "is_required"
        ],
        "msg": "field required",
        "type": "value_error.missing"
      },
      {
        "loc": [
          "gt_int"
        ],
        "msg": "ensure this value is greater than 42",
        "type": "value_error.number.not_gt",
        "ctx": {
          "limit_value": 42
        }
      },
      {
        "loc": [
          "list_of_ints",
          2
        ],
        "msg": "value is not a valid integer",
        "type": "type_error.integer"
      },
      {
        "loc": [
          "a_float"
        ],
        "msg": "value is not a valid float",
        "type": "type_error.float"
      },
      {
        "loc": [
          "recursive_model",
          "lng"
        ],
        "msg": "value is not a valid float",
        "type": "type_error.float"
      }
    ]
    """

(This script is complete, it should run "as is")

Custom Errors

In your custom data types or validators you should use ValueError, TypeError or AssertionError to raise errors.

See validators for more details on use of the @validator decorator.

from pydantic import BaseModel, ValidationError, validator


class Model(BaseModel):
    foo: str

    @validator('foo')
    def value_must_equal_bar(cls, v):
        if v != 'bar':
            raise ValueError('value must be "bar"')

        return v


try:
    Model(foo='ber')
except ValidationError as e:
    print(e.errors())
    """
    [
        {
            'loc': ('foo',),
            'msg': 'value must be "bar"',
            'type': 'value_error',
        },
    ]
    """

(This script is complete, it should run "as is")

You can also define your own error classes, which can specify a custom error code, message template, and context:

from pydantic import BaseModel, PydanticValueError, ValidationError, validator


class NotABarError(PydanticValueError):
    code = 'not_a_bar'
    msg_template = 'value is not "bar", got "{wrong_value}"'


class Model(BaseModel):
    foo: str

    @validator('foo')
    def value_must_equal_bar(cls, v):
        if v != 'bar':
            raise NotABarError(wrong_value=v)
        return v


try:
    Model(foo='ber')
except ValidationError as e:
    print(e.json())
    """
    [
      {
        "loc": [
          "foo"
        ],
        "msg": "value is not \"bar\", got \"ber\"",
        "type": "value_error.not_a_bar",
        "ctx": {
          "wrong_value": "ber"
        }
      }
    ]
    """

(This script is complete, it should run "as is")

Helper Functions

Pydantic provides three classmethod helper functions on models for parsing data:

  • parse_obj: this is very similar to the __init__ method of the model, except it takes a dict rather than keyword arguments. If the object passed is not a dict a ValidationError will be raised.
  • parse_raw: this takes a str or bytes and parses it as json, then passes the result to parse_obj. Parsing pickle data is also supported by setting the content_type argument appropriately.
  • parse_file: this takes in a file path, reads the file and passes the contents to parse_raw. If content_type is omitted, it is inferred from the file's extension.
import pickle
from datetime import datetime
from pathlib import Path

from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None


m = User.parse_obj({'id': 123, 'name': 'James'})
print(m)
#> id=123 signup_ts=None name='James'

try:
    User.parse_obj(['not', 'a', 'dict'])
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    __root__
      User expected dict not list (type=type_error)
    """

# assumes json as no content type passed
m = User.parse_raw('{"id": 123, "name": "James"}')
print(m)
#> id=123 signup_ts=None name='James'

pickle_data = pickle.dumps({
    'id': 123,
    'name': 'James',
    'signup_ts': datetime(2017, 7, 14)
})
m = User.parse_raw(
    pickle_data, content_type='application/pickle', allow_pickle=True
)
print(m)
#> id=123 signup_ts=datetime.datetime(2017, 7, 14, 0, 0) name='James'

path = Path('data.json')
path.write_text('{"id": 123, "name": "James"}')
m = User.parse_file(path)
print(m)
#> id=123 signup_ts=None name='James'

(This script is complete, it should run "as is")

Warning

To quote the official pickle docs, "The pickle module is not secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source."

Info

Because it can result in arbitrary code execution, as a security measure, you need to explicitly pass allow_pickle to the parsing function in order to load pickle data.

Creating models without validation

pydantic also provides the construct() method which allows models to be created without validation this can be useful when data has already been validated or comes from a trusted source and you want to create a model as efficiently as possible (construct() is generally around 30x faster than creating a model with full validation).

Warning

construct() does not do any validation, meaning it can create models which are invalid. You should only ever use the construct() method with data which has already been validated, or you trust.

from pydantic import BaseModel


class User(BaseModel):
    id: int
    age: int
    name: str = 'John Doe'


original_user = User(id=123, age=32)

user_data = original_user.dict()
print(user_data)
#> {'id': 123, 'age': 32, 'name': 'John Doe'}
fields_set = original_user.__fields_set__
print(fields_set)
#> {'id', 'age'}

# ...
# pass user_data and fields_set to RPC or save to the database etc.
# ...

# you can then create a new instance of User without
# re-running validation which would be unnecessary at this point:
new_user = User.construct(_fields_set=fields_set, **user_data)
print(repr(new_user))
#> User(id=123, age=32, name='John Doe')
print(new_user.__fields_set__)
#> {'id', 'age'}

# construct can be dangerous, only use it with validated data!:
bad_user = User.construct(id='dog')
print(repr(bad_user))
#> User(id='dog', name='John Doe')

(This script is complete, it should run "as is")

The _fields_set keyword argument to construct() is optional, but allows you to be more precise about which fields were originally set and which weren't. If it's omitted __fields_set__ will just be the keys of the data provided.

For example, in the example above, if _fields_set was not provided, new_user.__fields_set__ would be {'id', 'age', 'name'}.

Generic Models

Pydantic supports the creation of generic models to make it easier to reuse a common model structure.

In order to declare a generic model, you perform the following steps:

  • Declare one or more typing.TypeVar instances to use to parameterize your model.
  • Declare a pydantic model that inherits from pydantic.generics.GenericModel and typing.Generic, where you pass the TypeVar instances as parameters to typing.Generic.
  • Use the TypeVar instances as annotations where you will want to replace them with other types or pydantic models.

Here is an example using GenericModel to create an easily-reused HTTP response payload wrapper:

from typing import Generic, TypeVar, Optional, List

from pydantic import BaseModel, validator, ValidationError
from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Error(BaseModel):
    code: int
    message: str


class DataModel(BaseModel):
    numbers: List[int]
    people: List[str]


class Response(GenericModel, Generic[DataT]):
    data: Optional[DataT]
    error: Optional[Error]

    @validator('error', always=True)
    def check_consistency(cls, v, values):
        if v is not None and values['data'] is not None:
            raise ValueError('must not provide both data and error')
        if v is None and values.get('data') is None:
            raise ValueError('must provide data or error')
        return v


data = DataModel(numbers=[1, 2, 3], people=[])
error = Error(code=404, message='Not found')

print(Response[int](data=1))
#> data=1 error=None
print(Response[str](data='value'))
#> data='value' error=None
print(Response[str](data='value').dict())
#> {'data': 'value', 'error': None}
print(Response[DataModel](data=data).dict())
"""
{
    'data': {'numbers': [1, 2, 3], 'people': []},
    'error': None,
}
"""
print(Response[DataModel](error=error).dict())
"""
{
    'data': None,
    'error': {'code': 404, 'message': 'Not found'},
}
"""
try:
    Response[int](data='value')
except ValidationError as e:
    print(e)
    """
    2 validation errors for Response[int]
    data
      value is not a valid integer (type=type_error.integer)
    error
      must provide data or error (type=value_error)
    """
from typing import Generic, Optional, TypeVar

from pydantic import BaseModel, validator, ValidationError
from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Error(BaseModel):
    code: int
    message: str


class DataModel(BaseModel):
    numbers: list[int]
    people: list[str]


class Response(GenericModel, Generic[DataT]):
    data: Optional[DataT]
    error: Optional[Error]

    @validator('error', always=True)
    def check_consistency(cls, v, values):
        if v is not None and values['data'] is not None:
            raise ValueError('must not provide both data and error')
        if v is None and values.get('data') is None:
            raise ValueError('must provide data or error')
        return v


data = DataModel(numbers=[1, 2, 3], people=[])
error = Error(code=404, message='Not found')

print(Response[int](data=1))
#> data=1 error=None
print(Response[str](data='value'))
#> data='value' error=None
print(Response[str](data='value').dict())
#> {'data': 'value', 'error': None}
print(Response[DataModel](data=data).dict())
"""
{
    'data': {'numbers': [1, 2, 3], 'people': []},
    'error': None,
}
"""
print(Response[DataModel](error=error).dict())
"""
{
    'data': None,
    'error': {'code': 404, 'message': 'Not found'},
}
"""
try:
    Response[int](data='value')
except ValidationError as e:
    print(e)
    """
    2 validation errors for Response[int]
    data
      value is not a valid integer (type=type_error.integer)
    error
      must provide data or error (type=value_error)
    """
from typing import Generic, TypeVar

from pydantic import BaseModel, validator, ValidationError
from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Error(BaseModel):
    code: int
    message: str


class DataModel(BaseModel):
    numbers: list[int]
    people: list[str]


class Response(GenericModel, Generic[DataT]):
    data: DataT | None
    error: Error | None

    @validator('error', always=True)
    def check_consistency(cls, v, values):
        if v is not None and values['data'] is not None:
            raise ValueError('must not provide both data and error')
        if v is None and values.get('data') is None:
            raise ValueError('must provide data or error')
        return v


data = DataModel(numbers=[1, 2, 3], people=[])
error = Error(code=404, message='Not found')

print(Response[int](data=1))
#> data=1 error=None
print(Response[str](data='value'))
#> data='value' error=None
print(Response[str](data='value').dict())
#> {'data': 'value', 'error': None}
print(Response[DataModel](data=data).dict())
"""
{
    'data': {'numbers': [1, 2, 3], 'people': []},
    'error': None,
}
"""
print(Response[DataModel](error=error).dict())
"""
{
    'data': None,
    'error': {'code': 404, 'message': 'Not found'},
}
"""
try:
    Response[int](data='value')
except ValidationError as e:
    print(e)
    """
    2 validation errors for Response[int]
    data
      value is not a valid integer (type=type_error.integer)
    error
      must provide data or error (type=value_error)
    """

(This script is complete, it should run "as is")

If you set Config or make use of validator in your generic model definition, it is applied to concrete subclasses in the same way as when inheriting from BaseModel. Any methods defined on your generic class will also be inherited.

Pydantic's generics also integrate properly with mypy, so you get all the type checking you would expect mypy to provide if you were to declare the type without using GenericModel.

Note

Internally, pydantic uses create_model to generate a (cached) concrete BaseModel at runtime, so there is essentially zero overhead introduced by making use of GenericModel.

To inherit from a GenericModel without replacing the TypeVar instance, a class must also inherit from typing.Generic:

from typing import TypeVar, Generic
from pydantic.generics import GenericModel

TypeX = TypeVar('TypeX')


class BaseClass(GenericModel, Generic[TypeX]):
    X: TypeX


class ChildClass(BaseClass[TypeX], Generic[TypeX]):
    # Inherit from Generic[TypeX]
    pass


# Replace TypeX by int
print(ChildClass[int](X=1))
#> X=1

(This script is complete, it should run "as is")

You can also create a generic subclass of a GenericModel that partially or fully replaces the type parameters in the superclass.

from typing import TypeVar, Generic
from pydantic.generics import GenericModel

TypeX = TypeVar('TypeX')
TypeY = TypeVar('TypeY')
TypeZ = TypeVar('TypeZ')


class BaseClass(GenericModel, Generic[TypeX, TypeY]):
    x: TypeX
    y: TypeY


class ChildClass(BaseClass[int, TypeY], Generic[TypeY, TypeZ]):
    z: TypeZ


# Replace TypeY by str
print(ChildClass[str, int](x=1, y='y', z=3))
#> x=1 y='y' z=3

(This script is complete, it should run "as is")

If the name of the concrete subclasses is important, you can also override the default behavior:

from typing import Generic, TypeVar, Type, Any, Tuple

from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Response(GenericModel, Generic[DataT]):
    data: DataT

    @classmethod
    def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
        return f'{params[0].__name__.title()}Response'


print(repr(Response[int](data=1)))
#> IntResponse(data=1)
print(repr(Response[str](data='a')))
#> StrResponse(data='a')
from typing import Any, Generic, TypeVar

from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Response(GenericModel, Generic[DataT]):
    data: DataT

    @classmethod
    def __concrete_name__(cls: type[Any], params: tuple[type[Any], ...]) -> str:
        return f'{params[0].__name__.title()}Response'


print(repr(Response[int](data=1)))
#> IntResponse(data=1)
print(repr(Response[str](data='a')))
#> StrResponse(data='a')

(This script is complete, it should run "as is")

Using the same TypeVar in nested models allows you to enforce typing relationships at different points in your model:

from typing import Generic, TypeVar

from pydantic import ValidationError
from pydantic.generics import GenericModel

T = TypeVar('T')


class InnerT(GenericModel, Generic[T]):
    inner: T


class OuterT(GenericModel, Generic[T]):
    outer: T
    nested: InnerT[T]


nested = InnerT[int](inner=1)
print(OuterT[int](outer=1, nested=nested))
#> outer=1 nested=InnerT[int](inner=1)
try:
    nested = InnerT[str](inner='a')
    print(OuterT[int](outer='a', nested=nested))
except ValidationError as e:
    print(e)
    """
    2 validation errors for OuterT[int]
    outer
      value is not a valid integer (type=type_error.integer)
    nested -> inner
      value is not a valid integer (type=type_error.integer)
    """

(This script is complete, it should run "as is")

Pydantic also treats GenericModel similarly to how it treats built-in generic types like List and Dict when it comes to leaving them unparameterized, or using bounded TypeVar instances:

  • If you don't specify parameters before instantiating the generic model, they will be treated as Any
  • You can parametrize models with one or more bounded parameters to add subclass checks

Also, like List and Dict, any parameters specified using a TypeVar can later be substituted with concrete types.

from typing import Generic, TypeVar

from pydantic import ValidationError
from pydantic.generics import GenericModel

AT = TypeVar('AT')
BT = TypeVar('BT')


class Model(GenericModel, Generic[AT, BT]):
    a: AT
    b: BT


print(Model(a='a', b='a'))
#> a='a' b='a'

IntT = TypeVar('IntT', bound=int)
typevar_model = Model[int, IntT]
print(typevar_model(a=1, b=1))
#> a=1 b=1
try:
    typevar_model(a='a', b='a')
except ValidationError as exc:
    print(exc)
    """
    2 validation errors for Model[int, IntT]
    a
      value is not a valid integer (type=type_error.integer)
    b
      value is not a valid integer (type=type_error.integer)
    """

concrete_model = typevar_model[int]
print(concrete_model(a=1, b=1))
#> a=1 b=1

(This script is complete, it should run "as is")

Dynamic model creation

There are some occasions where the shape of a model is not known until runtime. For this pydantic provides the create_model method to allow models to be created on the fly.

from pydantic import BaseModel, create_model

DynamicFoobarModel = create_model('DynamicFoobarModel', foo=(str, ...), bar=123)


class StaticFoobarModel(BaseModel):
    foo: str
    bar: int = 123

(This script is complete, it should run "as is")

Here StaticFoobarModel and DynamicFoobarModel are identical.

Warning

See the note in Required Optional Fields for the distinction between an ellipsis as a field default and annotation-only fields. See pydantic/pydantic#1047 for more details.

Fields are defined by either a tuple of the form (<type>, <default value>) or just a default value. The special key word arguments __config__ and __base__ can be used to customise the new model. This includes extending a base model with extra fields.

from pydantic import BaseModel, create_model


class FooModel(BaseModel):
    foo: str
    bar: int = 123


BarModel = create_model(
    'BarModel',
    apple='russet',
    banana='yellow',
    __base__=FooModel,
)
print(BarModel)
#> <class 'pydantic.main.BarModel'>
print(BarModel.__fields__.keys())
#> dict_keys(['foo', 'bar', 'apple', 'banana'])

(This script is complete, it should run "as is")

You can also add validators by passing a dict to the __validators__ argument.

from pydantic import create_model, ValidationError, validator


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


validators = {
    'username_validator':
    validator('username')(username_alphanumeric)
}

UserModel = create_model(
    'UserModel',
    username=(str, ...),
    __validators__=validators
)

user = UserModel(username='scolvin')
print(user)
#> username='scolvin'

try:
    UserModel(username='scolvi%n')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    username
      must be alphanumeric (type=assertion_error)
    """

(This script is complete, it should run "as is")

Model creation from NamedTuple or TypedDict

Sometimes you already use in your application classes that inherit from NamedTuple or TypedDict and you don't want to duplicate all your information to have a BaseModel. For this pydantic provides create_model_from_namedtuple and create_model_from_typeddict methods. Those methods have the exact same keyword arguments as create_model.

from typing_extensions import TypedDict

from pydantic import ValidationError, create_model_from_typeddict


class User(TypedDict):
    name: str
    id: int


class Config:
    extra = 'forbid'


UserM = create_model_from_typeddict(User, __config__=Config)
print(repr(UserM(name=123, id='3')))
#> User(name='123', id=3)

try:
    UserM(name=123, id='3', other='no')
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    other
      extra fields not permitted (type=value_error.extra)
    """

(This script is complete, it should run "as is")

Custom Root Types

Pydantic models can be defined with a custom root type by declaring the __root__ field.

The root type can be any type supported by pydantic, and is specified by the type hint on the __root__ field. The root value can be passed to the model __init__ via the __root__ keyword argument, or as the first and only argument to parse_obj.

from typing import List
import json
from pydantic import BaseModel
from pydantic.schema import schema


class Pets(BaseModel):
    __root__: List[str]


print(Pets(__root__=['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets(__root__=['dog', 'cat']).json())
#> ["dog", "cat"]
print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.schema())
"""
{
    'title': 'Pets',
    'type': 'array',
    'items': {'type': 'string'},
}
"""
pets_schema = schema([Pets])
print(json.dumps(pets_schema, indent=2))
"""
{
  "definitions": {
    "Pets": {
      "title": "Pets",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}
"""
import json
from pydantic import BaseModel
from pydantic.schema import schema


class Pets(BaseModel):
    __root__: list[str]


print(Pets(__root__=['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets(__root__=['dog', 'cat']).json())
#> ["dog", "cat"]
print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.schema())
"""
{
    'title': 'Pets',
    'type': 'array',
    'items': {'type': 'string'},
}
"""
pets_schema = schema([Pets])
print(json.dumps(pets_schema, indent=2))
"""
{
  "definitions": {
    "Pets": {
      "title": "Pets",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}
"""

(This script is complete, it should run "as is")

If you call the parse_obj method for a model with a custom root type with a dict as the first argument, the following logic is used:

  • If the custom root type is a mapping type (eg., Dict or Mapping), the argument itself is always validated against the custom root type.
  • For other custom root types, if the dict has precisely one key with the value __root__, the corresponding value will be validated against the custom root type.
  • Otherwise, the dict itself is validated against the custom root type.

This is demonstrated in the following example:

from typing import List, Dict
from pydantic import BaseModel, ValidationError


class Pets(BaseModel):
    __root__: List[str]


print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.parse_obj({'__root__': ['dog', 'cat']}))  # not recommended
#> __root__=['dog', 'cat']


class PetsByName(BaseModel):
    __root__: Dict[str, str]


print(PetsByName.parse_obj({'Otis': 'dog', 'Milo': 'cat'}))
#> __root__={'Otis': 'dog', 'Milo': 'cat'}
try:
    PetsByName.parse_obj({'__root__': {'Otis': 'dog', 'Milo': 'cat'}})
except ValidationError as e:
    print(e)
    """
    1 validation error for PetsByName
    __root__ -> __root__
      str type expected (type=type_error.str)
    """
from pydantic import BaseModel, ValidationError


class Pets(BaseModel):
    __root__: list[str]


print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.parse_obj({'__root__': ['dog', 'cat']}))  # not recommended
#> __root__=['dog', 'cat']


class PetsByName(BaseModel):
    __root__: dict[str, str]


print(PetsByName.parse_obj({'Otis': 'dog', 'Milo': 'cat'}))
#> __root__={'Otis': 'dog', 'Milo': 'cat'}
try:
    PetsByName.parse_obj({'__root__': {'Otis': 'dog', 'Milo': 'cat'}})
except ValidationError as e:
    print(e)
    """
    1 validation error for PetsByName
    __root__ -> __root__
      str type expected (type=type_error.str)
    """

(This script is complete, it should run "as is")

Warning

Calling the parse_obj method on a dict with the single key "__root__" for non-mapping custom root types is currently supported for backwards compatibility, but is not recommended and may be dropped in a future version.

If you want to access items in the __root__ field directly or to iterate over the items, you can implement custom __iter__ and __getitem__ functions, as shown in the following example.

from typing import List
from pydantic import BaseModel


class Pets(BaseModel):
    __root__: List[str]

    def __iter__(self):
        return iter(self.__root__)

    def __getitem__(self, item):
        return self.__root__[item]


pets = Pets.parse_obj(['dog', 'cat'])
print(pets[0])
#> dog
print([pet for pet in pets])
#> ['dog', 'cat']
from pydantic import BaseModel


class Pets(BaseModel):
    __root__: list[str]

    def __iter__(self):
        return iter(self.__root__)

    def __getitem__(self, item):
        return self.__root__[item]


pets = Pets.parse_obj(['dog', 'cat'])
print(pets[0])
#> dog
print([pet for pet in pets])
#> ['dog', 'cat']

(This script is complete, it should run "as is")

Faux Immutability

Models can be configured to be immutable via allow_mutation = False. When this is set, attempting to change the values of instance attributes will raise errors. See model config for more details on Config.

Warning

Immutability in Python is never strict. If developers are determined/stupid they can always modify a so-called "immutable" object.

from pydantic import BaseModel


class FooBarModel(BaseModel):
    a: str
    b: dict

    class Config:
        allow_mutation = False


foobar = FooBarModel(a='hello', b={'apple': 'pear'})

try:
    foobar.a = 'different'
except TypeError as e:
    print(e)
    #> "FooBarModel" is immutable and does not support item assignment

print(foobar.a)
#> hello
print(foobar.b)
#> {'apple': 'pear'}
foobar.b['apple'] = 'grape'
print(foobar.b)
#> {'apple': 'grape'}

(This script is complete, it should run "as is")

Trying to change a caused an error, and a remains unchanged. However, the dict b is mutable, and the immutability of foobar doesn't stop b from being changed.

Abstract Base Classes

Pydantic models can be used alongside Python's Abstract Base Classes (ABCs).

import abc
from pydantic import BaseModel


class FooBarModel(BaseModel, abc.ABC):
    a: str
    b: int

    @abc.abstractmethod
    def my_abstract_method(self):
        pass

(This script is complete, it should run "as is")

Field Ordering

Field order is important in models for the following reasons:

As of v1.0 all fields with annotations (whether annotation-only or with a default value) will precede all fields without an annotation. Within their respective groups, fields remain in the order they were defined.

from pydantic import BaseModel, ValidationError


class Model(BaseModel):
    a: int
    b = 2
    c: int = 1
    d = 0
    e: float


print(Model.__fields__.keys())
#> dict_keys(['a', 'c', 'e', 'b', 'd'])
m = Model(e=2, a=1)
print(m.dict())
#> {'a': 1, 'c': 1, 'e': 2.0, 'b': 2, 'd': 0}
try:
    Model(a='x', b='x', c='x', d='x', e='x')
except ValidationError as e:
    error_locations = [e['loc'] for e in e.errors()]

print(error_locations)
#> [('a',), ('c',), ('e',), ('b',), ('d',)]

(This script is complete, it should run "as is")

Warning

As demonstrated by the example above, combining the use of annotated and non-annotated fields in the same model can result in surprising field orderings. (This is due to limitations of Python)

Therefore, we recommend adding type annotations to all fields, even when a default value would determine the type by itself to guarantee field order is preserved.

Required fields

To declare a field as required, you may declare it using just an annotation, or you may use an ellipsis (...) as the value:

from pydantic import BaseModel, Field


class Model(BaseModel):
    a: int
    b: int = ...
    c: int = Field(...)

(This script is complete, it should run "as is")

Where Field refers to the field function.

Here a, b and c are all required. However, use of the ellipses in b will not work well with mypy, and as of v1.0 should be avoided in most cases.

Required Optional fields

Warning

Since version v1.2 annotation only nullable (Optional[...], Union[None, ...] and Any) fields and nullable fields with an ellipsis (...) as the default value, no longer mean the same thing.

In some situations this may cause v1.2 to not be entirely backwards compatible with earlier v1.* releases.

If you want to specify a field that can take a None value while still being required, you can use Optional with ...:

from typing import Optional
from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
    a: Optional[int]
    b: Optional[int] = ...
    c: Optional[int] = Field(...)


print(Model(b=1, c=2))
#> a=None b=1 c=2
try:
    Model(a=1, b=2)
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    c
      field required (type=value_error.missing)
    """
from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
    a: int | None
    b: int | None = ...
    c: int | None = Field(...)


print(Model(b=1, c=2))
#> a=None b=1 c=2
try:
    Model(a=1, b=2)
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    c
      field required (type=value_error.missing)
    """

(This script is complete, it should run "as is")

In this model, a, b, and c can take None as a value. But a is optional, while b and c are required. b and c require a value, even if the value is None.

Field with dynamic default value

When declaring a field with a default value, you may want it to be dynamic (i.e. different for each model). To do this, you may want to use a default_factory.

In Beta

The default_factory argument is in beta, it has been added to pydantic in v1.5 on a provisional basis. It may change significantly in future releases and its signature or behaviour will not be concrete until v2. Feedback from the community while it's still provisional would be extremely useful; either comment on #866 or create a new issue.

Example of usage:

from datetime import datetime
from uuid import UUID, uuid4
from pydantic import BaseModel, Field


class Model(BaseModel):
    uid: UUID = Field(default_factory=uuid4)
    updated: datetime = Field(default_factory=datetime.utcnow)


m1 = Model()
m2 = Model()
print(f'{m1.uid} != {m2.uid}')
#> d65b2741-9363-4886-b8dc-54b53432e64d != a8ba5b6b-b094-44e1-b464-f629b2398f7e
print(f'{m1.updated} != {m2.updated}')
#> 2024-08-22 18:32:11.193618 != 2024-08-22 18:32:11.193631

(This script is complete, it should run "as is")

Where Field refers to the field function.

Warning

The default_factory expects the field type to be set.

Automatically excluded attributes

Class variables which begin with an underscore and attributes annotated with typing.ClassVar will be automatically excluded from the model.

Private model attributes

If you need to vary or manipulate internal attributes on instances of the model, you can declare them using PrivateAttr:

from datetime import datetime
from random import randint

from pydantic import BaseModel, PrivateAttr


class TimeAwareModel(BaseModel):
    _processed_at: datetime = PrivateAttr(default_factory=datetime.now)
    _secret_value: str = PrivateAttr()

    def __init__(self, **data):
        super().__init__(**data)
        # this could also be done with default_factory
        self._secret_value = randint(1, 5)


m = TimeAwareModel()
print(m._processed_at)
#> 2024-08-22 18:32:11.712256
print(m._secret_value)
#> 1

(This script is complete, it should run "as is")

Private attribute names must start with underscore to prevent conflicts with model fields: both _attr and __attr__ are supported.

If Config.underscore_attrs_are_private is True, any non-ClassVar underscore attribute will be treated as private:

from typing import ClassVar

from pydantic import BaseModel


class Model(BaseModel):
    _class_var: ClassVar[str] = 'class var value'
    _private_attr: str = 'private attr value'

    class Config:
        underscore_attrs_are_private = True


print(Model._class_var)
#> class var value
print(Model._private_attr)
#> <member '_private_attr' of 'Model' objects>
print(Model()._private_attr)
#> private attr value

(This script is complete, it should run "as is")

Upon class creation pydantic constructs __slots__ filled with private attributes.

Parsing data into a specified type

Pydantic includes a standalone utility function parse_obj_as that can be used to apply the parsing logic used to populate pydantic models in a more ad-hoc way. This function behaves similarly to BaseModel.parse_obj, but works with arbitrary pydantic-compatible types.

This is especially useful when you want to parse results into a type that is not a direct subclass of BaseModel. For example:

from typing import List

from pydantic import BaseModel, parse_obj_as


class Item(BaseModel):
    id: int
    name: str


# `item_data` could come from an API call, eg., via something like:
# item_data = requests.get('https://my-api.com/items').json()
item_data = [{'id': 1, 'name': 'My Item'}]

items = parse_obj_as(List[Item], item_data)
print(items)
#> [Item(id=1, name='My Item')]
from pydantic import BaseModel, parse_obj_as


class Item(BaseModel):
    id: int
    name: str


# `item_data` could come from an API call, eg., via something like:
# item_data = requests.get('https://my-api.com/items').json()
item_data = [{'id': 1, 'name': 'My Item'}]

items = parse_obj_as(list[Item], item_data)
print(items)
#> [Item(id=1, name='My Item')]

(This script is complete, it should run "as is")

This function is capable of parsing data into any of the types pydantic can handle as fields of a BaseModel.

Pydantic also includes two similar standalone functions called parse_file_as and parse_raw_as, which are analogous to BaseModel.parse_file and BaseModel.parse_raw.

Data Conversion

pydantic may cast input data to force it to conform to model field types, and in some cases this may result in a loss of information. For example:

from pydantic import BaseModel


class Model(BaseModel):
    a: int
    b: float
    c: str


print(Model(a=3.1415, b=' 2.72 ', c=123).dict())
#> {'a': 3, 'b': 2.72, 'c': '123'}

(This script is complete, it should run "as is")

This is a deliberate decision of pydantic, and in general it's the most useful approach. See here for a longer discussion on the subject.

Nevertheless, strict type checking is partially supported.

Model signature

All pydantic models will have their signature generated based on their fields:

import inspect
from pydantic import BaseModel, Field


class FooModel(BaseModel):
    id: int
    name: str = None
    description: str = 'Foo'
    apple: int = Field(..., alias='pear')


print(inspect.signature(FooModel))
#> (*, id: int, name: str = None, description: str = 'Foo', pear: int) -> None

(This script is complete, it should run "as is")

An accurate signature is useful for introspection purposes and libraries like FastAPI or hypothesis.

The generated signature will also respect custom __init__ functions:

import inspect

from pydantic import BaseModel


class MyModel(BaseModel):
    id: int
    info: str = 'Foo'

    def __init__(self, id: int = 1, *, bar: str, **data) -> None:
        """My custom init!"""
        super().__init__(id=id, bar=bar, **data)


print(inspect.signature(MyModel))
#> (id: int = 1, *, bar: str, info: str = 'Foo') -> None

(This script is complete, it should run "as is")

To be included in the signature, a field's alias or name must be a valid Python identifier. pydantic prefers aliases over names, but may use field names if the alias is not a valid Python identifier.

If a field's alias and name are both invalid identifiers, a **data argument will be added. In addition, the **data argument will always be present in the signature if Config.extra is Extra.allow.

Note

Types in the model signature are the same as declared in model annotations, not necessarily all the types that can actually be provided to that field. This may be fixed one day once #1055 is solved.

Structural pattern matching

pydantic supports structural pattern matching for models, as introduced by PEP 636 in Python 3.10.

from pydantic import BaseModel


class Pet(BaseModel):
    name: str
    species: str


a = Pet(name='Bones', species='dog')

match a:
    # match `species` to 'dog', declare and initialize `dog_name`
    case Pet(species='dog', name=dog_name):
        print(f'{dog_name} is a dog')
        #> Bones is a dog
    # default case
    case _:
        print('No dog matched')

(This script is complete, it should run "as is")

Note

A match-case statement may seem as if it creates a new model, but don't be fooled;
it is just syntactic sugar for getting an attribute and either comparing it or declaring and initializing it.