Python’s __slots__ Secret: Cut Memory 60% and Speed Up Attribute Access by 35%

Python's __slots__ Secret: Cut Memory 60%

Python’s __slots__ is one of the most powerful memory optimization features in the language — and one of the least used. Most Python developers never encounter it outside of performance-critical libraries. That’s a mistake. Understanding __slots__ can cut your object memory usage by 40–60% and speed up attribute access by up to 35%.

TL;DR: By default, every Python object stores its attributes in a __dict__ dictionary. __slots__ replaces that dict with a fixed-size array, eliminating the dictionary overhead entirely. For classes with millions of instances, this is transformative.

The Problem: Every Python Object Carries a Hidden Dictionary

Every Python class instance, by default, stores its attributes in a __dict__. That dictionary has significant overhead — a Python dict has a minimum size of ~200 bytes plus per-key overhead. When you create millions of objects, this adds up fast.

import sys

class PointWithDict:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

class PointWithSlots:
    __slots__ = ['x', 'y', 'z']
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

p1 = PointWithDict(1.0, 2.0, 3.0)
p2 = PointWithSlots(1.0, 2.0, 3.0)

print(sys.getsizeof(p1))           # 48 bytes (object) + dict overhead
print(sys.getsizeof(p1.__dict__))  # 232 bytes — the hidden cost
print(sys.getsizeof(p2))           # 64 bytes — no __dict__ at all

# Total memory per instance:
# With __dict__:  ~280 bytes
# With __slots__:  64 bytes
# Savings: 77%

Real Benchmark: 1 Million Objects

Deepen your Python knowledge

The Complete Python Bootcamp (Udemy) — 100 hours covering Python internals, OOP, and memory optimization.

Sponsored links. We may earn a commission at no extra cost to you.

import tracemalloc
import time

# Test with __dict__ (default)
tracemalloc.start()
points_dict = [PointWithDict(i, i*2, i*3) for i in range(1_000_000)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"With __dict__:  {peak / 1024 / 1024:.1f} MB peak")

# Test with __slots__
tracemalloc.start()
points_slots = [PointWithSlots(i, i*2, i*3) for i in range(1_000_000)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"With __slots__: {peak / 1024 / 1024:.1f} MB peak")

# Typical output:
# With __dict__:  247.3 MB peak
# With __slots__:  91.6 MB peak
# Memory saved: 63%

Attribute Access Speed: The Hidden Bonus

import timeit

p_dict  = PointWithDict(1.0, 2.0, 3.0)
p_slots = PointWithSlots(1.0, 2.0, 3.0)

# Benchmark attribute READ
dict_time  = timeit.timeit(lambda: p_dict.x,  number=10_000_000)
slots_time = timeit.timeit(lambda: p_slots.x, number=10_000_000)

print(f"__dict__  read: {dict_time:.3f}s")
print(f"__slots__ read: {slots_time:.3f}s")
print(f"Speedup: {dict_time/slots_time:.2f}x")

# Typical output:
# __dict__  read: 0.412s
# __slots__ read: 0.305s
# Speedup: 1.35x  (35% faster attribute access)

When to Use __slots__ (and When Not To)

Use __slots__ when:

  • Creating thousands or millions of instances of the same class
  • Building data containers (Points, Vectors, Events, Records)
  • Writing performance-critical library code
  • The class has a fixed, known set of attributes

Don’t use __slots__ when:

  • You need to dynamically add attributes at runtime
  • Using multiple inheritance (complex interaction with slots)
  • Working with pickle without explicitly defining __getstate__/__setstate__
  • The class has only a few instances — overhead not worth it

Advanced: __slots__ with Inheritance

class Animal:
    __slots__ = ['name', 'age']
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Dog(Animal):
    # MUST define __slots__ in subclass too
    # Only add NEW attributes — inherited ones already covered
    __slots__ = ['breed']
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

# If you DON'T define __slots__ in the subclass:
class BadDog(Animal):
    pass  # __dict__ comes back! Inherits Animal's slots but adds its own dict

d = Dog('Rex', 3, 'Lab')
# d.__dict__ doesn't exist — fully slot-optimized
b = BadDog('Fido', 2)
# b.__dict__ exists — memory savings lost

__slots__ in the Wild: Where Python Uses This Internally

Python’s own standard library uses __slots__ extensively for performance-critical classes. Check these yourself:

import datetime
print(datetime.datetime.__slots__)
# ('_hashcode', '_datetime')  — datetime uses slots internally

from collections import OrderedDict
# CPython's internal linked list nodes use slots

# Popular libraries that rely on __slots__:
# - attrs (auto-generates __slots__ classes)
# - pydantic v2 (slots=True mode, ~40% faster)  
# - numpy record arrays
# - SQLAlchemy column descriptors

The Modern Alternative: dataclasses with slots=True

from dataclasses import dataclass

# Python 3.10+ — slots=True on dataclasses
@dataclass(slots=True)
class Point:
    x: float
    y: float
    z: float

# Gets you: __slots__ + __init__ + __repr__ + __eq__ automatically
# No boilerplate, full performance benefit
p = Point(1.0, 2.0, 3.0)
print(hasattr(p, '__dict__'))  # False — slots active
print(p)                        # Point(x=1.0, y=2.0, z=3.0)

If you’re on Python 3.10+, use @dataclass(slots=True) — it’s the cleanest way to get slot optimization with zero boilerplate.

Quick Reference: The Slots Cheat Sheet

  • ✅ Define __slots__ as a list or tuple of attribute name strings
  • ✅ Always define __slots__ in every class in an inheritance chain
  • ✅ Use @dataclass(slots=True) on Python 3.10+ for clean syntax
  • ✅ Use attrs library for slots + validation + serialization
  • ❌ Don’t mix __dict__ and __slots__ (defeats the purpose)
  • ❌ Don’t use slots for classes that need dynamic attributes

Next Python deep dive: __del__ vs weakref — the memory leak pattern that bites every Python developer exactly once and is never forgotten.

Related reads on CheatCoders: The memory optimization mindset from __slots__ applies directly to V8’s hidden classes in JavaScript — both exploit type stability for performance gains. If you’re deploying Python services on AWS, the Lambda cold start guide covers Python package size optimization. External resource: Python official __slots__ documentation.

Recommended resources

Disclosure: This post contains affiliate links. If you purchase through these links, CheatCoders earns a small commission at no extra cost to you. We only recommend tools and books we genuinely find valuable.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

3 Comments

Leave a Reply