Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~25 min· topic 12 of 12

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 None when the target is freed.
32-bit / 64-bit build
The size of a pointer in the Python build. Sizes reported by getsizeof differ 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.

main.pywhole filepython
class Slim:
    __slots__ = ("a", "b", "c")

    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c
Declare the slotsdiagram
Rendering diagram…

03What 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. 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. 2

    Keep slots in a subclass

    Change Loose to declare __slots__ = () and print hasattr(Loose(1, 2, 3), "__dict__") again. It is now False.

  3. 3

    Weak reference lifetime

    Create a second name m = n before del n and run again. The weak reference still works, because m keeps the object alive.

Code & diagrams

Plain objects versus slotted objects New tab

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.

Sign in to run this example in your browser.

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
None
Measuring on your own machine (not runnable here)python

Run 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")
The same idea as a dataclass (Python 3.10) Python 3.10+python

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: int

Break 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__.

main.pywhole filepython
class Slim:
    __slots__ = ("a",)

    def __init__(self, a):
        self.a = a


s = Slim(1)
s.b = 2
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 9, in <module>
s.b = 2
^^^
AttributeError: 'Slim' object has no attribute 'b' and no __dict__ for setting new attributes

Myth 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.a reads 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_property needs 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. 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. 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. 3

    The trade-offs are real. A slotted object cannot get new attributes: s.d = 4 raises AttributeError: ... 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. 4

    Slots are handy as a typo guard: s.colour = "red" on a class that has only colour_name fails 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. 5

    To measure memory, sys.getsizeof(obj) gives the size of the object itself, not what it points to. For a normal object add sys.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 use tracemalloc or a profiler on your own machine with realistic data. Never memorise byte counts.

  6. 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 returns None. They are used in caches and observer lists. A slotted class must include "__weakref__" in __slots__ to be weak-referenceable.

  7. 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

01

Why does a normal Python object use more memory than a slotted one?

02

What do you gain and lose by adding __slots__?

03

Why can't you trust sys.getsizeof alone to measure memory?

04

What is a weak reference, and what does it have to do with slots?

Practice

01

Write a slotted class Point with x and y. Show that assigning p.z = 1 raises AttributeError and print Point.__slots__.

02

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.

03

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.getsizeof numbers differ between machines and builds, and how to measure properly.

  • Done when you can explain what a weak reference is.