Garbage Collection & Reference Counting Interview Questions & Answers
6 questions Updated 2026-06-18
Python interview questions on memory management — reference counting, the cyclic garbage collector and reference cycles, sys.getrefcount, weak references, __del__ pitfalls, and generational GC.
Every CPython object carries a reference count — the number of references
pointing at it. The count goes up when you bind a new name, append it to a
container, or pass it to a function, and down when a name is reassigned, goes
out of scope, or is del'd. When the count hits zero, the object is
immediately deallocated.
a = [1, 2, 3] # the list's refcount is 1
b = a # 2 — another reference
del a # 1 — still alive (b references it)
b = None # 0 — list is freed right away
This is deterministic and prompt — memory is reclaimed the instant the last reference disappears, not at some later sweep.
Why it matters: reference counting handles the vast majority of garbage, but it can't free reference cycles on its own, which is why CPython adds a second collector.
Reference counting fails on reference cycles — objects that refer to each other,
so their counts never reach zero even when nothing outside the cycle reaches them.
Without help they'd leak forever. CPython's cyclic garbage collector (the gc
module) periodically finds and frees these unreachable cycles.
import gc
a, b = {}, {}
a['b'] = b # a -> b
b['a'] = a # b -> a (a cycle)
del a, b # both refcounts stay at 1 — NOT freed by refcounting
gc.collect() # the cyclic collector reclaims the unreachable cycle
The collector works by tracking container objects (lists, dicts, instances) and detecting groups whose only references are internal to the group.
Rule of thumb: you rarely call gc.collect() manually — it runs automatically —
but it's worth knowing cycles are collected later, not immediately like refcounts.
sys.getrefcount(obj) returns the object's current reference count. It almost
always reads one higher than you expect because passing the object as the
argument creates a temporary reference for the duration of the call.
import sys
x = []
sys.getrefcount(x) # 2 — one for `x`, one for the argument itself
y = x
sys.getrefcount(x) # 3 — `x`, `y`, and the argument
Small ints and interned strings show huge counts because they're cached singletons shared across the whole interpreter.
Why it matters: it's a debugging/teaching tool for understanding aliasing and leaks — just remember to subtract one for the call's own reference, and don't rely on exact values across implementations.
A weak reference (weakref module) points to an object without increasing its
reference count, so it does not keep the object alive. When the last strong
reference is gone, the object is collected and the weak reference returns None.
import weakref
class Node: pass
n = Node()
r = weakref.ref(n) # weak — doesn't count toward n's lifetime
r() # <Node object> — still alive
del n # last strong ref gone -> object freed
r() # None — referent is dead
They're ideal for caches and observer/parent-child back-references where you
want to reference an object but not prevent its cleanup — weakref.WeakValueDictionary
is a common cache type.
Rule of thumb: use a weak reference to break a cycle or avoid a memory leak when one object should not own the lifetime of another.
__del__ is a finalizer called when an object is about to be destroyed — but
its timing is not guaranteed. It runs only when the refcount hits zero (or later,
via the cyclic collector), so you can't rely on when, or even whether, it
runs. Note del obj only decrements the refcount; it doesn't directly call __del__.
class Resource:
def __del__(self):
print("cleaning up") # may run late, or not at all on interpreter exit
r = Resource()
r2 = r
del r # nothing happens — r2 still references it
del r2 # NOW refcount is 0 -> __del__ runs
Objects in a reference cycle that define __del__ historically couldn't be
collected at all (improved in Python 3.4+ via PEP 442), and exceptions raised inside
__del__ are ignored.
Rule of thumb: don't use __del__ for important cleanup — use context managers
(with) or try/finally, which give deterministic, explicit release.
The cyclic collector is generational: it sorts tracked objects into three generations (0, 1, 2) based on how many collections they've survived. New objects start in generation 0, which is scanned most frequently; survivors are promoted to older generations that are scanned less often.
The idea is the weak generational hypothesis: most objects die young, so it's efficient to focus collection effort on the youngest generation and rarely re-scan long-lived objects.
import gc
gc.get_count() # (gen0, gen1, gen2) allocation counters
gc.get_threshold() # (700, 10, 10) — triggers per generation
gc.collect(0) # collect only generation 0 (cheap, frequent)
A generation-0 collection is fast and common; full (generation-2) collections are rarer and more expensive.
Rule of thumb: this is mostly automatic, but for latency-sensitive code you can tune
thresholds, call gc.collect() strategically, or gc.disable() it if you manage
lifetimes carefully.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.