Where does the memory go?
Two classes hold the same coordinates. Why can one use less memory? Follow where Python stores their attributes, then see what changes when you add __slots__.
Python slots: a storage story
Step each scene independently. Diagrams explain storage; outputs replay recorded CPython results.
Recorded CPython 3.13.13 · 64-bit Linux. Python does not run in your browser.
Where the attributes live
Keep x and y the same. Change the class declaration, then inspect what the instance needs to store them.
Conceptual layout. Fields hold references; values are separate objects. Box sizes do not represent bytes.
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y
point = RegularPoint(1, 2)A regular instance supports a dictionary interface for its attributes. The x and y fields refer to the values 1 and 2. Modern CPython can share attribute keys and use inline values; this diagram shows the logical storage, not a literal allocation map.
Fixed names, changeable values
Slots restrict which instance attributes exist. They do not make the values immutable.
Conceptual layout. Fields hold references; values are separate objects. Box sizes do not represent bytes.
point = SlottedPoint(1, 2)
point.x = 10
print(point.x, point.y)The x field now refers to 10; y still refers to 2. A fixed layout allows normal assignment to declared fields.
When a small saving adds up
Compare retained allocations for equal populations. Every point refers to the same two pre-existing values in this controlled experiment.
Instances + holding list. Pre-existing values and class storage excluded. Bars start at zero and rescale for each population.
for Point in (RegularPoint, SlottedPoint):
points = [Point(x_value, y_value)
for _ in range(1)]Both captures include the instances and their holding list, after the same warm-up. Class storage and the two pre-existing values are excluded. The difference belongs to this interpreter and workload; it is not a universal size or speed claim.
A subclass can bring the dictionary back
Follow the same two fields into a child class. The child declaration determines whether dynamic instance attributes return.
Conceptual layout. Fields hold references; values are separate objects. Box sizes do not represent bytes.
class PlainChild(SlottedPoint):
pass
point = PlainChild(1, 2)
print(hasattr(point, "__dict__"))The parent slots are still there, but this child also has an instance dictionary. Declaring slots only in the parent does not keep the whole hierarchy dictionary-free.
Allow observation without ownership
Weak references connect this layout choice back to garbage collection. A dictionary-free class needs explicit weak-reference support unless a parent provides it.
Conceptual layout. Fields hold references; values are separate objects. Box sizes do not represent bytes.
import weakref
point = SlottedPoint(1, 2)
weakref.ref(point)The two declared fields do not include weak-reference support. This call raises TypeError; it does not create a weak reference.
How the memory measurement was captured
Retained tracemalloc allocation delta: instances plus their holding list, after a 64-instance warm-up and a collection before sampling. Two shared values and the class are allocated before tracing and excluded. Attribute dictionaries are not accessed during measurement. This is not process RSS or an attribute-speed benchmark.
Regenerate with python3 scripts/gen-slots-data.py. The values describe one controlled run; measure your actual classes and payloads before choosing a representation.
What the declaration changes
__slots__ = ("x", "y") declares instance field names when Python creates the class. Python installs a descriptor for each name on the class; that descriptor reads and writes the corresponding reference field in an instance. The values remain ordinary Python objects.
A regular instance offers a flexible __dict__ interface. Its physical storage depends on the CPython version: attribute keys can be shared between instances, and CPython 3.13 can keep values inline. The explorer therefore shows a logical layout, rather than claiming every instance owns a separate full hash table.
For a class whose parents provide neither feature, slots avoid automatically adding an instance dictionary and weak-reference support. You can request either explicitly. The trade-off is a fixed set of field names, with memory savings that depend on the interpreter, inheritance hierarchy and workload.
Measure the objects you actually create
The recorded experiment isolates storage overhead by sharing two pre-existing values. Your application may allocate a different value for every field. Measure that workload too:
import sys
import tracemalloc
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
def build(cls, n=100_000):
return [cls(i, i + 1) for i in range(n)]
for cls in (RegularPoint, SlottedPoint):
tracemalloc.start()
points = build(cls)
current, peak = tracemalloc.get_traced_memory()
print(cls.__name__, f"retained: {current / 1024 / 1024:.2f} MiB")
print(f"peak: {peak / 1024 / 1024:.2f} MiB")
print("instance size:", sys.getsizeof(points[0]))
tracemalloc.stop()
Object size is only part of memory use
This example includes the list, instances and integers allocated while tracing. current reports retained traced allocations; peak also includes temporary allocations. Neither is process RSS. sys.getsizeof(instance) alone does not include every part of attribute storage or the objects referenced by the fields.
Declare the fields that belong to the class
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
This class now supports only the declared instance attributes. That is useful when the shape is part of the contract. It is wrong when callers are expected to attach extra fields for caching, instrumentation, or framework state.
Use a dataclass for a simple data container
from dataclasses import dataclass
@dataclass(slots=True)
class Point:
x: float
y: float
For simple data containers, slots=True keeps the field declaration in one place and generates slot storage. It is available in Python 3.10 and later. Dataclass field defaults are handled by the decorator, so they do not have the conflict that manually declared slots have with same-named class defaults.
Keep dynamic attributes or weak references deliberately
class HybridPoint:
__slots__ = ("x", "y", "__dict__")
def __init__(self, x, y):
self.x = x
self.y = y
point = HybridPoint(1, 2)
point.label = "origin" # stored in __dict__
Add __dict__ only when dynamic attributes are intentionally part of the API. Add __weakref__ when instances must work with weakref.ref(), weak caches, or observer registries.
For a dataclass on Python 3.11 or later, use @dataclass(slots=True, weakref_slot=True) when weak references are required. An inherited dictionary or weak-reference capability remains available even if the child declares slots.
Preserve the layout through inheritance
class BasePoint:
__slots__ = ("x", "y")
class LabeledPoint(BasePoint):
__slots__ = ("label",)
class Marker(BasePoint):
__slots__ = ()
Slots declared in a parent are available to child instances. A child that adds fields should declare only the additional slot names. A child that adds no fields can use __slots__ = () to avoid bringing back an instance dictionary.
Know the limits
Slot names are not class defaults
class Wrong:
__slots__ = ("x",)
# x = 10 # would conflict with the slot descriptor
class Right:
__slots__ = ("x",)
def __init__(self, x=10):
self.x = x
Multiple inheritance has layout limits
Only one parent class in a multiple-inheritance hierarchy can contribute non-empty slot storage. Other slotted bases need empty layouts such as __slots__ = ().
Slots are not packed numeric arrays
Slots store Python object references. For dense numeric data, arrays, NumPy, struct, or extension types may be a better representation.
The Python data model documents the slot and inheritance rules. The dataclasses reference describes the generated-layout options.
Choose the representation for the workload
| Situation | Use __slots__? | Reason |
|---|---|---|
| Many small objects with fixed attributes | Yes, after measuring | Removes per-instance dictionary overhead |
| Objects need arbitrary attributes | No | Dynamic assignment is part of the behavior |
| Simple annotated data container | Prefer @dataclass(slots=True) | Less duplication and fewer descriptor mistakes |
| Need weak references | Yes, with __weakref__ | Weak references are not automatic for slotted classes |
| Dense numeric vectors | Usually no | Slots still store Python object references |
Related concepts
Understand CPython garbage collection: reference counting, generational GC for circular references, weak references, and gc module tuning strategies.
A practical mental model for CPython memory management: names and references, object headers, PyMalloc arenas, reference counting, reuse paths, and memory profiling.
See how tail calls reuse stack frames, when compilers can eliminate them, and how CPython 3.14 applies tail calls to C opcode dispatch.
Deep dive into Python's asyncio library, understanding event loops, coroutines, tasks, and async/await patterns with interactive visualizations.
Follow direct-style and CPS factorial through the same calculation. See how closures carry pending work, why Python still grows call depth, and where trampolines fit.
C++ compiler optimization lab notebook — compare optimization levels, inspect compiler rewrites, diagnose auto-vectorization, and build production verification commands.
