Pydantic Field Types
Where possible pydantic uses standard library types to define fields, thus smoothing the learning curve. For many useful applications, however, no standard library type exists, so pydantic implements many commonly used types.
If no existing type suits your purpose you can also implement your own pydantic-compatible types with custom properties and validation.
Standard Library Types⚑
pydantic supports many common types from the python standard library. If you need stricter processing see Strict Types; if you need to constrain the values allowed (e.g. to require a positive int) see Constrained Types.
bool
: see Booleans for details on how bools are validated and what values are permitted.
int
: pydantic uses int(v)
to coerce types to an int
; see this warning on loss of information during data conversion.
float
: similarly, float(v)
is used to coerce values to floats.
str
: strings are accepted as-is, int
float
and Decimal
are coerced using str(v)
, bytes
and bytearray
are converted using v.decode()
, enums inheriting from str
are converted using v.value
, and all other types cause an error.
list
: allows list
, tuple
, set
, frozenset
, or generators and casts to a list.
tuple
: allows list
, tuple
, set
, frozenset
, or generators and casts to a tuple.
dict
: dict(v)
is used to attempt to convert a dictionary.
set
: allows list
, tuple
, set
, frozenset
, or generators and casts to a set.
frozenset
: allows list
, tuple
, set
, frozenset
, or generators and casts to a frozen set.
datetime.date
: see Datetime Types below for more detail on parsing and validation.
datetime.time
: see Datetime Types below for more detail on parsing and validation.
datetime.datetime
: see Datetime Types below for more detail on parsing and validation.
datetime.timedelta
: see Datetime Types below for more detail on parsing and validation.
typing.Any
: allows any value include None
, thus an Any
field is optional.
typing.TypeVar
: constrains the values allowed based on constraints
or bound
, see TypeVar.
typing.Union
: see Unions below for more detail on parsing and validation.
typing.Optional
: Optional[x]
is simply short hand for Union[x, None]
; see Unions below for more detail on parsing and validation.
typing.List
:
typing.Tuple
:
typing.Dict
:
typing.Set
:
typing.FrozenSet
:
typing.Sequence
:
typing.Iterable
: this is reserved for iterables that shouldn't be consumed. See Infinite Generators below for more detail on parsing and validation.
typing.Type
: see Type below for more detail on parsing and validation.
typing.Callable
: see Callable for more detail on parsing and validation.
typing.Pattern
: will cause the input value to be passed to re.compile(v)
to create a regex pattern.
ipaddress.IPv4Address
: simply uses the type itself for validation by passing the value to IPv4Address(v)
.
ipaddress.IPv4Interface
: simply uses the type itself for validation by passing the value to IPv4Address(v)
.
ipaddress.IPv4Network
: simply uses the type itself for validation by passing the value to IPv4Network(v)
.
enum.Enum
: checks that the value is a valid member of the enum; see Enums and Choices for more details.
enum.IntEnum
: checks that the value is a valid member of the integer enum; see Enums and Choices for more details.
decimal.Decimal
: pydantic attempts to convert the value to a string, then passes the string to Decimal(v)
.
pathlib.Path
: simply uses the type itself for validation by passing the value to Path(v)
.
Iterables⚑
Define default value for an iterable⚑
If you want to define an empty list, dictionary, set or other iterable as a model attribute, you can use the default_factory
.
from typing import Sequence
from pydantic import BaseModel, Field
class Foo(BaseModel):
defaulted_list_field: Sequence[str] = Field(default_factory=list)
It might be tempting to do
class Foo(BaseModel):
defaulted_list_field: Sequence[str] = [] # Bad!
But you'll follow the mutable default argument anti-pattern.
Unions⚑
The Union
type allows a model attribute to accept different types, e.g.:
from uuid import UUID
from typing import Union
from pydantic import BaseModel
class User(BaseModel):
id: Union[int, str, UUID]
name: str
user_01 = User(id=123, name="John Doe")
print(user_01)
# > id=123 name='John Doe'
print(user_01.id)
# > 123
user_02 = User(id="1234", name="John Doe")
print(user_02)
# > id=1234 name='John Doe'
print(user_02.id)
# > 1234
user_03_uuid = UUID("cf57432e-809e-4353-adbd-9d5c0d733868")
user_03 = User(id=user_03_uuid, name="John Doe")
print(user_03)
# > id=275603287559914445491632874575877060712 name='John Doe'
print(user_03.id)
# > 275603287559914445491632874575877060712
print(user_03_uuid.int)
# > 275603287559914445491632874575877060712
However, as can be seen above, pydantic will attempt to 'match' any of the types defined under Union
and will use the first one that matches. In the above example the id
of user_03
was defined as a uuid.UUID
class (which is defined under the attribute's Union
annotation) but as the uuid.UUID
can be marshalled into an int
it chose to match against the int
type and disregarded the other types.
As such, it is recommended that, when defining Union
annotations, the most specific type is included first and followed by less specific types. In the above example, the UUID
class should precede the int
and str
classes to preclude the unexpected representation as such:
from uuid import UUID
from typing import Union
from pydantic import BaseModel
class User(BaseModel):
id: Union[UUID, int, str]
name: str
user_03_uuid = UUID("cf57432e-809e-4353-adbd-9d5c0d733868")
user_03 = User(id=user_03_uuid, name="John Doe")
print(user_03)
# > id=UUID('cf57432e-809e-4353-adbd-9d5c0d733868') name='John Doe'
print(user_03.id)
# > cf57432e-809e-4353-adbd-9d5c0d733868
print(user_03_uuid.int)
# > 275603287559914445491632874575877060712
Enums and Choices⚑
pydantic uses python's standard enum
classes to define choices.
from enum import Enum, IntEnum
from pydantic import BaseModel, ValidationError
class FruitEnum(str, Enum):
pear = "pear"
banana = "banana"
class ToolEnum(IntEnum):
spanner = 1
wrench = 2
class CookingModel(BaseModel):
fruit: FruitEnum = FruitEnum.pear
tool: ToolEnum = ToolEnum.spanner
print(CookingModel())
# > fruit=<FruitEnum.pear: 'pear'> tool=<ToolEnum.spanner: 1>
print(CookingModel(tool=2, fruit="banana"))
# > fruit=<FruitEnum.banana: 'banana'> tool=<ToolEnum.wrench: 2>
try:
CookingModel(fruit="other")
except ValidationError as e:
print(e)
"""
1 validation error for CookingModel
fruit
value is not a valid enumeration member; permitted: 'pear', 'banana'
(type=type_error.enum; enum_values=[<FruitEnum.pear: 'pear'>,
<FruitEnum.banana: 'banana'>])
"""
Datetime Types⚑
Pydantic supports the following datetime types:
-
datetime
fields can be: -
datetime
, existingdatetime
object -
int
orfloat
, assumed as Unix time, i.e. seconds (if >=-2e10
or \<=2e10
) or milliseconds (if \<-2e10
or >2e10
) since 1 January 1970 -
str
, following formats work:YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z[±]HH[:]MM]]]
int
orfloat
as a string (assumed as Unix time)
-
date
fields can be: -
date
, existingdate
object -
int
orfloat
, seedatetime
-
str
, following formats work:YYYY-MM-DD
int
orfloat
, seedatetime
-
time
fields can be: -
time
, existingtime
object -
str
, following formats work:HH:MM[:SS[.ffffff]]
-
timedelta
fields can be: -
timedelta
, existingtimedelta
object -
int
orfloat
, assumed as seconds -
str
, following formats work:[-][DD ][HH:MM]SS[.ffffff]
[±]P[DD]DT[HH]H[MM]M[SS]S
(ISO 8601 format for timedelta)
Type⚑
pydantic supports the use of Type[T]
to specify that a field may only accept classes (not instances) that are subclasses of T
.
from typing import Type
from pydantic import BaseModel
from pydantic import ValidationError
class Foo:
pass
class Bar(Foo):
pass
class Other:
pass
class SimpleModel(BaseModel):
just_subclasses: Type[Foo]
SimpleModel(just_subclasses=Foo)
SimpleModel(just_subclasses=Bar)
try:
SimpleModel(just_subclasses=Other)
except ValidationError as e:
print(e)
"""
1 validation error for SimpleModel
just_subclasses
subclass of Foo expected (type=type_error.subclass; expected_class=Foo)
"""
TypeVar⚑
TypeVar
is supported either unconstrained, constrained or with a bound.
from typing import TypeVar
from pydantic import BaseModel
Foobar = TypeVar("Foobar")
BoundFloat = TypeVar("BoundFloat", bound=float)
IntStr = TypeVar("IntStr", int, str)
class Model(BaseModel):
a: Foobar # equivalent of ": Any"
b: BoundFloat # equivalent of ": float"
c: IntStr # equivalent of ": Union[int, str]"
print(Model(a=[1], b=4.2, c="x"))
# > a=[1] b=4.2 c='x'
# a may be None and is therefore optional
print(Model(b=1, c=1))
# > a=None b=1.0 c=1
Pydantic Types⚑
pydantic also provides a variety of other useful types:
EmailStr
:
FilePath
: like Path
, but the path must exist and be a file.
DirectoryPath
: like Path
, but the path must exist and be a directory.
Color
: for parsing HTML and CSS colors; see Color Type.
Json
: a special type wrapper which loads JSON before parsing; see JSON Type.
AnyUrl
: any URL; see URLs.
AnyHttpUrl
: an HTTP URL; see URLs.
HttpUrl
: a stricter HTTP URL; see URLs.
PostgresDsn
: a postgres DSN style URL; see URLs.
RedisDsn
: a redis DSN style URL; see URLs.
SecretStr
: string where the value is kept partially secret; see Secrets.
IPvAnyAddress
: allows either an IPv4Address
or an IPv6Address
.
IPvAnyInterface
: allows either an IPv4Interface
or an IPv6Interface
.
IPvAnyNetwork
: allows either an IPv4Network
or an IPv6Network
.
NegativeFloat
: allows a float which is negative; uses standard float
parsing then checks the value is less than 0; see Constrained Types.
NegativeInt
: allows an int which is negative; uses standard int
parsing then checks the value is less than 0; see Constrained Types.
PositiveFloat
: allows a float which is positive; uses standard float
parsing then checks the value is greater than 0; see Constrained Types.
PositiveInt
: allows an int which is positive; uses standard int
parsing then checks the value is greater than 0; see Constrained Types.
condecimal
: type method for constraining Decimals; see Constrained Types.
confloat
: type method for constraining floats; see Constrained Types.
conint
: type method for constraining ints; see Constrained Types.
conlist
: type method for constraining lists; see Constrained Types.
conset
: type method for constraining sets; see Constrained Types.
constr
: type method for constraining strs; see Constrained Types.
Custom Data Types⚑
You can also define your own custom data types. There are several ways to achieve it.
Classes with __get_validators__
⚑
You use a custom class with a classmethod __get_validators__
. It will be called to get validators to parse and validate the input data.
Tip
Validators, you can declare a parameter config
, field
, etc.
import re
from pydantic import BaseModel
# https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation
post_code_regex = re.compile(
r"(?:"
r"([A-Z]{1,2}[0-9][A-Z0-9]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?"
r"([0-9][A-Z]{2})|"
r"(BFPO) ?([0-9]{1,4})|"
r"(KY[0-9]|MSR|VG|AI)[ -]?[0-9]{4}|"
r"([A-Z]{2}) ?([0-9]{2})|"
r"(GE) ?(CX)|"
r"(GIR) ?(0A{2})|"
r"(SAN) ?(TA1)"
r")"
)
class PostCode(str):
"""
Partial UK postcode validation. Note: this is just an example, and is not
intended for use in production; in particular this does NOT guarantee
a postcode exists, just that it has a valid format.
"""
@classmethod
def __get_validators__(cls):
# one or more validators may be yielded which will be called in the
# order to validate the input, each validator will receive as an input
# the value returned from the previous validator
yield cls.validate
@classmethod
def __modify_schema__(cls, field_schema):
# __modify_schema__ should mutate the dict it receives in place,
# the returned value will be ignored
field_schema.update(
# simplified regex here for brevity, see the wikipedia link above
pattern="^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$",
# some example postcodes
examples=["SP11 9DG", "w1j7bu"],
)
@classmethod
def validate(cls, v):
if not isinstance(v, str):
raise TypeError("string required")
m = post_code_regex.fullmatch(v.upper())
if not m:
raise ValueError("invalid postcode format")
# you could also return a string here which would mean model.post_code
# would be a string, pydantic won't care but you could end up with some
# confusion since the value's type won't match the type annotation
# exactly
return cls(f"{m.group(1)} {m.group(2)}")
def __repr__(self):
return f"PostCode({super().__repr__()})"
class Model(BaseModel):
post_code: PostCode
model = Model(post_code="sw8 5el")
print(model)
# > post_code=PostCode('SW8 5EL')
print(model.post_code)
# > SW8 5EL
print(Model.schema())
"""
{
'title': 'Model',
'type': 'object',
'properties': {
'post_code': {
'title': 'Post Code',
'pattern': '^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$',
'examples': ['SP11 9DG', 'w1j7bu'],
'type': 'string',
},
},
'required': ['post_code'],
}
"""
Generic Classes as Types⚑
Warning
beginning. In most of the cases you will probably be fine with standard pydantic models.
You can use Generic Classes as field types and perform custom validation based on the "type parameters" (or sub-types) with __get_validators__
.
If the Generic class that you are using as a sub-type has a classmethod __get_validators__
you don't need to use arbitrary_types_allowed
for it to work.
Because you can declare validators that receive the current field
, you can extract the sub_fields
(from the generic class type parameters) and validate data with them.
from pydantic import BaseModel, ValidationError
from pydantic.fields import ModelField
from typing import TypeVar, Generic
AgedType = TypeVar("AgedType")
QualityType = TypeVar("QualityType")
# This is not a pydantic model, it's an arbitrary generic class
class TastingModel(Generic[AgedType, QualityType]):
def __init__(self, name: str, aged: AgedType, quality: QualityType):
self.name = name
self.aged = aged
self.quality = quality
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
# You don't need to add the "ModelField", but it will help your
# editor give you completion and catch errors
def validate(cls, v, field: ModelField):
if not isinstance(v, cls):
# The value is not even a TastingModel
raise TypeError("Invalid value")
if not field.sub_fields:
# Generic parameters were not provided so we don't try to validate
# them and just return the value as is
return v
aged_f = field.sub_fields[0]
quality_f = field.sub_fields[1]
errors = []
# Here we don't need the validated value, but we want the errors
valid_value, error = aged_f.validate(v.aged, {}, loc="aged")
if error:
errors.append(error)
# Here we don't need the validated value, but we want the errors
valid_value, error = quality_f.validate(v.quality, {}, loc="quality")
if error:
errors.append(error)
if errors:
raise ValidationError(errors, cls)
# Validation passed without errors, return the same instance received
return v
class Model(BaseModel):
# for wine, "aged" is an int with years, "quality" is a float
wine: TastingModel[int, float]
# for cheese, "aged" is a bool, "quality" is a str
cheese: TastingModel[bool, str]
# for thing, "aged" is a Any, "quality" is Any
thing: TastingModel
model = Model(
# This wine was aged for 20 years and has a quality of 85.6
wine=TastingModel(name="Cabernet Sauvignon", aged=20, quality=85.6),
# This cheese is aged (is mature) and has "Good" quality
cheese=TastingModel(name="Gouda", aged=True, quality="Good"),
# This Python thing has aged "Not much" and has a quality "Awesome"
thing=TastingModel(name="Python", aged="Not much", quality="Awesome"),
)
print(model)
"""
wine=<types_generics.TastingModel object at 0x7f3593a4eee0>
cheese=<types_generics.TastingModel object at 0x7f3593a46100>
thing=<types_generics.TastingModel object at 0x7f3593a464c0>
"""
print(model.wine.aged)
# > 20
print(model.wine.quality)
# > 85.6
print(model.cheese.aged)
# > True
print(model.cheese.quality)
# > Good
print(model.thing.aged)
# > Not much
try:
# If the values of the sub-types are invalid, we get an error
Model(
# For wine, aged should be an int with the years, and quality a float
wine=TastingModel(name="Merlot", aged=True, quality="Kinda good"),
# For cheese, aged should be a bool, and quality a str
cheese=TastingModel(name="Gouda", aged="yeah", quality=5),
# For thing, no type parameters are declared, and we skipped validation
# in those cases in the Assessment.validate() function
thing=TastingModel(name="Python", aged="Not much", quality="Awesome"),
)
except ValidationError as e:
print(e)
"""
2 validation errors for Model
wine -> quality
value is not a valid float (type=type_error.float)
cheese -> aged
value could not be parsed to a boolean (type=type_error.bool)
"""
Using constrained strings in list attributes⚑
If you try to use:
from pydantic import constr
Regexp = constr(regex="^i-.*")
class Data(pydantic.BaseModel):
regex: List[Regex]
You'll encounter the Variable "Regexp" is not valid as a type [valid-type]
mypy error.
There are a few ways to achieve this:
Using typing.Annotated
with pydantic.Field
⚑
Instead of using constr
to specify the regex
constraint, you can specify it as an argument to Field
and then use it in combination with typing.Annotated
:
!!! warning "Until this open issue is not solved, this won't work."
!!! note "typing.Annotated
is only available since Python 3.9. For older Python versions typing_extensions.Annotated
can be used."
import pydantic
from pydantic import Field
from typing import Annotated
Regex = Annotated[str, Field(regex="^[0-9a-z_]*$")]
class DataNotList(pydantic.BaseModel):
regex: Regex
data = DataNotList(**{"regex": "abc"})
print(data)
# regex='abc'
print(data.json())
# {"regex": "abc"}
Mypy treats Annotated[str, Field(regex="^[0-9a-z_]*$")]
as a type alias of str
. But it also tells pydantic to do validation. This is described in the pydantic docs.
Unfortunately it does not currently work with the following:
class Data(pydantic.BaseModel):
regex: List[Regex]
regex: Optional[Regex]
Inheriting from pydantic.ConstrainedStr⚑
Instead of using constr
to specify the regex constraint (which uses pydantic.ConstrainedStr
internally), you can inherit from pydantic.ConstrainedStr
directly:
import re
import pydantic
from pydantic import Field
from typing import List
class Regex(pydantic.ConstrainedStr):
regex = re.compile("^[0-9a-z_]*$")
class Data(pydantic.BaseModel):
regex: List[Regex]
data = Data(**{"regex": ["abc", "123", "asdf"]})
print(data)
# regex=['abc', '123', 'asdf']
print(data.json())
# {"regex": ["abc", "123", "asdf"]}
Mypy accepts this happily and pydantic does correct validation. The type of data.regex[i]
is Regex
, but as pydantic.ConstrainedStr
itself inherits from str
, it can be used as a string in most places.