By default, Python allocates a __dict__ for every instance of a class. Flexible, yes. Cheap, no. When you hold thousands or millions of objects in memory at once, that dictionary overhead adds up fast. __slots__ removes it and replaces per-instance storage with compact internal descriptors. Typical result: 40 to 60 percent less memory per instance.

What Python does without slots

Without any declaration, each instance carries its own __dict__:

class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

p = Point(1.0, 2.0)
print(p.__dict__)  # {'x': 1.0, 'y': 2.0}

This dictionary allows adding attributes at runtime:

p.z = 3.0       # works
p.label = "A"   # also works

That flexibility comes at a cost. An empty Python dict takes several hundred bytes (184 bytes on Python 3.12, 232 on Python 3.8), and every single instance pays that price.

slots: removing the dict

Declaring __slots__ tells Python the exact set of attributes an instance is allowed to have. Python skips the __dict__ allocation entirely and uses slot descriptors instead:

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

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

p = Point(1.0, 2.0)
print(hasattr(p, "__dict__"))  # False

Trying to set an undeclared attribute raises AttributeError immediately:

p.z = 3.0  # AttributeError: 'Point' object has no attribute 'z'

Measuring the real gain

Here is a concrete benchmark using tracemalloc:

import tracemalloc


class WithoutSlots:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y


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

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y


N = 100_000

tracemalloc.start()
without = [WithoutSlots(i, i) for i in range(N)]
_, peak_without = tracemalloc.get_traced_memory()
tracemalloc.stop()

tracemalloc.start()
with_slots = [WithSlots(i, i) for i in range(N)]
_, peak_with = tracemalloc.get_traced_memory()
tracemalloc.stop()

print(f"Without __slots__: {peak_without / 1_000_000:.1f} MB")
print(f"With __slots__:    {peak_with / 1_000_000:.1f} MB")

Typical output on Python 3.12:

Without __slots__: 28.3 MB
With __slots__:     9.6 MB

Around 66 percent reduction. The exact number varies by attribute count and type.

When to use slots

Large numbers of instances held in memory simultaneously. Classic cases: data objects for simulations, graph or tree nodes, events in a processing pipeline, records bulk-loaded from an API or a flat file.

Preventing accidental attribute creation. On value objects and DTOs, forbidding dynamic attributes eliminates a subtle bug class: a typo on an attribute name that silently creates a new attribute instead of modifying the intended one.

Attribute access speed. Slot access via descriptor is marginally faster than hash-table lookup through __dict__. The difference rarely matters in a typical web request, but it shows up on tight benchmarks or hot inner loops.

The pitfalls worth knowing

Inheritance: parent classes without slots

If a parent class has no __slots__, its instances carry a __dict__, and subclasses inherit it even if they declare their own slots:

class Base:
    pass  # no __slots__ → has __dict__

class Child(Base):
    __slots__ = ("x",)

c = Child()
c.surprise = "still works"  # Base has no __slots__, so its instances have __dict__, which Child inherits

For the memory benefit to hold across a hierarchy, every class in the chain must declare its own __slots__ without inheriting from a class that has __dict__.

Framework incompatibilities

Several libraries expect a __dict__ on instances: some ORMs, serialization frameworks, and certain pickle or deep-copy implementations depending on their version. Test compatibility before applying __slots__ to classes used with third-party frameworks.

weakref disappears too

Instances with __slots__ lose weak reference support by default. If you need it, add "__weakref__" explicitly to the list:

class Node:
    __slots__ = ("value", "next", "__weakref__")

dataclasses and slots

Since Python 3.10, @dataclass accepts slots=True directly:

from dataclasses import dataclass


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

This is the cleanest way to use __slots__ on modern data classes. No manual declaration, no risk of the slot list going out of sync with the fields.

Summary

Without slotsWith slots
Attribute storagePer-instance __dict__Internal descriptors
Dynamic attribute additionYesNo
Memory per instanceHigher40–60% less
Attribute access speedSlightly slowerSlightly faster
Framework compatibilityUniversalMust verify
Multi-level inheritanceSimpleRequires discipline

__slots__ is not the first optimization to reach for. It is a precision tool for situations where memory has been measured and documented as a real problem. Hundreds of objects in a typical web application: negligible impact. Millions of objects in memory during a pipeline run or a simulation: one of the most direct levers available in pure Python.