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 slotsSlots 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 == 2slots=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: intAn assignment such as coordinate.x = 10 then raises FrozenInstanceError.
Keep the concepts separate:
slots=True -> fixed attribute layout
frozen=True -> generated assignment protectionA 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 fitSlots 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: strThe 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 = 0If it belongs to a processing stage rather than the event itself, keep it separate:
@dataclass(slots=True)
class WorkItem:
event: Event
retry_count: int = 0Slots 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 documentA 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 documentweakref_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: intMetadata 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 = yFor a dataclass, prefer the decorator option:
from dataclasses import dataclass
@dataclass(slots=True)
class Point:
x: int
y: intThis 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: intThe 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.