Attrs vs Pydantic vs Dataclasses (Image credit)

Attrs vs Pydantic vs Dataclasses: Which to Use?

Discover the pros and cons of Attrs, Pydantic, and Dataclasses. Get practical insights and example codes for your projects.

Bhavik Jikadara
4 min readDec 4, 2024

--

When working with Python, developers often need efficient and clean ways to manage structured data. Three popular tools for this are Attrs, Pydantic, and Dataclasses. Each offers unique strengths tailored to different needs, and choosing the right one depends on your specific requirements for validation, performance, and boilerplate reduction. In this blog, we’ll compare their features and provide code examples to highlight when to use each.

1. Attrs

Attrs is a powerful library that provides fine-grained control over class creation with built-in validation. It is particularly suited for projects where you need explicit type and value checks, but without extensive schema validation.

Features

  • Flexible and declarative attribute validation using validators.
  • Customizable equality and hashing logic.
  • Lightweight and highly performant.
  • Does not enforce JSON-like serialization.
  • Explicit and Pythonic API.

Code Example: Attrs

from attrs import define, field, validators
from datetime import date
from enum import StrEnum, auto

# Enum for Order Status
class OrderStatus(StrEnum):
OPEN = auto()
CLOSED = auto()

# Validators
def positive_number(instance, attribute, value):
if value <= 0:
raise ValueError(f"{attribute.name} must be greater than zero.")
def percentage_value(instance, attribute, value):
if not 0 <= value <= 1:
raise ValueError(f"{attribute.name} must be between 0 and 1.")

# Product Class
@define
class Product:
name: str = field(eq=str.lower)
category: str = field(eq=str.lower)
shipping_weight: float = field(validator=[validators.instance_of(float), positive_number])
unit_price: int = field(validator=[validators.instance_of(int), positive_number])
tax_percent: float = field(validator=[validators.instance_of(float), percentage_value])

# Order Class
@define(kw_only=True)
class Order:
status: OrderStatus
creation_date: date = date.today()
products: list[Product] = field(factory=list)
def add_product(self, product: Product):
self.products.append(product)
@property
def sub_total(self) -> int:
return sum(p.unit_price for p in self.products)
@property
def tax(self) -> float:
return sum(p.unit_price * p.tax_percent for p in self.products)
@property
def total_price(self) -> float:
return self.sub_total + self.tax

# Example Usage
banana = Product(name="banana", category="fruit", shipping_weight=0.5, unit_price=215, tax_percent=0.07)
mango = Product(name="mango", category="fruit", shipping_weight=2.0, unit_price=319, tax_percent=0.11)
order = Order(status=OrderStatus.OPEN)
order.add_product(banana)
order.add_product(mango)
print(f"Total Price: ${order.total_price / 100:.2f}")

2. Pydantic

Pydantic is ideal for cases where robust data validation and manipulation are required. It’s heavily used in APIs and web frameworks due to its JSON serialization support and automatic type coercion.

Features

  • Strong focus on validation.
  • Automatic parsing and coercion of input data.
  • JSON serialization/deserialization.
  • Integrates seamlessly with FastAPI.
  • Detailed and helpful error messages.

Code Example: Pydantic

from pydantic import BaseModel, Field, PositiveInt, PositiveFloat
from datetime import date
from enum import StrEnum, auto

# Enum for Order Status
class OrderStatus(StrEnum):
OPEN = auto()
CLOSED = auto()

# Product Model
class Product(BaseModel):
name: str
category: str
shipping_weight: PositiveFloat
unit_price: PositiveInt
tax_percent: float = Field(ge=0, le=1)

# Order Model
class Order(BaseModel):
status: OrderStatus
creation_date: date = date.today()
products: list[Product] = Field(default_factory=list)
def add_product(self, product: Product):
self.products.append(product)
@property
def sub_total(self) -> int:
return sum(p.unit_price for p in self.products)
@property
def tax(self) -> float:
return sum(p.unit_price * p.tax_percent for p in self.products)
@property
def total_price(self) -> float:
return self.sub_total + self.tax

# Example Usage
banana = Product(name="banana", category="fruit", shipping_weight=0.5, unit_price=215, tax_percent=0.07)
mango = Product(name="mango", category="fruit", shipping_weight=2.0, unit_price=319, tax_percent=0.11)
order = Order(status=OrderStatus.OPEN)
order.add_product(banana)
order.add_product(mango)
print(f"Total Price: ${order.total_price / 100:.2f}")

3. Dataclasses

Dataclasses provide a lightweight and minimalistic way to define data structures in Python. They are part of the standard library, making them accessible without external dependencies.

Features

  • Simple and clean syntax for creating immutable or mutable data structures.
  • Less overhead and no runtime validation.
  • Ideal for small projects where performance and simplicity are key.
  • Extensible with __post_init__ for basic validation.

Code Example: Dataclasses

from dataclasses import dataclass, field
from datetime import date
from enum import StrEnum, auto

# Enum for Order Status
class OrderStatus(StrEnum):
OPEN = auto()
CLOSED = auto()
@dataclass
class Product:
name: str
category: str
shipping_weight: float
unit_price: int
tax_percent: float
def __post_init__(self):
if self.unit_price < 0 or self.shipping_weight < 0:
raise ValueError("unit_price and shipping_weight must be positive.")
if not (0 <= self.tax_percent <= 1):
raise ValueError("tax_percent must be between 0 and 1.")
@dataclass
class Order:
status: OrderStatus
creation_date: date = date.today()
products: list[Product] = field(default_factory=list)
def add_product(self, product: Product):
self.products.append(product)
@property
def sub_total(self) -> int:
return sum(p.unit_price for p in self.products)
@property
def tax(self) -> float:
return sum(p.unit_price * p.tax_percent for p in self.products)
@property
def total_price(self) -> float:
return self.sub_total + self.tax
# Example Usage
banana = Product(name="banana", category="fruit", shipping_weight=0.5, unit_price=215, tax_percent=0.07)
mango = Product(name="mango", category="fruit", shipping_weight=2.0, unit_price=319, tax_percent=0.11)
order = Order(status=OrderStatus.OPEN)
order.add_product(banana)
order.add_product(mango)
print(f"Total Price: ${order.total_price / 100:.2f}")

Conclusion

  • Use Attrs: When you want flexibility, performance, and custom validation but don’t need JSON serialization.
  • Use Pydantic: For robust data validation and manipulation, especially for APIs or user input.
  • Use Dataclasses: For lightweight, boilerplate-free data models in small or internal projects.

Each tool has its strengths, and understanding these will help you choose the right one for your next project!

--

--

Bhavik Jikadara
Bhavik Jikadara

Written by Bhavik Jikadara

🚀 AI/ML & MLOps expert 🌟 Crafting advanced solutions to speed up data retrieval 📊 and enhance ML model lifecycles. buymeacoffee.com/bhavikjikadara

No responses yet