KAHIBARO
Discord Login Register

5.7 Classes and Dataclasses

Why Classes Matter in Backend Python

In backend development you quickly reach a point where simple variables and functions are not enough. You need a way to group related data and behavior together. That is exactly what classes are for.

You will not learn every object oriented programming concept here. Other chapters cover OOP in more detail. In this chapter we focus on how to use classes and especially Python dataclasses in typical backend code.


Recap: What Is a Class?

A class is a blueprint for creating objects. An object is an instance of a class with its own data.

In backend code you will often model things like:

Each of these can be a class.

A Simple Class Example

python
class User:
    def __init__(self, id: int, email: str, is_active: bool = True):
        self.id = id
        self.email = email
        self.is_active = is_active
    def deactivate(self) -> None:
        self.is_active = False
    def __repr__(self) -> str:
        return f"User(id={self.id}, email={self.email!r}, is_active={self.is_active})"
user = User(id=1, email="alice@example.com")
print(user)              # User(id=1, email='alice@example.com', is_active=True)
user.deactivate()
print(user.is_active)    # False

Key ideas:

For small data holder classes you end up writing repetitive __init__, __repr__, __eq__, and more. Dataclasses help with that.


What Is a Dataclass?

A dataclass is a Python decorator that automatically adds common methods to classes that mainly store data.

You import and use it like this:

python
from dataclasses import dataclass
@dataclass
class User:
    id: int
    email: str
    is_active: bool = True

The @dataclass decorator automatically generates:

This makes your backend code shorter and easier to maintain.

Rule: Use @dataclass when a class mainly holds data and has simple behavior.
Use a regular class when the class has complex initialization, heavy logic, or you need full control over methods.


Creating Your First Dataclass

Basic Dataclass Example

python
from dataclasses import dataclass
@dataclass
class Product:
    id: int
    name: str
    price: float
    in_stock: bool = True
p = Product(id=10, name="Keyboard", price=49.99)
print(p)          # Product(id=10, name='Keyboard', price=49.99, in_stock=True)
print(p.name)     # Keyboard
p.in_stock = False

With @dataclass you did not need to write __init__ or __repr__.

With Type Hints (Required)

Dataclasses require type hints for each field to know what is a field:

python
@dataclass
class User:
    id: int
    email: str
    is_active: bool

If you forget the type, it will not be treated as a dataclass field.


Default Values and Field Order

Dataclasses behave like functions in Python with respect to default values.

Rule for Default Values

In a dataclass, all fields without defaults must come before fields with defaults.

python
from dataclasses import dataclass
# Correct
@dataclass
class Order:
    id: int                # no default
    total_amount: float    # no default
    status: str = "new"    # default value

If you put status before total_amount, you will get an error.

python
@dataclass
class BrokenOrder:
    status: str = "new"
    total_amount: float   # ❌ This will raise: non-default argument follows default argument

Rule: In a dataclass definition, non default fields must come before default fields.
Example of invalid order:

Using `field` for More Control

Sometimes you want a default value that is not a simple constant, or you want to exclude a field from comparisons or representation. For this you use dataclasses.field.

python
from dataclasses import dataclass, field
@dataclass
class Order:
    id: int
    total_amount: float
    status: str = "new"
    internal_notes: str = field(default="", repr=False, compare=False)

Here:

This is useful in backend code for things like internal flags, secrets, or technical metadata.


Mutable Default Values, `field(default_factory=...)`

In backend development you often have lists or dicts as fields. For example, an order may have a list of items.

You must be very careful with mutable default values.

The Wrong Way

python
from dataclasses import dataclass
@dataclass
class Order:
    id: int
    items: list = []  # ❌ Dangerous mutable default

This list is created once. All Order instances share the same list.

python
o1 = Order(id=1)
o2 = Order(id=2)
o1.items.append("apple")
print(o1.items)  # ['apple']
print(o2.items)  # ['apple']  ← same list reused, very bad

The Correct Way: `default_factory`

Use field(default_factory=...) to create a new list or dict each time.

python
from dataclasses import dataclass, field
from typing import List
@dataclass
class Order:
    id: int
    items: List[str] = field(default_factory=list)
o1 = Order(id=1)
o2 = Order(id=2)
o1.items.append("apple")
print(o1.items)  # ['apple']
print(o2.items)  # []

Now each order has its own list.

Other common factories:

python
from typing import Dict, Any
@dataclass
class RequestContext:
    headers: Dict[str, str] = field(default_factory=dict)
    attrs: Dict[str, Any] = field(default_factory=dict)

Rule: Never use a mutable object (like [], {}, set()) as a direct default value in a dataclass field.
Always use field(default_factory=...) so each instance gets its own object.


Auto-generated Methods

@dataclass can automatically create several methods. You can control which ones you want.

`__init__`

Created by default, uses type hints and defaults.

python
@dataclass
class User:
    id: int
    email: str
    is_admin: bool = False

This is similar to writing:

python
class User:
    def __init__(self, id: int, email: str, is_admin: bool = False):
        self.id = id
        self.email = email
        self.is_admin = is_admin

If you need custom initialization, you can implement your own __post_init__ or override __init__ manually.

`__repr__`

Useful for debugging backend code.

python
user = User(1, "alice@example.com")
print(user)  # User(id=1, email='alice@example.com', is_admin=False)

If you do not want a field in the representation, use repr=False as shown earlier.

`__eq__` (Equality)

By default dataclasses compare objects by field values.

python
@dataclass
class User:
    id: int
    email: str
u1 = User(1, "alice@example.com")
u2 = User(1, "alice@example.com")
print(u1 == u2)  # True

This is useful for testing and business logic.

If you do not want comparison, set eq=False:

python
@dataclass(eq=False)
class User:
    id: int
    email: str

Order Comparison (`order=True`)

If your objects have ordering, for example by id, you can set order=True.

python
from dataclasses import dataclass
@dataclass(order=True)
class Task:
    priority: int
    description: str

Now you can sort tasks:

python
tasks = [
    Task(3, "Low priority"),
    Task(1, "High priority"),
    Task(2, "Medium priority"),
]
for t in sorted(tasks):
    print(t.priority)
# 1, 2, 3

`__post_init__`: Post Processing After Initialization

When you want @dataclass to still generate __init__ but you also need custom logic afterwards, use __post_init__.

The method __post_init__ runs automatically right after the dataclass generated __init__.

Example: Basic Validation

python
from dataclasses import dataclass
@dataclass
class User:
    id: int
    email: str
    def __post_init__(self):
        if "@" not in self.email:
            raise ValueError("Invalid email address")

Now:

python
User(1, "alice@example.com")  # OK
User(2, "invalid-email")      # Raises ValueError

Example: Derived Fields

python
from dataclasses import dataclass, field
@dataclass
class Product:
    name: str
    price: float
    price_with_tax: float = field(init=False)
    def __post_init__(self):
        self.price_with_tax = self.price * 1.20

Here:

Dataclasses and Immutability (`frozen=True`)

Sometimes you want objects that cannot be changed after creation, similar to tuple. This can help avoid bugs in concurrent or complex backend systems.

You can use frozen=True for that.

python
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
    database_url: str
    redis_url: str
    debug: bool = False

Now any attempt to modify a field will raise an error:

python
cfg = Config(database_url="postgres://...", redis_url="redis://...")
cfg.debug = True  # ❌ dataclasses.FrozenInstanceError

Caveat: Nested Mutables

Even if a dataclass is frozen, mutable fields inside it can still change.

python
from dataclasses import dataclass, field
from typing import Dict
@dataclass(frozen=True)
class Settings:
    feature_flags: Dict[str, bool] = field(default_factory=dict)
s = Settings()
s.feature_flags["new_ui"] = True  # This still works, inner dict is mutable

To make it truly immutable, you must use immutable inner types, for example tuples or types.MappingProxyType.


Converting Dataclasses to Dicts and Back

In backend development you often need to:

`asdict` and `astuple`

Use dataclasses.asdict for a deep conversion to dict.

python
from dataclasses import dataclass, asdict
from typing import List
@dataclass
class Item:
    name: str
    price: float
@dataclass
class Order:
    id: int
    items: List[Item]
order = Order(
    id=1,
    items=[Item("apple", 1.0), Item("banana", 2.0)],
)
print(asdict(order))
# {'id': 1, 'items': [{'name': 'apple', 'price': 1.0}, {'name': 'banana', 'price': 2.0}]}

For tuples:

python
from dataclasses import astuple
print(astuple(order))
# (1, (('apple', 1.0), ('banana', 2.0)))

This is useful when you want to log structured data or send it as JSON (after passing it to json.dumps or a web framework).


Examples From Backend Use Cases

Example 1: Simple DTO (Data Transfer Object)

In backend APIs you often create small classes that represent request or response data. Dataclasses are perfect for this when you do not use heavier tools like Pydantic.

python
from dataclasses import dataclass
from datetime import datetime
@dataclass
class CreateUserRequest:
    email: str
    password: str
@dataclass
class UserResponse:
    id: int
    email: str
    created_at: datetime

You can use these around your service layer to keep data structured and typed.

Example 2: Domain Model Object

python
from dataclasses import dataclass, field
from datetime import datetime
from typing import List
@dataclass
class OrderItem:
    product_id: int
    quantity: int
    unit_price: float
    @property
    def total_price(self) -> float:
        return self.quantity * self.unit_price
@dataclass
class Order:
    id: int
    user_id: int
    items: List[OrderItem] = field(default_factory=list)
    created_at: datetime = field(default_factory=datetime.utcnow)
    is_paid: bool = False
    @property
    def total_amount(self) -> float:
        return sum(item.total_price for item in self.items)
    def add_item(self, product_id: int, quantity: int, unit_price: float) -> None:
        self.items.append(OrderItem(product_id, quantity, unit_price))
    def mark_paid(self) -> None:
        self.is_paid = True

Here the dataclasses are not just dumb containers. They also provide convenient methods and properties for your business logic.

Example 3: Configuration Object

python
from dataclasses import dataclass
import os
@dataclass
class AppConfig:
    database_url: str
    redis_url: str
    debug: bool
    @classmethod
    def from_env(cls) -> "AppConfig":
        return cls(
            database_url=os.environ["DATABASE_URL"],
            redis_url=os.environ.get("REDIS_URL", "redis://localhost:6379/0"),
            debug=os.environ.get("DEBUG", "false").lower() == "true",
        )
config = AppConfig.from_env()

This pattern is common in backend apps, where configuration comes from environment variables.


When to Use Dataclasses vs Regular Classes

Both are valid in backend projects. Choose based on your needs.

SituationUse
Mostly data, few methodsDataclass
Need automatic __init__, __repr__, __eq__Dataclass
You want immutabilityFrozen dataclass
Complex inheritance hierarchiesRegular classes (often)
Framework requires normal classesRegular classes
Heavy custom initialization logicRegular class or dataclass with __post_init__

Often you start with a dataclass, and if it grows too complex, you convert it into a normal class.


Common Pitfalls and How to Avoid Them

1. Mutable Defaults

Already covered, but it is so important it is worth repeating.

Use:

python
field(default_factory=list)
field(default_factory=dict)

Not:

python
items: list = []
options: dict = {}

2. Wrong Field Order

Do not put fields with defaults before fields without defaults. Keep this order:

python
@dataclass
class Example:
    required1: int
    required2: str
    optional1: str = "x"
    optional2: int = 0

3. Forgetting Type Hints

Fields without type hints do not become dataclass fields.

python
@dataclass
class User:
    id: int
    email = "?"  # Not a field, just a class attribute

If you want email as a field, do:

python
@dataclass
class User:
    id: int
    email: str = "?"

4. Mixing Dataclasses and ORM Models

Many ORMs, like SQLAlchemy, have their own base classes and patterns. Directly combining @dataclass and ORM models can be tricky. Often the better pattern is:

Summary

Dataclasses fit naturally into backend development, especially for request and response objects, configuration, and small domain models. In later chapters, when you work with FastAPI and Pydantic, you will see similar ideas applied specifically to web APIs.

Views: 10

Comments

Please login to add a comment.

Don't have an account? Register now!