Sheet ⁨05⁩ · ⁨Code snippets⁩Surveyed ⁨2026⁩

Blog post image for Python Dataclass Patterns: Slots, Frozen Instances, and Field Validation - A Python snippet exploring advanced dataclass patterns for production code. Covers `__slots__` with dataclasses, frozen immutable instances, `__post_init__` validation, field factories, and comparison with Pydantic for data modeling.

Python Dataclass Patterns: Slots, Frozen Instances, and Field Validation

Published: Updated: 03 Mins read04 Mins listen
Markdown for AI(opens in a new tab)

Quick Tip

Reach for @dataclass(slots=True, frozen=True) with a __post_init__ check and you get a small, immutable, validated value object in about five lines.

The Problem

The problem with plain dicts

Passing structured data around as plain dicts feels quick at first, but it rots fast. There is no autocomplete, no type checking, typos in keys fail silently at runtime, and nothing stops one part of the code from mutating a dict another part is still using. You end up writing defensive data.get("x") calls everywhere and guessing at the shape.

Real-world impact

At scale this shows up as two concrete costs. Memory: millions of small objects each carrying a per-instance __dict__ add up quickly. Correctness: a mutable default (the classic def __init__(self, items=[])) silently shares one list across every instance, and immutable-looking data gets mutated in place. Both are the kind of bug that only bites in production.

The Solution

The fix

Dataclasses give you a real typed object with generated __init__, __repr__, and __eq__ for free. Two flags make them production-ready: slots=True (Python 3.10+) drops the per-instance dict for less memory and faster access, and frozen=True makes instances immutable and hashable. Validation and derived fields go in __post_init__, and mutable defaults use field(default_factory=...).

TL;DR

  • slots=True for memory and speed; frozen=True for safe, hashable value objects.
  • Validate and compute derived fields in __post_init__.
  • Never use a mutable default directly; use field(default_factory=list).

The difference slots=True makes to each instance:

A regular instance carries a per-instance __dict__ hash map. With slots=True the attributes live in fixed slots and there is no __dict__, so each object is smaller and attribute access is faster; the trade-off is you cannot add new attributes at runtime.

Script Implementation

Setup

Start with a plain, typed dataclass. Even without any flags this beats a dict: you get types, a real constructor, and a readable repr.

models.py
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float

Slots and frozen

Add slots=True for memory and frozen=True for immutability. A frozen instance is hashable, so it works as a dict key or set member.

@dataclass(slots=True, frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
seen = {p} # hashable: works in a set
# p.x = 5.0 # raises FrozenInstanceError

Validation with __post_init__

__post_init__ runs right after the generated __init__, so it is the place for validation and derived fields. On a frozen instance you set derived fields with object.__setattr__.

@dataclass(slots=True, frozen=True)
class Temperature:
celsius: float
fahrenheit: float = field(init=False)
def __post_init__(self) -> None:
if self.celsius < -273.15:
raise ValueError("below absolute zero")
# frozen instances block normal assignment, so go around it:
object.__setattr__(self, "fahrenheit", self.celsius * 9 / 5 + 32)

Mutable defaults and nesting

Never write tags: list = [] on a dataclass; every instance would share the same list. Use field(default_factory=...), and nest dataclasses freely.

@dataclass(slots=True)
class Order:
id: str
items: list[str] = field(default_factory=list) # fresh list per instance
shipping: "Point | None" = None # optional nested field

Usage and Benefits

Why this helps

You get objects that are small, safe, and self-checking, with almost no boilerplate. slots=True cuts memory on large collections of objects, frozen=True makes it impossible to accidentally mutate shared state, and __post_init__ means an invalid object can never be constructed in the first place.

Using it

>>> Temperature(25.0)
Temperature(celsius=25.0, fahrenheit=77.0)
>>> Temperature(-300)
ValueError: below absolute zero

Community Discussion

Your turn

How do you decide between a dataclass, Pydantic, and a TypedDict? I lean on the rule below, but I am curious where other people draw the line.

Alternative approaches

The three tools solve different problems. Pydantic parses and validates untrusted input at runtime, which makes it the right choice at an API boundary. TypedDict adds static typing over a plain dict with zero runtime cost, good for JSON you just pass through. Dataclasses sit in between: real objects with behavior, light validation, and memory control, for your own trusted internal models.

Validate untrusted input at a boundary? Pydantic. Just need a typed shape over a dict with no runtime cost? TypedDict. Want real objects with behavior, light validation, and memory control? A dataclass.

Was this useful?

You might also enjoy

More posts on similar topics

Python Async HTTP Requests with aiohttp: Fetch Multiple URLs Concurrently

Python Async HTTP Requests with aiohttp: Fetch Multiple URLs Concurrently

Quick Tip Reuse one aiohttp session, fan your requests out with asyncio.gather, and cap them with a semaphore to fetch hundreds of URLs in the time one loop would take. The Problem **The

Node.js Environment Variable Validation with Zod at Startup

Node.js Environment Variable Validation with Zod at Startup

Most Node.js apps treat process.env like a trusted friend. You reach into it whenever you need a value, assume the key is there, assume it's spelled right, and assume the string is actually the type

Redis Caching Patterns: Cache-Aside, Write-Through & Cache Invalidation

Redis Caching Patterns: Cache-Aside, Write-Through & Cache Invalidation

Need to scale your backend without throwing money at servers? Start with Redis caching patterns. Most databases can handle hundreds of queries per second, but thousands? Your app slows to a crawl

Optimizing your python code with __slots__?

Optimizing your python code with __slots__?

Memory optimization with slots Understanding the problem Optimizing data models in big data workflows with slots In big data and MLOps workflows, you often work with massive

List S3 Buckets

List S3 Buckets

Overview Multi-profile S3 management Ever juggled multiple AWS accounts and needed a quick S3 bucket inventory across all of them? This Python script handles it. Use case Perfect for or

Bash Script Locking: Prevent Concurrent Runs with a PID File

Bash Script Locking: Prevent Concurrent Runs with a PID File

Quick Tip Wrap any script that must not run twice at once in a PID-file lock, and a second copy simply exits instead of corrupting your data. The Problem The Problem Cron does not c

6 related posts