Python __slots__: Reduce Memory Usage by 40%
Imagine a parking lot. A normal Python object is like an open field where cars can park anywhere. Flexible, but wasteful — you need a huge space even if only a few cars show up.
`__slots__` is like a parking lot with reserved spots. You decide exactly how many spots you need ahead of time. Less space is wasted, and cars find their spot faster.
In this tutorial, you'll learn how Python stores object attributes, why __slots__ saves memory, and when it's worth using. Let's peek under the hood.
How Does Python Store Object Attributes?
Every normal Python object carries a hidden dictionary called __dict__. This dictionary maps attribute names to their values. When you write obj.name = 'Alice', Python is really doing obj.__dict__['name'] = 'Alice'.
This flexibility is nice — you can add any attribute at any time. But dictionaries use a lot of memory. Each dictionary needs space for hash tables, pointers, and room to grow.
What Does __slots__ Do?
When you define __slots__ in a class, Python skips creating the __dict__ dictionary. Instead, it reserves a fixed amount of memory for exactly the attributes you listed.
The __slots__ variable is a tuple (or list) of strings. Each string is the name of an attribute your objects are allowed to have. Python allocates memory for exactly those attributes and nothing more.
How Much Memory Does __slots__ Actually Save?
The savings depend on how many objects you create. For a single object, the difference is tiny. For thousands or millions, it adds up fast.
The exact numbers vary by Python version, but slotted objects are typically 40-60% smaller. Multiply that by a million objects and you're saving hundreds of megabytes.
What Are the Limitations of __slots__?
There are a few important gotchas to know about before using __slots__ in your code.
1. No dynamic attributes. You can only use the attributes listed in __slots__. We saw this error earlier.
2. Inheritance gets tricky. If a parent class uses __slots__, the child class needs its own __slots__ too. Otherwise the child gets a __dict__ and you lose the memory savings.
3. No multiple inheritance with conflicting slots. If two parent classes both define __slots__ with non-empty tuples, you'll get an error unless one of them has empty slots.
4. No default values in __slots__. You can't write __slots__ = ('name=Alice',). Use __init__ to set defaults instead.
When Should You Use __slots__?
Use __slots__ when you create many instances of a class and the attributes are known ahead of time. Common examples include data points, game entities, network packets, and ORM models.
Skip __slots__ for classes that need dynamic attributes, use complex inheritance, or are only instantiated a few times. The memory savings won't matter, and you'll just make your code harder to work with.
class Config:
def __init__(self):
self.debug = False
c = Config()
c.verbose = True # Works fine
c.log_file = 'app.log' # Also fine
print(c.__dict__)class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# Create 1000 points efficiently
points = [Point(i, i*2) for i in range(1000)]
print(f'Created {len(points)} points')Practice Exercises
Create a class called Pixel with __slots__ for three attributes: x, y, and color. Write an __init__ method that accepts all three. Create a Pixel at position (10, 20) with color 'red' and print its color.
Read the code carefully. What will be printed? Think about what happens when you try to set an attribute that is not listed in __slots__.
This code defines a Student class with __slots__ but crashes when setting the grade attribute. Fix the __slots__ definition so all three attributes work. The output should print the student's name, age, and grade.
Create a base class Shape with __slots__ = ('name',) and an __init__ that sets self.name. Then create a child class Circle that adds a radius slot. Its __init__ should take radius, call super().__init__('Circle'), and set self.radius. Create a Circle with radius 5, then print its name and radius on separate lines.
Create two classes: RegularVec (no slots) and SlottedVec (with slots). Both should have x and y attributes. Use sys.getsizeof() to measure the size of one instance of each (for RegularVec, add the size of its __dict__). Print 'Regular: X bytes' and 'Slotted: Y bytes' where X and Y are the actual sizes. Then print 'Slotted is smaller' if the slotted version uses less memory.