Difficulty: Advanced
How does Python manage memory? Explain garbage collection.
Python uses a combination of reference counting and a cyclic garbage collector.
Reference counting: Each object tracks how many references point to it. When the count drops to zero, the object is immediately deallocated.
Cyclic GC: Handles circular references that reference counting cannot clean up. It periodically scans for reference cycles and breaks them.
Memory pools: CPython uses a private heap with memory pools (arenas of 256KB, pools of 4KB) for small objects. The pymalloc allocator handles objects up to 512 bytes.
Key tools: sys.getrefcount(), gc module, weakref module, __slots__ for reduced memory.
import sys
# sys.getrefcount() returns count + 1 (the argument itself adds a reference)
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (a + getrefcount arg)
b = a # Another reference
print(sys.getrefcount(a)) # 3
c = [a, a] # Two more references
print(sys.getrefcount(a)) # 5
del b # Remove one reference
print(sys.getrefcount(a)) # 4
# Circular reference problem
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __del__(self):
print(f"Deleting {self.name}")
# Create circular reference
a = Node('A')
b = Node('B')
a.next = b
b.next = a # Circular! Reference count never reaches 0
# Must rely on cyclic GC to clean this up
del a
del b
# Objects are not immediately deleted - GC will handle it later
import gc
gc.collect() # Force garbage collection
Reference counting provides immediate cleanup for most objects. Circular references require the cyclic garbage collector, which runs periodically or on demand with gc.collect().
import sys
# Regular class: uses __dict__ for instance attributes
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y
# Slotted class: fixed attribute set, no __dict__
class SlottedPoint:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
reg = RegularPoint(1, 2)
slot = SlottedPoint(1, 2)
# Memory comparison
print(f"Regular: {sys.getsizeof(reg)} + {sys.getsizeof(reg.__dict__)} (dict) bytes")
print(f"Slotted: {sys.getsizeof(slot)} bytes (no __dict__)")
# slot.z = 3 # AttributeError: 'SlottedPoint' has no attribute 'z'
# Cannot add attributes not in __slots__
# For millions of objects, savings add up
import time
start = time.time()
regular_list = [RegularPoint(i, i) for i in range(100_000)]
print(f"Regular: {time.time() - start:.4f}s")
start = time.time()
slotted_list = [SlottedPoint(i, i) for i in range(100_000)]
print(f"Slotted: {time.time() - start:.4f}s")
__slots__ saves ~50% memory per instance by eliminating __dict__. Use it for classes with many instances (data points, tokens, etc.).
import weakref
import gc
# Weak references don't prevent garbage collection
class ExpensiveObject:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"ExpensiveObject('{self.name}')"
obj = ExpensiveObject('heavy')
weak_ref = weakref.ref(obj)
print(weak_ref()) # ExpensiveObject('heavy')
print(weak_ref() is obj) # True
del obj
print(weak_ref()) # None (object was garbage collected)
# WeakValueDictionary: cache that doesn't prevent GC
cache = weakref.WeakValueDictionary()
def get_or_create(key):
obj = cache.get(key)
if obj is None:
obj = ExpensiveObject(key)
cache[key] = obj
return obj
# GC control
print(f"GC enabled: {gc.isenabled()}")
print(f"GC thresholds: {gc.get_threshold()}")
print(f"GC counts: {gc.get_count()}")
# Disable GC for performance-critical sections
gc.disable()
# ... do performance-critical work ...
gc.enable()
gc.collect() # Manual collection
Weak references let you reference objects without preventing their garbage collection. WeakValueDictionary is perfect for caches. GC thresholds control collection frequency.
Reference Counting, Garbage Collection, Memory Pools, Weak References, __slots__