Data Classes & Advanced Class Patterns

Reviewed & published by Brayan K

Master the powerful @dataclass decorator and advanced class patterns that professional engineers use when building large Python systems. Learn to create efficient, immutable, and production-ready data models used in APIs, ML pipelines, and enterprise architectures.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn

This comprehensive lesson teaches you everything professional engineers use when building large Python systems:

🔥 1. Why Dataclasses Exist

Before Python 3.7, writing classes was repetitive:

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"User(name={self.name}, age={self.age})"

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

Dataclasses remove the boilerplate:

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int

This is why dataclasses became standard in production systems.

⚙️ 2. Creating Dataclasses

from dataclasses import dataclass

@dataclass
class Product:
    id: int
    price: float
    name: str

🧠 3. Default Values

@dataclass
class User:
    name: str
    active: bool = True

But never use mutable defaults!

@dataclass
class Bad:
    tags: list[str] = []  # All instances share the same list!
from dataclasses import field

@dataclass
class Good:
    tags: list[str] = field(default_factory=list)

default_factory is critical for safe dataclass design.

🧩 4. Post-Init Processing

Sometimes you need validation or computed attributes.

@dataclass
class User:
    name: str
    age: int

    def __post_init__(self):
        if self.age < 0:
            raise ValueError("Age cannot be negative")

⚡ 5. Making Dataclasses Immutable (Frozen Models)

@dataclass(frozen=True)
class Config:
    host: str
    port: int

Frozen dataclasses behave like lightweight value objects (DDD concept).

🔄 6. Ordering & Comparison

@dataclass(order=True)
class Score:
    points: int
    player: str

📦 7. Dataclasses + Type Hints (Power Combo)

Dataclasses work perfectly with typing tools like MyPy, Pyright, and IDE autocomplete.

@dataclass
class Item:
    id: int
    price: float
    tags: list[str]

Your entire codebase becomes clearer, safer, faster to maintain.

🧬 8. Slots Dataclasses (Big Performance Boost)

Python normally stores instance values in a dictionary (__dict__).

Slots remove the dict and store variables in fixed memory locations.

@dataclass(slots=True)
class User:
    id: int
    name: str

🔥 9. Inheritance With Dataclasses

@dataclass
class Base:
    id: int

@dataclass
class User(Base):
    name: str

🧱 10. Frozen + Slots (Enterprise Pattern)

@dataclass(frozen=True, slots=True)
class Vector2:
    x: float
    y: float

This is used heavily in:

📊 11. Dataclasses vs NamedTuple vs Pydantic

A backend system often uses all three, depending on needs.

🎮 12. Real Project Example — Inventory Item

@dataclass(slots=True)
class InventoryItem:
    id: int
    name: str
    quantity: int = 0
    tags: list[str] = field(default_factory=list)

    def add_stock(self, amount: int):
        self.quantity += amount

This structure is suitable for:

🧪 13. Real Project Example — API Request Model

@dataclass(frozen=True)
class CreateUserRequest:
    email: str
    password: str

This mirrors real FastAPI/Pydantic usage but with pure dataclasses.

🎯 14. Real Project Example — ML Config

@dataclass
class TrainConfig:
    batch_size: int
    lr: float
    epochs: int = 10
    optimizer: str = "adam"

Dataclasses are used massively in ML research tools like:

🔥 15. Field Customization (metadata, repr, compare, init control)

Every field in a dataclass can be finely controlled:

from dataclasses import dataclass, field

@dataclass
class User:
    id: int
    name: str = field(repr=True)
    password: str = field(repr=False)  # hide sensitive info
    cache: dict = field(default_factory=dict, compare=False)

Metadata example (used in FastAPI/Pydantic-style schemas):

@dataclass
class Product:
    id: int = field(metadata={"description": "Unique ID"})
    price: float = field(metadata={"currency": "GBP"})

This allows libraries to generate automatic documentation.

⚙️ 16. Keyword-Only & Positional-Only Fields

Python supports forcing fields to be keyword-only:

@dataclass
class Config:
    host: str
    port: int
    *,
    secure: bool = False

# Usage:
Config("localhost", 8000, secure=True)

# Not allowed:
# Config("localhost", 8000, True)  # ❌ keyword-only violation

🧠 17. Dataclass Factories (Dynamic Dataclass Creation)

You can generate dataclasses at runtime:

from dataclasses import make_dataclass

User = make_dataclass(
    "User",
    [("name", str), ("age", int, field(default=0))]
)

u = User("Bob", 20)

This is an enterprise-level technique.

🔄 18. Inheritance Pitfalls & Solutions

Dataclasses + inheritance can get tricky.

PROBLEM 1: Parent fields come before child fields

@dataclass
class A:
    x: int = 1

@dataclass
class B(A):
    y: int = 2

# Works fine.

PROBLEM 2: Parent has default values but child doesn't

@dataclass
class A:
    x: int = 1

@dataclass
class B(A):
    y: int  # ❌ Error: non-default after default

# FIX: Use keyword-only fields or provide defaults
@dataclass
class B(A):
    y: int = field(default=0)

🧱 19. Mixing Dataclasses With OOP

Dataclasses are not a replacement for OOP — they enhance it.

Example with methods + state:

@dataclass
class BankAccount:
    owner: str
    balance: float = 0.0

    def deposit(self, amount: float):
        self.balance += amount

    def withdraw(self, amount: float):
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        self.balance -= amount

📦 20. Dataclasses + Abstract Base Classes (ABC)

Combine clean models with abstraction:

from dataclasses import dataclass
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        ...

@dataclass
class Circle(Shape):
    radius: float

    def area(self):
        return 3.14 * self.radius**2

This pattern powers plugin systems, physics engines, rendering systems, etc.

🧩 21. Immutable Value Objects (Enterprise Architecture)

In Domain-Driven Design (DDD), models like Money, Weight, Coordinates, Identity, Version should be immutable.

Dataclasses make it easy:

@dataclass(frozen=True, slots=True)
class Money:
    amount: float
    currency: str

Used in finance & high-risk systems.

🧬 22. Dataclasses for Validation-Like Behavior

While not as powerful as Pydantic, you can build lightweight validators:

@dataclass
class User:
    email: str
    age: int

    def __post_init__(self):
        if "@" not in self.email:
            raise ValueError("Invalid email")
        if self.age < 0:
            raise ValueError("Age must be positive")

📚 23. Dataclasses as DTOs (Data Transfer Objects)

DTOs move data between layers:

@dataclass
class UserDTO:
    id: int
    email: str
    premium: bool

Frameworks like Django, Flask, FastAPI use DTO patterns everywhere.

🚀 24. Dataclasses + JSON Serialization

from dataclasses import asdict

@dataclass
class User:
    name: str
    age: int

u = User("Sam", 30)
print(asdict(u))

# Output: {'name': 'Sam', 'age': 30}

🧵 25. Frozen Dataclasses + Hashing

Use frozen to make objects hashable:

@dataclass(frozen=True)
class Point:
    x: int
    y: int

# Now:
s = {Point(1, 2), Point(1, 2)}
# Only one entry exists.

🕹 26. Advanced Pattern — Rich Models With Methods + Validation

Example combining: slots, frozen, methods, computed properties

@dataclass(slots=True, frozen=True)
class Rectangle:
    width: float
    height: float

    @property
    def area(self):
        return self.width * self.height

    def scale(self, factor: float):
        return Rectangle(self.width * factor, self.height * factor)

Immutable models like this are ideal for:

🎮 27. Real Project Example — E-Commerce Order Model

@dataclass
class OrderItem:
    product_id: int
    quantity: int
    price: float

    @property
    def total(self):
        return self.quantity * self.price

🔥 28. Slots + Dataclasses — High-Performance Python

Adding slots=True dramatically reduces memory usage and speeds attribute access.

@dataclass(slots=True)
class Particle:
    x: float
    y: float
    z: float

⚙️ 29. Combining Frozen + Slots (Ultimate Efficiency)

A frozen & slotted dataclass is: immutable, hashable, extremely memory efficient, and extremely fast.

@dataclass(frozen=True, slots=True)
class Vector:
    x: float
    y: float
    z: float

This is production-grade performance tuning.

🧠 30. Overriding post_init in Frozen Dataclasses

Frozen normally blocks all changes — but you can bypass immutability inside __post_init__:

@dataclass(frozen=True)
class User:
    name: str
    email: str

    def __post_init__(self):
        if "@" not in self.email:
            raise ValueError("Invalid email")
        object.__setattr__(self, "email", self.email.lower())

Used by: FastAPI, Pydantic, ORMs, Serializers

🔧 31. Rich Comparison & Ordering

Dataclasses let you customize how objects compare.

Used in: ranking systems, leaderboards, sorting algorithms, priority queues

@dataclass(order=True)
class Product:
    sort_index: float = field(init=False, repr=False)
    price: float
    rating: float

    def __post_init__(self):
        object.__setattr__(self, "sort_index", self.price / self.rating)

Sorts products by value per rating.

📦 32. Converting Between Models (DTO ↔ Entity)

Dataclasses shine when mapping: database rows → Python objects, API requests → models, ML preprocessing → features

@dataclass
class UserEntity:
    id: int
    name: str
    email: str

@dataclass
class UserDTO:
    name: str
    email: str

    def to_entity(self, id: int):
        return UserEntity(id=id, name=self.name, email=self.email)

Used in: backend microservices, data ingestion pipelines, enterprise systems

🧬 33. Nested Dataclasses (Deep Structured Data)

@dataclass
class Address:
    city: str
    postcode: str

@dataclass
class Customer:
    name: str
    address: Address

# asdict() automatically handles nested structures
from dataclasses import asdict
print(asdict(Customer("Bob", Address("London", "SW1"))))

# Output: {'name': 'Bob', 'address': {'city': 'London', 'postcode': 'SW1'}}

Perfect for JSON APIs.

🧵 34. Dataclasses + Thread Safety

Dataclasses are not thread-safe by default. To create safe models:

from threading import Lock
from dataclasses import dataclass, field

@dataclass
class Counter:
    value: int = 0
    lock: Lock = field(default_factory=Lock, repr=False)

    def increment(self):
        with self.lock:
            self.value += 1

Used in: async job systems, game engine ticks, analytics counters, concurrent caches

🧩 35. Advanced Pattern — Config Objects (Immutable + Validated)

Real systems use typed configuration objects.

@dataclass(frozen=True)
class AppConfig:
    env: str
    debug: bool
    db_url: str

    def __post_init__(self):
        if self.env not in {"dev", "prod"}:
            raise ValueError("Invalid environment")

Used in: FastAPI projects, internal developer tools, cloud services

⚡ 36. Dataclasses + Caching Layers

Make models hashable → usable as cache keys.

from functools import lru_cache

@dataclass(frozen=True)
class Query:
    user_id: int
    limit: int

@lru_cache(maxsize=1000)
def get_user_feed(query: Query):
    ...

Used in: feed ranking systems, recommendation engines, caching APIs

🧠 37. Dataclasses in Clean Architecture (DDD)

Domain-Driven Design heavily uses dataclasses for: Value objects, Entities, Aggregates, DTOs, Commands, Events

@dataclass(frozen=True)
class UserRegistered:
    user_id: int
    email: str
    timestamp: float

Used in: Kafka event streams, cloud-native apps, CQRS systems

🔥 38. Dataclasses as Event Objects (Message Buses)

Perfect for internal event buses:

@dataclass(frozen=True)
class OrderPlaced:
    order_id: int
    user_id: int
    total: float

Event handlers consume these structured dataclass messages.

🧱 39. Serialization Hooks (post_init + getstate)

@dataclass
class Session:
    user: str
    token: str

    def __getstate__(self):
        return {"user": self.user}

Used in: caching, distributed systems, multiprocessing

🚀 40. Combining Dataclasses With Polymorphism

@dataclass
class Vehicle:
    speed: int

@dataclass
class Car(Vehicle):
    seats: int

@dataclass
class Truck(Vehicle):
    capacity: int

Useful for: game engines, simulation systems, logistics modeling

🎮 41. Dataclasses in Game Development

@dataclass(slots=True)
class NPC:
    name: str
    hp: int
    position: tuple

Extremely efficient for large worlds (like Minecraft entities).

🧊 42. Dataclasses for Tensor Metadata (ML Workflows)

@dataclass
class TensorMeta:
    shape: tuple
    dtype: str
    source: str
# 🎯 YOUR TURN — replace each ___ using the hint beside it.

from dataclasses import dataclass, field

# 1) The decorator that writes __init__, __repr__ and __eq__ for you.
@___                                    # 👉 replace ___ with dataclass
class Book:
    title: str
    author: str
    pages: int = 0
    # 2) A mutable default MUST come from a factory, or every Book would
    #    share one list. Python refuses to let you write [] here.
    tags: list[str] = ___(default_factory=list)   # 👉 replace ___ with field

    def is_long(self) -> bool:
        return self.pages > 300

a = Book("Dune", "Herbert", 412)
b = Book("Dune", "Herbert", 412)

# 3) The generated __repr__ prints every field by name.
print(___)                              # 👉 replace ___ with a

# 4) Two different objects, compared field by field.
print("Equal?", a ___ b)                # 👉 replace ___ with ==
print("Long?", a.is_long())

# Thanks to default_factory, a's tags are its own.
a.tags.append("scifi")
print("Tags on a:", a.tags, "tags on b:", b.tags)

# 5) frozen=True makes instances read-only, and hashable.
@dataclass(___=True)                    # 👉 replace ___ with frozen
class Point:
    x: int
    y: int

p = Point(1, 2)
print("Frozen point:", p)
try:
    p.x = 9
except Exception as e:
    print("Cannot change it:", type(e).__name__)

# ✅ Expected output:
# Book(title='Dune', author='Herbert', pages=412, tags=[])
# Equal? True
# Long? True
# Tags on a: ['scifi'] tags on b: []
# Frozen point: Point(x=1, y=2)
# Cannot change it: FrozenInstanceError

Used in: ML pipelines, dataset loaders, feature engineering

📌 43. Best Practices Summary (Elite Level)

🎉 Conclusion — You Now Write Enterprise-Grade Python Models

You're building at professional software engineer level.

📋 Quick Reference — Data Classes

SyntaxWhat it does
@dataclassAuto-generate __init__, __repr__, __eq__
@dataclass(frozen=True)Make class immutable (hashable)
field(default_factory=list)Mutable default values safely
dataclasses.asdict(obj)Convert dataclass to dict
@dataclass(order=True)Auto-generate comparison methods

🎉 Great work! You've completed this lesson.

You can now use @dataclass to build clean data containers with auto-generated methods, validation, and serialisation.

Practice quiz

What does the @dataclass decorator auto-generate?

  • Only __init__
  • Database tables
  • __init__, __repr__, and __eq__
  • Type checks at runtime

Answer: __init__, __repr__, and __eq__. @dataclass removes boilerplate by auto-generating __init__, __repr__, and __eq__.

Why must you avoid a mutable default like tags: list = []?

  • All instances would share the same list
  • It is a syntax error
  • Lists cannot be defaults
  • It makes the class frozen

Answer: All instances would share the same list. A bare mutable default is shared across all instances; use field(default_factory=list) instead.

What is the correct way to give a dataclass field a safe mutable default?

  • tags: list = []
  • tags: list = list
  • tags: list = None
  • tags: list = field(default_factory=list)

Answer: tags: list = field(default_factory=list). field(default_factory=list) creates a fresh list for each instance.

Which method runs validation or computed attributes right after a dataclass is created?

  • __init__
  • __post_init__
  • __new__
  • __setup__

Answer: __post_init__. __post_init__ runs after the auto-generated __init__ for validation or computed fields.

What does @dataclass(frozen=True) give you?

  • Immutable, hashable instances usable as dict keys
  • Faster attribute access only
  • Automatic slots
  • Mutable fields

Answer: Immutable, hashable instances usable as dict keys. Frozen dataclasses are immutable and hashable, so they can be dict keys or set elements.

What does @dataclass(order=True) add?

  • A frozen flag
  • JSON serialization
  • Comparison methods <, <=, >, >=
  • Slots

Answer: Comparison methods <, <=, >, >=. order=True auto-generates the ordering comparison methods.

What is the main benefit of @dataclass(slots=True)?

  • Adds validation
  • Lower memory use and faster attribute access
  • Makes the class frozen
  • Enables inheritance

Answer: Lower memory use and faster attribute access. slots removes the per-instance __dict__, reducing memory and speeding attribute access.

What does dataclasses.asdict(obj) return for a dataclass?

  • A JSON string
  • A tuple
  • A copy of the object
  • A dict of its fields (recursively for nested dataclasses)

Answer: A dict of its fields (recursively for nested dataclasses). asdict() converts the dataclass (and any nested dataclasses) into a plain dict.

Inside a frozen dataclass's __post_init__, how can you still normalize a field?

  • self.email = value
  • object.__setattr__(self, 'email', value)
  • frozen=False
  • You cannot at all

Answer: object.__setattr__(self, 'email', value). object.__setattr__ bypasses the frozen restriction during initialization only.

Two instances User('Sam', 30) and User('Sam', 30) of a basic @dataclass compare as...

  • Not equal
  • An error
  • Equal
  • Equal only with frozen=True

Answer: Equal. The auto-generated __eq__ compares by field values, so they are equal.

Continue this course