Topic 8.12
__slots__ and Object Memory
In one line
By default every object carries its own __dict__ for attributes, which costs memory. Declaring __slots__ stores attributes in a fixed layout instead, saving memory for millions of small objects and blocking accidental new attributes, at the price of flexibility.
Think of it like this
A hotel. A normal object is like a guest who arrives with a personal wardrobe that can hold anything, even if they bring one shirt. A slotted object is a room with exactly three labelled hooks. The hooks are cheaper and tidier, but you cannot hang a fourth thing.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- `__dict__`
- The dictionary that holds an ordinary object's attributes.
- `__slots__`
- A class attribute that lists the only allowed attribute names and replaces the per-object dictionary.
- Memory footprint
- How much memory something occupies.
- `sys.getsizeof`
- Returns the size in bytes of an object itself, not counting the objects it refers to.
- `tracemalloc`
- A standard-library tool that measures how much memory Python code allocates.
- Weak reference
- A reference that does not keep its target alive. It becomes
Nonewhen the target is freed. - 32-bit / 64-bit build
- The size of a pointer in the Python build. Sizes reported by
getsizeofdiffer between them. - Profile
- To measure where a program spends time or memory.
Step by step
01Where attributes normally live
In Plain, self.a = a stores into a hidden dictionary. vars(p) shows it: ['a', 'b', 'c']. Because it is a dictionary, p.d = 4 simply works.
02Declare the slots
Slim lists its three attribute names in __slots__. The methods look the same. hasattr(s, "__dict__") is False: the values live in fixed positions inside the object.
class Slim:
__slots__ = ("a", "b", "c")
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c03What you give up
s.d = 4 fails with AttributeError: 'Slim' object has no attribute 'd' and no __dict__ for setting new attributes. vars(s) raises TypeError. Both follow from having no dictionary.
Sometimes that is exactly what you want: a misspelt attribute can no longer create a hidden bug.
04Measuring, and its caveats
The example compares sys.getsizeof(s) with sys.getsizeof(p) + sys.getsizeof(p.__dict__) and prints only whether the slotted object is smaller, because the byte counts change with the Python build. This page runs a 32-bit WebAssembly build, which reports smaller numbers than a 64-bit desktop. The conclusion (slots are smaller) holds in both.
05Inheritance undoes slots
class Loose(Slim): pass has no __slots__ of its own, so Python adds a __dict__ and the saving is lost for those objects. If you subclass a slotted class, declare __slots__ = () in the child, or list its extra names.
06Weak references
weakref.ref(n) gives a reference that does not keep n alive. After del n, nothing else holds the object, so ref() returns None. A slotted class must list "__weakref__" or it cannot be weak-referenced.
CPython frees most objects the moment their last reference disappears (reference counting, Topic 13.2), which is why the example works without calling the garbage collector.
07Decide with evidence
Measure first. If you create a few thousand objects, slots will not matter. If you hold millions, use tracemalloc on realistic data before and after, and keep slots only if the saving is worth the lost flexibility.
Try it yourself
- 1
Trigger the guard
In the example, try
s.colour = "red"on the slotted object. Read the error. Then do the same on the plain object and notice nothing stops you. - 2
Keep slots in a subclass
Change
Looseto declare__slots__ = ()and printhasattr(Loose(1, 2, 3), "__dict__")again. It is nowFalse. - 3
Weak reference lifetime
Create a second name
m = nbeforedel nand run again. The weak reference still works, becausemkeeps the object alive.
Code & diagrams
The program prints comparisons, not byte counts, because sizes differ between builds (this page is a 32-bit WebAssembly build). On your own machine print sys.getsizeof values to see the real numbers.
Expected output
True False
['a', 'b', 'c']
AttributeError: 'Slim' object has no attribute 'd' and no __dict__ for setting new attributes
4
TypeError: vars() argument must have __dict__ attribute
1 2 3 ('a', 'b', 'c')
slots object is smaller: True
pointer size in bits: 32
True
5
NoneRun it on your computer. The exact numbers depend on your Python version and machine, so none are shown here. Expect the slotted version to use clearly less memory.
import tracemalloc
class Plain:
def __init__(self, a, b):
self.a = a
self.b = b
class Slim:
__slots__ = ("a", "b")
def __init__(self, a, b):
self.a = a
self.b = b
def measure(cls):
tracemalloc.start()
items = [cls(i, i) for i in range(100_000)]
current, _ = tracemalloc.get_traced_memory()
tracemalloc.stop()
return current
print("plain:", measure(Plain), "bytes")
print("slots:", measure(Slim), "bytes")Not run here because there is nothing to print. This writes the __slots__ line for you.
from dataclasses import dataclass
@dataclass(slots=True)
class Token:
text: str
start: int
end: intBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Add a new attribute to a slotted object
Assign an attribute that is not listed in __slots__.
class Slim:
__slots__ = ("a",)
def __init__(self, a):
self.a = a
s = Slim(1)
s.b = 2Myth vs fact
Myth
__slots__ makes every Python program faster.
Fact
It saves memory per object and can slightly speed attribute access. For a few objects it changes nothing noticeable.
Myth
sys.getsizeof tells you how much memory an object uses in total.
Fact
It counts the object itself only, not the things it refers to. A list of a million strings reports the size of the list's pointers, not the strings.
Myth
Slots make attributes private.
Fact
They only fix which names exist. Anyone can still read and write them.
Myth
Byte counts from one Python build apply to every machine.
Fact
Sizes change with the version and with 32-bit or 64-bit builds. Measure on the machine that matters.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Attribute access with slots uses member descriptors (one per slot, stored on the class), so
s.areads a fixed offset in the object. Ordinary objects look up a dictionary. Since Python 3.11, instances of ordinary classes have an optimised inline layout for their dictionary, which narrowed the gap, so always measure with your Python version. - ▸
Slots disable the per-instance
__dict__and__weakref__unless you ask for them. Pickling slotted objects works on modern protocols; multiple inheritance with several slotted parents that define non-empty slots raises a layout conflict error. - ▸
@dataclass(slots=True)creates a new class when it decorates, which can surprise code holding a reference to the original class.functools.cached_propertyneeds a__dict__, so it does not work on a slotted class without one. Dataclass slots also drop__weakref__unless you ask for it with@dataclass(slots=True, weakref_slot=True)(Python 3.11). - ▸
Slot rules worth knowing: a name in
__slots__cannot also have a class-level default (ValueError: 'x' in __slots__ conflicts with class variable), so set defaults in__init__. List"__dict__"in__slots__if you want fixed slots plus free-form extras. In a slotted child, list only the new names; repeating a parent's slot wastes space. Each slot is a descriptor on the class (Topic 13.5). - ▸
For millions of numeric records, the biggest memory win usually comes from leaving objects behind: NumPy arrays or pandas columns store raw numbers compactly (Phase 14). Slotted objects are the middle ground when each record needs methods or mixed types.
Remember this
- 1
Each ordinary Python object keeps its attributes in a dictionary called
__dict__(Topic 8.2). A dictionary is flexible, because you can add any attribute at any time, but it also takes memory: it is a hash table (Topic 4.5) that grows in steps. For one object this is nothing. For millions of small objects (graph nodes, parsed records, tokens) it adds up. - 2
__slots__ = ("a", "b", "c")in the class body tells Python the exact attribute names. The class then stores the values in a compact fixed layout, with no__dict__. The program is the same, but each object is smaller, and in a typical case attribute reading is a little faster. - 3
The trade-offs are real. A slotted object cannot get new attributes:
s.d = 4raisesAttributeError: ... no __dict__ for setting new attributes.vars(s)fails because there is no dictionary. A subclass that does not declare its own__slots__gets a__dict__back, undoing the savings. Slots also do not combine with multiple inheritance from two classes that both have non-empty slots. - 4
Slots are handy as a typo guard:
s.colour = "red"on a class that has onlycolour_namefails loudly instead of creating a stray attribute. Dataclasses expose this as@dataclass(slots=True)(Python 3.10), so you rarely write__slots__by hand today. - 5
To measure memory,
sys.getsizeof(obj)gives the size of the object itself, not what it points to. For a normal object addsys.getsizeof(obj.__dict__). The numbers depend on the Python build: this page runs in a 32-bit WebAssembly build (WebAssembly is the format that lets Python run inside your browser tab), where pointers are 4 bytes, so sizes are about half of those on a 64-bit desktop. For a real measurement usetracemallocor a profiler on your own machine with realistic data. Never memorise byte counts. - 6
Weak references (
weakref) let you point to an object without keeping it alive: when the last normal reference goes, the object is freed and the weak reference returnsNone. They are used in caches and observer lists. A slotted class must include"__weakref__"in__slots__to be weak-referenceable. - 7
Do not optimise early. Use slots when a profile shows millions of instances dominating memory. Otherwise a plain class or a plain dataclass is clearer. For large numeric data, memory is better saved with NumPy arrays (Phase 14) than with slotted objects.
Explain it without notes
Why does a normal Python object use more memory than a slotted one?
What do you gain and lose by adding __slots__?
Why can't you trust sys.getsizeof alone to measure memory?
What is a weak reference, and what does it have to do with slots?
Practice
Write a slotted class Point with x and y. Show that assigning p.z = 1 raises AttributeError and print Point.__slots__.
Write a function has_dict(obj) that returns whether an object has a __dict__, and show it for a plain object and a slotted object.
Create a Cell class with __slots__ = ("value", "__weakref__"). Make a weak reference to one, delete the object and print what the reference returns.
Trade-offs
- ↔
Slots save memory and catch typos, but remove flexibility. Use them for large numbers of small, fixed-shape objects, not as a default.
- ↔
Optimising memory by hand adds restrictions that readers must know about. Prove the need with a measurement first, and prefer
@dataclass(slots=True)so the intent is visible.
Done when you can
Done when you can explain where an ordinary object keeps its attributes.
Done when you can write a class with
__slots__and state what it prevents.Done when you can explain why a subclass can lose the benefit of slots.
Done when you can say why
sys.getsizeofnumbers differ between machines and builds, and how to measure properly.Done when you can explain what a weak reference is.