Skip to main content
Python advanced Lesson 18 of 28

Python Memory Management

Understand Python's garbage collection, reference counting, weak references, __slots__, and memory profiling techniques.

How Python Manages Memory

CPython (the reference implementation) uses two complementary mechanisms:

  1. Reference counting — every object tracks how many references point to it. When the count drops to zero, the object is immediately deallocated.
  2. Cyclic garbage collector — handles reference cycles (A → B → A) that reference counting alone cannot collect.
import sys

x = [1, 2, 3]
sys.getrefcount(x)  # 2 (x + the argument to getrefcount)

y = x              # ref count becomes 3
del y              # ref count back to 2
del x              # ref count → 0, object freed immediately

Reference Counting

import ctypes

def ref_count(obj_id):
    """Get reference count by object id (after the variable is deleted)."""
    return ctypes.c_long.from_address(obj_id).value

a = "hello world"   # unique string, not interned
obj_id = id(a)

b = a
print(sys.getrefcount(a))   # 3

del b
print(sys.getrefcount(a))   # 2

del a
# Object is now freed — obj_id is no longer valid

Circular References and the GC

Reference counting cannot collect cycles:

import gc

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

# Create a cycle
a = Node(1)
b = Node(2)
a.next = b
b.next = a   # cycle: a → b → a

del a
del b
# Both objects still alive — their ref counts are 1 (each holds the other)
# The cyclic GC will eventually collect them

The cyclic GC runs periodically (tunable with gc.set_threshold()). It finds unreachable cycles and breaks them.

import gc

# Force a collection
gc.collect()

# Disable for performance-critical sections
gc.disable()
# ... allocate lots of short-lived objects ...
gc.enable()

# Get unreachable objects
unreachable = gc.collect()
print(f"Collected {unreachable} objects")

weakref — References Without Ownership

A weak reference does not increment an object’s reference count. Useful for caches and observer patterns.

import weakref

class ExpensiveResource:
    def __init__(self, name):
        self.name = name
        print(f"Creating {name}")

    def __del__(self):
        print(f"Destroying {self.name}")

obj = ExpensiveResource("Database Connection")
weak = weakref.ref(obj)

print(weak())        # <ExpensiveResource: Database Connection>
print(weak() is obj) # True

del obj
print(weak())        # None — object was collected

WeakValueDictionary for Caches

import weakref

class Cache:
    def __init__(self):
        self._store = weakref.WeakValueDictionary()

    def get(self, key):
        return self._store.get(key)

    def set(self, key, value):
        self._store[key] = value

cache = Cache()
data = SomeLargeObject()
cache.set("key", data)

del data   # WeakValueDictionary does not keep data alive
cache.get("key")   # None — automatically evicted

slots

By default, every Python instance stores its attributes in a __dict__. For classes with many instances, this is wasteful.

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

class PointWithSlots:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

import sys
p1 = PointWithDict(1.0, 2.0)
p2 = PointWithSlots(1.0, 2.0)

print(sys.getsizeof(p1))             # ~48 bytes + 200+ for __dict__
print(sys.getsizeof(p2))             # ~56 bytes, no __dict__
print(hasattr(p1, "__dict__"))       # True
print(hasattr(p2, "__dict__"))       # False

When slots Matters

import tracemalloc

tracemalloc.start()

# Create 1 million instances
points_dict = [PointWithDict(i, i) for i in range(1_000_000)]
snapshot1 = tracemalloc.take_snapshot()

del points_dict

points_slots = [PointWithSlots(i, i) for i in range(1_000_000)]
snapshot2 = tracemalloc.take_snapshot()

# __slots__ version uses roughly 40-50% less memory

Tradeoffs of __slots__:

  • Cannot add arbitrary attributes at runtime
  • Does not work well with multiple inheritance
  • Not inherited unless subclasses also define __slots__

Memory Profiling

tracemalloc (stdlib)

import tracemalloc

tracemalloc.start()

# Code to profile
data = {i: str(i) * 100 for i in range(10_000)}

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")

print("Top 5 memory allocations:")
for stat in top_stats[:5]:
    print(stat)

tracemalloc.stop()

memory_profiler (pip install memory-profiler)

from memory_profiler import profile

@profile
def process_data():
    data = [i**2 for i in range(100_000)]
    result = sum(data)
    del data
    return result

process_data()
# Line-by-line memory usage printed to console

objgraph (pip install objgraph)

import objgraph

# Show the most common types in memory
objgraph.show_most_common_types(limit=10)

# Find what's holding a reference to an object
obj = SomeClass()
objgraph.show_backrefs(obj, max_depth=3)

Generator-Based Memory Efficiency

import sys

# List — loads everything into memory
data_list = [x**2 for x in range(1_000_000)]
print(sys.getsizeof(data_list))     # ~8 MB

# Generator — computes on demand
data_gen = (x**2 for x in range(1_000_000))
print(sys.getsizeof(data_gen))      # ~128 bytes

# Both sum to the same value
sum(data_list) == sum(data_gen)     # True

intern() for String Memory

Python automatically interns short strings that look like identifiers. You can force interning for repeated strings:

import sys

a = "hello"
b = "hello"
a is b   # True — CPython interns this automatically

# Force interning for arbitrary strings
s1 = sys.intern("some long repeated string")
s2 = sys.intern("some long repeated string")
s1 is s2   # True — same object, saves memory when repeated millions of times

Frequently Asked Questions

Does Python have manual memory management?
No. Python uses automatic memory management through reference counting plus a cyclic garbage collector. You rarely need to think about memory, but understanding the model helps you avoid leaks and optimize usage.
What is a memory leak in Python?
The most common causes are unbounded caches, circular references in objects that define __del__, and global state that accumulates data over time.
When do __slots__ actually help?
__slots__ help when you create millions of instances of a class. Each instance without __slots__ carries a __dict__ (typically 200-300 bytes overhead). With __slots__ that overhead drops to almost nothing.