How does Python's reference counting work for memory management?
Reference counting is CPython's primary memory management mechanism. Every Python object has an internal reference count that tracks how many references point to it.
How it works:
- Increment: When a new reference is created (assignment, function argument, adding to a container)
- Decrement: When a reference is deleted (
del), goes out of scope, or is reassigned - Deallocation: When the count reaches zero, the object is immediately freed
Advantages:
- Deterministic: Objects are freed immediately when no longer referenced
- Simple: Low overhead for the common case
- Predictable:
__del__is called promptly
Limitations:
- Cannot handle reference cycles (A references B, B references A)
- Overhead of maintaining counts on every reference change
- Not thread-safe without the GIL
Object lifecycle:
x = SomeObject() # refcount = 1
y = x # refcount = 2
z = [x] # refcount = 3
del y # refcount = 2
z.pop() # refcount = 1
del x # refcount = 0 -> FREED immediately
import sys
# sys.getrefcount returns the reference count
# Note: passing to getrefcount itself creates a temporary reference (+1)
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (a + getrefcount argument)
b = a # new reference
print(sys.getrefcount(a)) # 3 (a + b + getrefcount argument)
c = [a, a, a] # three more references
print(sys.getrefcount(a)) # 6 (a + b + 3 in list + getrefcount)
del b
print(sys.getrefcount(a)) # 5
c.clear()
print(sys.getrefcount(a)) # 2 (a + getrefcount)
# Demonstrating immediate deallocation
class Tracked:
def __init__(self, name):
self.name = name
print(f"Created {self.name}")
def __del__(self):
print(f"Destroyed {self.name}")
print("--- Creating ---")
obj = Tracked("obj1")
print("--- Replacing ---")
obj = Tracked("obj2") # obj1 is freed immediately (refcount -> 0)
print("--- Deleting ---")
del obj # obj2 is freed immediately
print("--- Done ---")
# Reference cycle (refcount never reaches 0)
class Node:
def __init__(self, name):
self.name = name
self.next = None
a = Node("A")
b = Node("B")
a.next = b # A -> B
b.next = a # B -> A (cycle!)
# Even after deleting a and b, the cycle keeps refcount > 0
# The cyclic garbage collector must handle this
del a, b # objects not freed! (refcount = 1 each due to cycle)
- •Every object has an internal reference count tracking pointers to it
- •Objects are freed immediately when refcount reaches zero
- •sys.getrefcount() returns the count (adds 1 for the argument itself)
- •Reference counting cannot handle cyclic references -- needs cyclic GC
- •sys.getrefcount() always shows one more than expected (the argument reference)
- •Reference cycles prevent refcount from reaching zero -- leads to leaks without cyclic GC
- •__del__ timing depends on reference counting -- not guaranteed in all Python implementations
- •Interned objects (small ints, string literals) have artificially high refcounts