Pydantic, Enums and IntEnums. A Story of Validation
Taming Data Validation: Mastering Pydantic, Enums and IntEnums for Robust Python Applications.
What is a Enum and its integration with Pydantic?
Oh, this is great question! Never heard that one before. An Enum, for the uninitiated, is a nifty little feature that united with the Pydantic library that helps you control the chaos of the data jungle. It’s a combination of Python’s Enum (short for enumeration) and Pydantic’s validation powers. Enums let you define a set of named values that your data must adhere to. Pydantic then checks if your data is part of this exclusive club, and if it isn’t, kindly shows it the door.
How does Pydantic and Enum help me?
If you’ve ever dealt with data, you know that it has a mind of its own. Sometimes, it’s pristine and perfect. Other times, it’s a dumpster fire. Enums are here to save you from those days when your data decides to go rogue.
By defining enums and using them in your Pydantic models, you create a set of rules that your data must follow, like a stern yet fair teacher. This way, you can ensure that only valid data enters your system, and any outliers are dealt with accordingly.
from enum import Enum
from pydantic import BaseModel, ValidationError
class Pet(BaseModel):
name: str
animal_type: str
sex: str
You can be more specific defining your Pydantic models by using Enums:
from pydantic import ValidationError
class Sex(Enum):
MALE = 'male'
FEMALE = 'female'
class DomesticAnimals(Enum):
CAT = 'cat'
DOG = 'dog'
FISH = 'fish'
BIRD = 'bird'
# And then
class Pet(BaseModel):
name: str
animal_type: DomesticAnimals
sex: Sex
Pet(name='Timmy', animal_type='bird', sex='male')
## Pet(name='Timmy', animal_type=<DomesticAnimals.BIRD: 'bird'>, sex=<Sex.MALE: 'male'>)
Adding a non existent value (Tiger not in allowed in domestic animals) to an existent Enum will raise a value is not a valid enumeration member;
error. This is one of the main use cases for them:
import pytest
with pytest.raises(ValidationError, match=' value is not a valid enumeration member') as e_info:
Pet(
name='Timmy',
animal_type='tiger',
sex='male'
)
print(e_info.value)
## 1 validation error for Pet
## animal_type
## value is not a valid enumeration member; permitted: 'cat', 'dog', 'fish', 'bird' (type=type_error.enum; enum_values=[<DomesticAnimals.CAT: 'cat'>, <DomesticAnimals.DOG: 'dog'>, <DomesticAnimals.FISH: 'fish'>, <DomesticAnimals.BIRD: 'bird'>])
(*Note the UPPER_CASE_NOTATION
)
What about IntENum? Which is the difference between Enum and IntEnum?
In a nutshell, the main difference between Enum and IntEnum is the type of values they represent. Enum is a generic enumeration class for any data type, while IntEnum is specifically designed for integer values and allows for direct comparison with integers.
Two are the main benefits that bring using IntEnums in the correct use cases: - As IntEnum ensures that all the enumeration members have an integer value, it’s also possible to order them. - IntEnum members can be compared to integers directly, while Enum members can’t be used in integer comparison operators.
from enum import IntEnum
import pytest
class ResponseCode(IntEnum):
OK = 200
NOT_FOUND = 404
ERROR = 500
assert ResponseCode.OK == 200
assert ResponseCode.OK <= ResponseCode.NOT_FOUND
with pytest.raises(TypeError, match='cannot extend enumeration') as e_info: # Check that a TypeError is raised
class ExtendedResponseCode(ResponseCode):
CUSTOM = 300
print(e_info.value)
## ExtendedResponseCode: cannot extend enumeration 'ResponseCode'
Is it possible to subclass an Enum (or an StrEnum / IntEnum)?
It is not supposed to be possible. If you do so by direct inheritance a TypeError may be raised. To explain that, the documentation says: “Allowing subclassing of enums that define members would lead to a violation of some important invariants of types and instances.”
Which are those violations? Let’s check the Guido comment in (2013):
from enum import Enum
class Color(Enum):
red = 1
green = 2
blue = 3
class MoreColor(Color): # this is not possible as we've seen
cyan = 4
magenta = 5
yellow = 6
type(MoreColor.red) is Color
type(MoreColor.red) is not MoreColor
#In other words, while `red` is accessible in MoreColor, it's actually a
#Color instance?
#Oh dear, this is actually a mess. I don't want MoreColor.red and
#Color.red to be distinct objects, but then the isinstance() checks
#will become confusing. If we don't override isinstance(), we'll get
#not isinstance(Color.red, MoreColor)
#isinstance(MoreColor.yellow, Color)
In some Python versions, this is working without an error message, but it is a unwanted behaviour.
class Color(Enum):
red = 1
green = 2
blue = 3
class MoreColor(Enum, Color):
cyan = 4
magenta = 5
yellow = 6
One could argue that enumerations exist to guarantee mutual exclusion over a finite not ordered set. Appending additional members onto an existing enumeration don’t violates this guarantee. So being sure about your use case and what you’re doing, it’s possible to create a workaround. A clean solution using a decorator is:
from enum import Enum
from typing import Any, Callable
class EnumBase(Enum):
def __eq__(self, other: Any) -> bool:
if isinstance(other, Enum):
return self.value == other.value
return False
def extend_enum(parent_enum: EnumBase) -> Callable[[EnumBase], EnumBase]:
"""Decorator function that extends an enum class with values from another enum class."""
def wrapper(extended_enum: EnumBase) -> EnumBase:
joined = {}
for item in parent_enum:
joined[item.name] = item.value
for item in extended_enum:
joined[item.name] = item.value
return EnumBase(extended_enum.__name__, joined)
return wrapper
class Parent(EnumBase):
A = 1
B = 2
@extend_enum(Parent)
class ExtendedParent(EnumBase):
C = 3
print(
type(Parent.A) is Parent,
type(Parent.A) is not ExtendedParent,
Parent.A == ExtendedParent.A
)
## True True True
But this is not a perfect solution, as it has some drawbacks or limitations you should be aware of. In this case, a nor related Enum (called RandomEnum
) that implements the same enum value, is equal in the comparison to our Parent
and ExtendedParent
classes :
## True
Stay updated on Pydantic and Python tips
Hopefully, this post has helped you become familiar with Enum usage in Pydantic and allowed you to enjoy a showcase of some of its functionalities.
If you want to stay updated…