Pydantic
Introduction | |
Installing Pydantic | |
Example | |
BaseModel | |
Show Error Message as JSON | |
Check Range of Allowed Values | |
@validator | |
@root_validator | |
Final Code of this Demo | |
Related Articles |
Intro
Pydantic is using modern Python features. Please ensure that you have at least Python 3.7 installed.
It is recommended to install latest stable Python version. Consider visiting
«Install Python on Linux»
manul for guidance.
However if you plan to use Open API or Swagger consider studying current compatibility between libraries.
Example
Create a Python script and PydanticDemo.py
from dataclasses import dataclass
from typing import Tuple
from enum import Enum
@dataclass
class IceCreamMix:
name: str
flavor: str
toppings: Tuple[str, ...]
scoops: int
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
"peanut butter",
("strawberries", "sprinkles"),
2
)
print(ice_cream_mix)
if __name__ == '__main__':
main()
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor='peanut butter', toppings=('strawberries', 'sprinkles'), scoops=2)
This script shows type of icecream
Lets add some more OOP
from dataclasses import dataclass
from typing import Tuple
from enum import Enum
class Flavor(str, Enum):
chocolate = 'chocolate'
vanilla = 'vanilla'
strawberry = 'strawberry'
mint = 'mint'
coffeee = 'coffee'
peanut_butter = 'peanut butter'
class Topping(str, Enum):
sprinkles = 'sprinkles'
hot_fudge = 'hot fudge'
cookies = 'cookies'
brownie = 'brownie'
whipped_cream = 'whipped cream'
strawberries = 'strawberries'
@dataclass
class IceCreamMix:
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
Flavor.peanut_butter,
(Topping.strawberries, Topping.sprinkles),
2
)
print(ice_cream_mix)
if __name__ == '__main__':
main()
$ python PydanticDemo.py
IceCreamMix(name='PB&J', flavor=<Flavor.peanut_butter: 'peanut butter'>, toppings=(<Topping.strawberries: 'strawberries'>, <Topping.sprinkles: 'sprinkles'>), scoops=2)
Script is still working find
What if we use unexisting flavor or topping
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
"smells bad",
(Topping.strawberries, 111),
2
)
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor='smells bad', toppings=(<Topping.strawberries: 'strawberries'>, 111), scoops=2)
Script does not understand that the value is wrong.
To check values automatically install pydantic and make a change in the first line
from pydantic.dataclasses import dataclass
python PydanticDemo.py
Traceback (most recent call last): File "PydanticDemo.py", line 41, in <module> main() File "PydanticDemo.py", line 31, in main ice_cream_mix = IceCreamMix( File "<string>", line 7, in __init__ File "C:\Users\Andrei\python\pydantic\venv\lib\site-packages\pydantic\dataclasses.py", line 99, in _pydantic_post_init raise validation_error pydantic.error_wrappers.ValidationError: 2 validation errors for IceCreamMix flavor value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter' (type=type_error.enum; enum_values=[<Flavor.chocolate: 'chocolate'>, <Flavor.vanilla: 'vanilla'>, <Flavor.strawberry: 'strawberry'>, <Flavor.mint: 'mint'>, <Flavor.coffeee: 'coffee'>, <Flavor.peanut_butter: 'peanut butter'>]) toppings -> 1 value is not a valid enumeration member; permitted: 'sprinkles', 'hot fudge', 'cookies', 'brownie', 'whipped cream', 'strawberries' (type=type_error.enum; enum_values=[<Topping.sprinkles: 'sprinkles'>, <Topping.hot_fudge: 'hot fudge'>, <Topping.cookies: 'cookies'>, <Topping.brownie: 'brownie'>, <Topping.whipped_cream: 'whipped cream'>, <Topping.strawberries: 'strawberries'>])
pydantic did not allow our code to run. Lets study the output with more detail
pydantic.error_wrappers.ValidationError: 2 validation errors for IceCreamMix
The quantity of erros is shown as well as the class where it happened
It would help us with the debug if we did know where the errors are
flavor value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter' (type=type_error.enum; enum_values=[<Flavor.chocolate: 'chocolate'>, <Flavor.vanilla: 'vanilla'>, <Flavor.strawberry: 'strawberry'>, <Flavor.mint: 'mint'>, <Flavor.coffeee: 'coffee'>, <Flavor.peanut_butter: 'peanut butter'>])
The quantity of erros is shown as well as the class where it happened
It would help us with the debug if we did know where the errors are
Use allowed values for Flavor and Topping but change scoops value from 2 to '2' so now it is a string not integer
def main():
ice_cream_mix = IceCreamMix(
"PB&J",
Flavor.peanut_butter,
(Topping.strawberries, Topping.sprinkles),
'2'
)
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor=<Flavor.peanut_butter: 'peanut butter'>, toppings=(<Topping.strawberries: 'strawberries'>, <Topping.sprinkles: 'sprinkles'>), scoops=2)
scoops is still equal to 2 because Pydantic suppurts type coercion
BaseModel
To get access to extra features such as Serialization and JSON support use BaseModel class
Just a reminder that originally we had
from dataclasses import dataclass
Then
from pydantic.dataclasses import dataclass
Now we need
from pydantic import BaseModel
Please remove @dataclass decorator before class IceCreamMix:
class IceCreamMix: needs to be changed to class IceCreamMix(BaseModel):
and attributes names should be added to the code that creates an object of class IceCreamMix
e.g: name = "PB&J" flavor = Flavor.peanut_butter etc.
strawberries = 'strawberries'
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int
def main():
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = Flavor.peanut_butter,
toppings = (Topping.strawberries, Topping.sprinkles),
scoops = 2
)
python PydanticDemo.py
IceCreamMix(name='PB&J', flavor=<Flavor.peanut_butter: 'peanut butter'>, toppings=(<Topping.strawberries: 'strawberries'>, <Topping.sprinkles: 'sprinkles'>), scoops=2)
Everything works in a same way as it used to.
Now we can have output in JSON format
print(ice_cream_mix.json())
python PydanticDemo.py
{"name": "PB&J", "flavor": "peanut butter", "toppings": ["strawberries", "brownie"], "scoops": 2}
Notice JSON that we have in the output
Now it is possible to copy it, change if needed and create more objects
fro JSON with
parse_raw() method
Example:
another_mix = IceCreamMix.parse_raw('{"name": "New mix", "flavor": "mint", "toppings": ["cookies", "hot fudge"], "scoops": 2}')
print(another_mix.json())
{"name": "New mix", "flavor": "mint", "toppings": ["cookies", "hot fudge"], "scoops": 2}
If we make an accident mistake in the attribute value - pydantic will help us to notice it.
another_mix = IceCreamMix.parse_raw('{"name": "New mix", "flavor": "novichoke", "toppings": ["cookies", "hot fudge"], "scoops": 2}')
print(another_mix.json())
python PydanticDemo.py
Traceback (most recent call last): File "/home/andrei/python/pydantic/PydanticDemo.py", line 45, in <module> main() File "/home/andrei/python/pydantic/PydanticDemo.py", line 40, in main another_mix = IceCreamMix.parse_raw('{"name": "New mix", "flavor": "novichoke", "toppings": ["cookies", "hot fudge"], "scoops": 2}') File "pydantic/main.py", line 543, in pydantic.main.BaseModel.parse_raw File "pydantic/main.py", line 520, in pydantic.main.BaseModel.parse_obj File "pydantic/main.py", line 362, in pydantic.main.BaseModel.__init__ pydantic.error_wrappers.ValidationError: 1 validation error for IceCreamMix flavor value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter' (type=type_error.enum; enum_values=[<Flavor.chocolate: 'chocolate'>, <Flavor.vanilla: 'vanilla'>, <Flavor.strawberry: 'strawberry'>, <Flavor.mint: 'mint'>, <Flavor.coffeee: 'coffee'>, <Flavor.peanut_butter: 'peanut butter'>])
ValidationError как JSON
Error messages are also can be formatted as JSON. Need to import ValidationError from pydantic and use try: except
from pydantic import BaseModel, ValidationError
…
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = "spring",
toppings = (Topping.strawberries, Topping.sprinkles),
scoops = 2
)
except ValidationError as e:
print(e.json())
python PydanticDemo.py
[ { "loc": [ "flavor" ], "msg": "value is not a valid enumeration member; permitted: 'chocolate', 'vanilla', 'strawberry', 'mint', 'coffee', 'peanut butter'", "type": "type_error.enum", "ctx": { "enum_values": [ "chocolate", "vanilla", "strawberry", "mint", "coffee", "peanut butter" ] } } ]
Allowed values range
Lets say you want to limit scoopes quantity to be in range 0 to 5 (excluding borders) and make in mandatory
from pydantic import BaseModel, ValidationError, Field
…
strawberries = 'strawberries'
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int = Field(..., gt=0, lt=5)
python PydanticDemo.py
{"name": "PB&J", "flavor": "peanut butter", "toppings": ["strawberries", "brownie"], "scoops": 2}
2 scoops are set, so no error here
Try 5 scoops
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = "spring",
toppings = (Topping.strawberries, Topping.sprinkles),
scoops = 5
)
except ValidationError as e:
print(e.json())
python PydanticDemo.py
[ { "loc": [ "scoops" ], "msg": "ensure this value is less than 5", "type": "value_error.number.not_lt", "ctx": { "limit_value": 5 } } ] Traceback (most recent call last): File "/home/andrei/python/pydantic/PydanticDemo.py", line 50, in <module> main() File "/home/andrei/python/pydantic/PydanticDemo.py", line 41, in main print(ice_cream_mix.json()) UnboundLocalError: local variable 'ice_cream_mix' referenced before assignment
Do not pay attention to Traceback - it is because of validation failure → object is not created. Will not happen if scoops value is coorect
Validation with validator decorator
One more usefull method is using @validator
It is designed to monitor a signle attribute.
from pydantic import BaseModel, ValidationError, Field, validator
…
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int = Field(..., gt=0, lt=5)
@validator('toppings')
def check_toppings(cls, toppings):
if len(toppings) > 4:
raise ValueError('Too many toppings')
return toppings
If we run this code with two toppings there are no errors. Lets add three more to have five.
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
flavor = "spring",
toppings = (Topping.strawberries, Topping.brownie,Topping.sprinkles,Topping.hot_fudge,Topping.whipped_cream),
python PydanticDemo.py
[ { "loc": [ "toppings" ], "msg": "Too many toppings", "type": "value_error" } ]
Now we can reduce to 4 and check that there are no errors.
@root_validator
Is used when whole model should be monitored. For example to monitor multiple attributes combinations.
from pydantic import BaseModel, ValidationError, Field, validator, root_validator
…
Create one more class - Container
strawberries = 'strawberries'
class Container(str, Enum):
cup = 'cup'
cone = 'cone'
waffle_cone = 'waffle cone'
class IceCreamMix(BaseModel):
name: str
flavor: Flavor
We will create a new condition: if topping is hot_fudge cone and waffle_cone are not allowed - only cup is allowed
Will validate with @root_validator
@validator('toppings')
def check_toppings(cls, toppings):
if len(toppings) > 4:
raise ValueError('Too many toppings')
return toppings
@root_validator
def check_cone_toppings(cls, toppings):
container = values.get('container')
toppings = values.get('toppings')
if container == Container.cone or container == Container.waffle_cone:
if Topping.hot_fudge in toppings:
raise ValueError('Cones cannot have hot fudge')
return values
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
container = Container.waffle_cone,
flavor = "spring",
We should have hot fudge from the previous example, if not - add it to the code
python PydanticDemo.py
[ { "loc": [ "__root__" ], "msg": "Cones cannot have hot fudge", "type": "value_error" } ]
It was the short review of pydantic library.
Thank you for your attention, the full code of this example is below.
from pydantic import BaseModel, ValidationError, Field, validator, root_validator
from typing import Tuple
from enum import Enum
class Flavor(str, Enum):
chocolate = 'chocolate'
vanilla = 'vanilla'
strawberry = 'strawberry'
mint = 'mint'
coffeee = 'coffee'
peanut_butter = 'peanut butter'
class Topping(str, Enum):
sprinkles = 'sprinkles'
hot_fudge = 'hot fudge'
cookies = 'cookies'
brownie = 'brownie'
whipped_cream = 'whipped cream'
strawberries = 'strawberries'
class Container(str, Enum):
cup = 'cup'
cone = 'cone'
waffle_cone = 'waffle cone'
class IceCreamMix(BaseModel):
name: str
container: Container
flavor: Flavor
toppings: Tuple[Topping, ...]
scoops: int = Field(..., gt=0, lt=5)
@validator('toppings')
def check_toppings(cls, toppings):
if len(toppings) > 4:
raise ValueError('Too many toppings')
return toppings
@root_validator
def check_cone_toppings(cls, values):
container = values.get('container')
toppings = values.get('toppings')
if container == Container.cone or container == Container.waffle_cone:
if Topping.hot_fudge in toppings:
raise ValueError('Cones cannot have hot fudge')
return values
def main():
try:
ice_cream_mix = IceCreamMix(
name = "PB&J",
container = Container.waffle_cone,
flavor = Flavor.peanut_butter,
# flavor = 'unknown flavour'
toppings = (Topping.strawberries, Topping.brownie,Topping.sprinkles),
# на validator
# toppings = (Topping.strawberries, Topping.brownie,Topping.sprinkles,Topping.cookies, Topping.sprinkles),
# на root_validator
# toppings = (Topping.strawberries, Topping.brownie,Topping.sprinkles,Topping.hot_fudge),
scoops = 2
# scoops = 5
)
print(ice_cream_mix.json())
except ValidationError as e:
print(e.json())
if __name__ == '__main__':
main()
Pydantic models | |
Python | |
enumerate | |
Pydantic models |