---
title: "Python Dataclass Patterns: Slots, Frozen Instances, and Field Validation"
description: "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."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/codesnippets/post/python-dataclass-slots-frozen-field-validation
---

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

**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:

_Regular dataclass vs. slots=True in memory_

## 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.

```python title="models.py" showLineNumbers
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.

```python
@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__`.

```python
@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.

```python
@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

```python
>>> 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.

_Choosing dataclass vs. Pydantic vs. TypedDict_
