Object Memory and __slots__
15 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Every Python value is an object with a header, and ordinary instances add a dictionary on top. This lesson shows what that costs, how to cut it, and the is-versus-equals rule interviewers ask about.
How big is an object
Shallow size is the memory of one object alone, as sys.getsizeof reports it. It leaves out everything the object refers to. The deep size adds all of that.
Every Python object carries a header with its reference count and its type, so even a small number takes more than one byte. sys.getsizeof reports only the object itself, not the objects it refers to.
import sys
rows = [list(range(1000))]print(sys.getsizeof(rows) < sys.getsizeof(rows[0]))print(sys.getsizeof(rows) < 200)Example 1 · Shallow size hides what an object holds
import sys
names = ["Asha", "Bilal", "Chen", "Dara"]shallow = sys.getsizeof(names)deep = shallow + sum(sys.getsizeof(name) for name in names)print(deep > shallow)print(len(names))True 4
What this code does
getsizeof(names)counts the list and its pointers.- Adding each string gives a closer total.
- For a full measurement of nested data, use
tracemallocor a profiler tool.
The outer list holds one pointer, so it is tiny. The inner list holds a thousand. Exact byte counts change between Python versions, so compare sizes instead of printing them.
Instances keep their attributes in a dictionary
The instance dictionary, __dict__, is where a normal object keeps its attributes by name. It lets you add or remove attributes freely, at the cost of extra memory per object.
A normal object stores its attributes in a dictionary called __dict__. That is flexible, you can add attributes any time, but each object pays for its own dictionary.
class Point: def __init__(self, x, y): self.x = x self.y = y
p = Point(1, 2)print(p.__dict__)p.z = 3print(sorted(p.__dict__))Example 2 · Where attributes live
class Employee: def __init__(self, name, salary): self.name = name self.salary = salary
e = Employee("Ravi", 52000)print(e.__dict__)del e.salaryprint(hasattr(e, "salary"))print(vars(e) is e.__dict__){'name': 'Ravi', 'salary': 52000}
False
TrueWhat this code does
__dict__shows the attributes by name.del e.salaryremoved the key.vars(obj)is the same dictionary.
p.z = 3 simply added a key. Python 3.11 and later store the values of ordinary instances more compactly, but the model stays the same.
__slots__ fixes the attributes
__slots__ is a class attribute that lists the only attribute names its instances may have. Python then stores them in fixed positions and skips the per-object dictionary.
Setting __slots__ tells Python the attribute names in advance. It then stores them in fixed positions and creates no __dict__. Objects get smaller, and attribute access is a little quicker.
class SlotPoint: __slots__ = ("x", "y")
def __init__(self, x, y): self.x = x self.y = y
sp = SlotPoint(1, 2)print(hasattr(sp, "__dict__"))try: sp.z = 3except AttributeError as error: print(type(error).__name__)Example 3 · Measuring the saving of __slots__
import tracemalloc
class Plain: def __init__(self, a, b, c): self.a, self.b, self.c = a, b, c
class Slim: __slots__ = ("a", "b", "c")
def __init__(self, a, b, c): self.a, self.b, self.c = a, b, c
def measure(cls): tracemalloc.start() items = [cls(1, 2, 3) for _ in range(20000)] used, _ = tracemalloc.get_traced_memory() tracemalloc.stop() return used
print("slots use less memory:", measure(Slim) < measure(Plain))slots use less memory: True
What this code does
tracemalloccounts the memory Python allocates while it runs.- The same 20,000 objects are built with and without slots.
- The saving is real when you hold millions of small objects. For a few objects it does not matter.
The price is flexibility: no new attributes, and no weak references unless you list __weakref__. A subclass also gets a __dict__ again unless it defines its own __slots__.
Identity, equality and shared small objects
Identity (is) means two names refer to one object. Equality (==) means two objects hold the same value, as defined by their __eq__ method. Equal objects need not be the same object.
== asks whether two objects have equal values. is asks whether they are the very same object. CPython shares some small integers and strings, which can make is look like it works when it does not.
a = int("1000")b = int("1000")print(a == b, a is b)
x = [1, 2]y = xz = [1, 2]print(x is y, x == z, x is z)Example 4 · is versus == with a custom __eq__
class Anything: def __eq__(self, other): return True
obj = Anything()print(obj == None)print(obj is None)print(obj is obj)True False True
What this code does
==calls__eq__, which this class answers withTruefor everything.is Nonecannot be changed by the class.- That is why
is Noneis the safe way to test forNone.
Use is for None, True and False only. Never use it to compare numbers or text: caching is an implementation detail and can change between versions.
Next step
Check your understanding
11 quick questions