Python dataclasses reduce boilerplate for record-like classes, but their instances are still ordinary Python objects by default. Declared fields normally live in an instance dictionary, and new attributes can be attached later.

That flexibility is useful until the model is supposed to have a fixed shape. Coordinates, parsed records, configuration snapshots, protocol messages, and other compact value objects often have a known set of fields. When many such objects exist, keeping dynamic per-instance attribute storage may also be unnecessary.

@dataclass(slots=True) creates a slotted dataclass. Its declared fields use slots rather than relying on the usual per-instance __dict__.

The useful mental model is:

ordinary dataclass -> declared fields plus dynamic instance attributes
slotted dataclass  -> predefined instance attribute slots

Slots change the instance layout and restrict attribute names. They do not make the object immutable, and they do not guarantee a performance improvement.

Start with the default dataclass behavior

A normal dataclass behaves like a regular Python class with generated methods:

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int


point = Point(2, 3)

assert point.__dict__ == {"x": 2, "y": 3}
point.label = "checkpoint"
assert point.label == "checkpoint"

The annotations tell dataclass which fields participate in generated behavior. They do not prevent other attributes from being created.

That distinction matters when a class is intended to represent a fixed schema.

slots=True gives instances a fixed attribute layout

Add slots=True to the decorator:

from dataclasses import dataclass


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


point = Point(2, 3)

assert point.x == 2
assert point.y == 3
assert not hasattr(point, "__dict__")

Trying to attach an undeclared attribute now fails:

point.label = "checkpoint"

Python raises AttributeError because label is not an available slot.

This restriction can prevent accidental state from appearing on an object. If a misspelled attribute is assigned, it fails instead of silently becoming another entry in __dict__.

Slots do not make fields read-only

A slotted dataclass remains mutable unless you choose another mechanism:

from dataclasses import dataclass


@dataclass(slots=True)
class Counter:
    value: int


counter = Counter(1)
counter.value += 1

assert counter.value == 2

slots=True answers which attribute names may be stored. It does not answer whether those attributes may be reassigned.

If you also want dataclass-generated protection against normal assignment, use frozen=True:

from dataclasses import dataclass


@dataclass(slots=True, frozen=True)
class Coordinate:
    x: int
    y: int

An assignment such as coordinate.x = 10 then raises FrozenInstanceError.

Keep the concepts separate:

slots=True   -> fixed attribute layout
frozen=True  -> generated assignment protection

A class may need either option, both, or neither.

Why slots can reduce per-instance overhead

Ordinary Python instances commonly need storage that supports a dynamic mapping from attribute names to values. Slots declare the attribute names in advance, so a slotted instance normally does not need its own attribute dictionary.

For many small fixed-shape instances, avoiding that dictionary can reduce memory overhead.

The important word is can. Exact object size is an implementation detail. It varies with the Python implementation, interpreter version, architecture, inheritance structure, and object contents. A universal claim such as “slots save N bytes per object” is not portable.

If memory matters, measure representative objects under the Python implementation you deploy.

A useful decision rule is:

few instances
    -> prefer the clearest model

many small fixed-shape instances
    -> slots may be worth measuring

dynamic attributes required
    -> ordinary instances are usually the better fit

Slots are a structural design choice first and a possible optimization second.

Use slots when fixed shape is part of the model

Suppose a parser creates large numbers of event records:

from dataclasses import dataclass


@dataclass(slots=True)
class Event:
    timestamp_ms: int
    user_id: int
    event_type: str

The intended instance state is explicit. Code cannot later attach unrelated attributes such as debug_note or cached_json.

If a value is truly part of every event, declare it:

@dataclass(slots=True)
class Event:
    timestamp_ms: int
    user_id: int
    event_type: str
    retry_count: int = 0

If it belongs to a processing stage rather than the event itself, keep it separate:

@dataclass(slots=True)
class WorkItem:
    event: Event
    retry_count: int = 0

Slots help most when they reinforce a meaningful model boundary rather than forcing unrelated state into one class.

Weak references need explicit support

Weak-reference behavior is a subtle consequence of slots.

A normal dataclass is generally weak-referenceable:

from dataclasses import dataclass
import weakref


@dataclass
class Document:
    name: str


document = Document("guide")
reference = weakref.ref(document)

assert reference() is document

A slotted dataclass does not automatically include the special __weakref__ slot:

from dataclasses import dataclass
import weakref


@dataclass(slots=True)
class Document:
    name: str


document = Document("guide")
weakref.ref(document)

That raises TypeError.

Starting with Python 3.11, dataclasses provide weakref_slot=True:

from dataclasses import dataclass
import weakref


@dataclass(slots=True, weakref_slot=True)
class Document:
    name: str


document = Document("guide")
reference = weakref.ref(document)

assert reference() is document

weakref_slot=True requires slots=True.

This matters for weak caches, registries, observer relationships, and other designs that deliberately avoid extending an object’s lifetime.

Inheritance needs more care than a flat dataclass

A slotted dataclass can inherit from another slotted dataclass:

from dataclasses import dataclass


@dataclass(slots=True)
class Position:
    x: int
    y: int


@dataclass(slots=True)
class NamedPosition(Position):
    name: str


position = NamedPosition(2, 3, "checkpoint")

assert position.x == 2
assert position.name == "checkpoint"

Do not inspect __slots__ to discover the complete set of dataclass fields. Inherited slot names are handled as part of class layout, while dataclass field metadata has its own API.

Use dataclasses.fields():

from dataclasses import fields

names = [field.name for field in fields(NamedPosition)]

assert names == ["x", "y", "name"]

That asks the correct question directly: which fields belong to this dataclass?

A non-slotted base class can bring dict back

Consider this hierarchy:

class Metadata:
    pass


from dataclasses import dataclass


@dataclass(slots=True)
class Record(Metadata):
    value: int

Metadata is an ordinary class. Its instances provide dynamic instance storage, and subclasses can inherit that capability.

Therefore, slots=True on Record does not justify assuming the whole object has no __dict__.

When fixed shape or memory layout matters, inspect the complete inheritance chain. Slotted designs are easiest to reason about when the hierarchy is intentionally designed around compatible layouts.

Prefer dataclass slots to manual slots

Python also supports manual __slots__:

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

For a dataclass, prefer the decorator option:

from dataclasses import dataclass


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

This keeps dataclass fields and generated slots coordinated.

Do not define __slots__ manually on the same class and also request @dataclass(slots=True). Dataclasses reject that combination with TypeError.

slots=True returns a new class

There is one advanced behavior worth knowing: when slots=True is requested, the dataclass decorator returns a new class instead of modifying and returning the original class object.

Ordinary code rarely notices:

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

The name Point simply refers to the class returned by the decorator.

This can matter to advanced frameworks that customize class creation, use __init_subclass__, or make assumptions about class identity during decoration. If a framework performs such work, test the slotted form explicitly rather than assuming it follows the same creation path as a regular dataclass.

Do not assume slots guarantee faster attribute access

It is tempting to present slots as a blanket speed optimization. That claim is too broad.

Instance layout can affect attribute access and allocation behavior, but actual results depend on the Python implementation and workload. Modern interpreters also optimize ordinary attribute access.

If throughput or latency is the reason for adopting slots, benchmark the operation that matters on the deployed interpreter. A parser may spend far more time decoding input or allocating strings than loading fields from each record.

Use slots because the fixed layout fits the model. Treat speed improvements as something to measure, not something to assume.

Common mistakes

Using slots for every dataclass

Small object counts rarely justify extra constraints. If dynamic attributes are useful, the default dataclass is simpler.

Confusing slots with immutability

Slotted fields remain assignable unless another mechanism such as frozen=True prevents normal assignment.

Forgetting weak-reference requirements

Slotted objects need a __weakref__ slot for weak references. On Python 3.11 and later, weakref_slot=True provides it.

Reading slots as dataclass metadata

Use dataclasses.fields(). Class layout and dataclass field metadata are related but distinct concerns.

Assuming a slotted subclass has no dict

A non-slotted base class can provide one. Consider the entire inheritance hierarchy.

Quoting universal memory savings

Exact memory use is implementation-dependent. Measure representative instances when memory reduction is an actual requirement.

When a normal dataclass is the better choice

Keep the default dataclass when dynamic attributes are intentional, a framework expects __dict__, object counts are too small for layout overhead to matter, or the inheritance hierarchy was not designed with slots in mind.

Dynamic attributes are part of Python’s ordinary object model. They are useful for extensible application objects and interactive code.

Slots deliberately trade some of that flexibility for a declared instance layout.

Choose slots because the object really has a fixed shape

@dataclass(slots=True) is a strong fit when a class represents a stable set of fields and many instances make per-instance layout worth considering.

The central behavior is structural: declared fields get slots, arbitrary new attributes are normally unavailable, and the usual per-instance dictionary is absent unless the hierarchy provides one.

Keep slots, immutability, and weak-reference support as separate decisions. Check inheritance carefully, use dataclasses.fields() for metadata, and measure memory or performance claims in the environment you actually deploy.

That turns slots from a folklore optimization into a deliberate object-model choice.