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.