Python manages memory automatically. We create objects, use them, and Python cleans them up when they’re no longer needed. The primary mechanism is reference counting, backed by a cyclic garbage collector for edge cases.
Reference Counting
Every object in Python has a reference count — the number of names or containers pointing to it. When that count drops to zero, Python immediately frees the memory.
a = [1, 2]
b = a
del a
del b
We can check an object’s reference count with sys.getrefcount():
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (one for 'a', one for the argument to getrefcount)
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2 again
Note: getrefcount() always shows one extra because passing the object to the function temporarily creates another reference.
What Increases the Count?
- Assigning to a variable:
a = obj - Adding to a container:
my_list.append(obj) - Passing as a function argument
- Creating an alias:
b = a
What decreases it: del a, reassigning a = something_else, or the variable going out of scope.
The Circular Reference Problem
Reference counting alone can’t handle this:
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b # a points to b
b.ref = a # b points to a — circular!
del a
del b
# Both refcounts are still 1 (they reference each other)
# Reference counting alone can't free them!
The Generational Garbage Collector
Python’s GC handles circular references using a generational approach. Objects are grouped into three generations:
- Generation 0 — newly created objects. Collected most frequently.
- Generation 1 — survived one collection cycle.
- Generation 2 — long-lived objects. Collected least frequently.
The idea: most objects die young. By checking new objects more often, we save time.
import gc
print(gc.get_count()) # (num_gen0, num_gen1, num_gen2)
print(gc.get_threshold()) # (700, 10, 10) — default thresholds
gc.collect() # manually trigger a full collection
The GC runs automatically when the number of allocations minus deallocations in gen 0 exceeds the threshold (default 700).
__del__ Destructor
We can define __del__ to run cleanup code when an object is about to be destroyed. But it’s rarely used and can cause issues with the garbage collector.
class TempFile:
def __init__(self, name):
self.name = name
print(f"Created {name}")
def __del__(self):
print(f"Deleting {self.name}")
f = TempFile("data.tmp")
del f # Deleting data.tmp
Prefer context managers (with statement) over __del__ for cleanup — they’re more predictable.
Weak References
Sometimes we want to reference an object without preventing it from being garbage collected. That’s what weakref is for.
import weakref
class BigData:
pass
obj = BigData()
weak = weakref.ref(obj)
print(weak()) # <__main__.BigData object ...>
del obj
print(weak()) # None — object was collected
In simple language, Python uses reference counting as its main memory strategy — when nothing points to an object anymore, it’s immediately freed. For tricky cases like circular references, the generational garbage collector steps in and cleans up periodically.