Comparing Python's Dataclass, Attrs, and Pydantic for Class Definition
Written on
Understanding Python Class Definitions
This article delves into the differences among attrs, dataclass, and Pydantic when defining classes. It evaluates their performance concerning type violations, handling of positional arguments, and the ability to add new attributes (immutability). Generally speaking, attrs occupies a middle ground between dataclass and Pydantic.
Target Audience
This content is geared towards all Python developers.
Outline
- Pydantic raises errors for type violations.
- Pydantic does not accept positional arguments.
- Dataclass is mutable by default.
- A comparison of class definitions using dataclass, Pydantic, and attrs.
In a previous article, "Python Class vs. Data Class vs. Pydantic: Write to CSV with Data Validation," I focused solely on comparing Pydantic with dataclass. Tom Brown mentioned attrs, which I wasn't very familiar with at the time. After reviewing its documentation, I realized the need to include attrs in this comparison.
Pydantic and Type Violations
Let’s attempt to define a class using dataclass, Pydantic, and attrs. For this example, I will create a function to calculate an employee's severance package based on their duration of service and performance rating. (Note: This is a simplified example and should not be taken too seriously.)
Here is the code that uses dataclass to define our function:
from dataclasses import dataclass from datetime import datetime
@dataclass class EmployeeDataclass:
name: str
email: str
joining_date: datetime
passed_probation: bool
monthly_salary: float
performance_rating: int
def calculate_months_of_service(self) -> int:
import math
"""Calculate the number of months since joining the company."""
months_service = math.floor((datetime.today() - self.joining_date).days / 30)
return months_service
def calculate_severance_package(self) -> float:
"""Calculate the severance package based on months of service and performance rating.
Note: This is a simplified calculation for demonstration purposes.
"""
months_service = self.calculate_months_of_service()
if self.passed_probation:
if months_service < 12:
severance_package = self.monthly_salary * months_service / 12else:
severance_package = (
self.monthly_salary * months_service / 12
- self.monthly_salary * self.performance_rating
)
else:
severance_package = 0return severance_package
Let's test it using correct type values first, then deliberately introduce a type mismatch.
Dataclass Handling of Types
The passed_probation attribute should be a boolean. Will Python raise an error? Surprisingly, it will not. Dataclass does not enforce strict type checks.
Using Attrs to Define a Class
Now, let’s see how attrs handles the same situation:
from attrs import define from datetime import datetime
@define class EmployeeAttrs:
name: str
email: str
joining_date: datetime
passed_probation: bool
monthly_salary: float
performance_rating: float
def calculate_months_of_service(self) -> int:
import math
"""Calculate the number of months since joining the company."""
months_service = math.floor((datetime.today() - self.joining_date).days / 30)
return months_service
def calculate_severance_package(self) -> float:
"""Calculate the severance package based on months of service and performance rating.
Note: This is a simplified calculation for demonstration purposes.
"""
months_service = self.calculate_months_of_service()
if self.passed_probation:
if months_service < 12:
severance_package = self.monthly_salary * months_service / 12else:
severance_package = (
self.monthly_salary * months_service / 12
- self.monthly_salary * self.performance_rating
)
else:
severance_package = 0return severance_package
Similar to dataclass, attrs also allows for type violations.
Pydantic's Strict Type Enforcement
Next, let’s define a class using Pydantic:
from pydantic import BaseModel from datetime import datetime
class EmployeePydantic(BaseModel):
name: str
email: str
joining_date: datetime
passed_probation: bool
monthly_salary: float
performance_rating: float
def calculate_months_of_service(self) -> int:
import math
"""Calculate the number of months since joining the company."""
months_service = math.floor((datetime.today() - self.joining_date).days / 30)
return months_service
def calculate_severance_package(self) -> float:
"""Calculate the severance package based on months of service and performance rating.
Note: This is a simplified calculation for demonstration purposes.
"""
months_service = self.calculate_months_of_service()
if self.passed_probation:
if months_service < 12:
severance_package = self.monthly_salary * months_service / 12else:
severance_package = (
self.monthly_salary * months_service / 12
- self.monthly_salary * self.performance_rating
)
else:
severance_package = 0return severance_package
In this case, Pydantic will raise an error if a type mismatch occurs, as it treats type hints as constraints rather than mere suggestions.
Only Pydantic Enforces Keyword Arguments
I seldom use positional arguments, but it’s noteworthy how each of these classes responds to them.
Using Positional Arguments with Dataclass
Using positional arguments with dataclass works seamlessly. Dataclass accepts positional arguments without issues.
To enforce keyword-only arguments in dataclass, you can specify kw_only=True in the decorator.
Using Positional Arguments with Attrs
Similarly, attrs accepts positional arguments by default. However, if you want to restrict it to keyword arguments, there is no straightforward method—each attribute must be marked as kw_only=True.
Pydantic's Restriction on Positional Arguments
Pydantic, on the other hand, only accepts keyword arguments by default. It does not allow for changing this behavior.
Immutability in Class Definitions
Mutability of Dataclass
Dataclass is mutable by default. You can add new attributes even after its definition:
@dataclass class EmployeeDataclass:
# existing attributes...
# Adding a new attribute employee = EmployeeDataclass(...) employee.remarks = "Good performance"
To make a dataclass immutable, set frozen=True in the decorator.
Immutability of Attrs
In contrast, attrs is immutable by default. Attempting to add a new attribute after defining an attrs class will result in an error.
Pydantic's Immutability
Pydantic behaves similarly to attrs, enforcing immutability by default. Adding a new attribute after class definition will also raise an error.
Conclusion
This article does not cover data validation decorators; I am currently working on that aspect. For those interested in the code presented above, please follow the provided links.
Follow me on LinkedIn | 👏🏽 for my story | Follow me on Medium
The first video, "Attrs, Pydantic, or Python Data Classes?" provides insights into the differences among these three approaches.
The second video, "Which Python @dataclass is best? Feat. Pydantic, NamedTuple, attrs..." explores which option might be the most suitable for your needs.