datarekha
Python Medium Asked at GoogleAsked at MetaAsked at Palantir

How does Python manage memory — reference counting and the garbage collector?

The short answer

CPython uses reference counting as its primary mechanism: every object carries a counter of how many names or containers point to it, and the object is freed immediately when the count hits zero. Because reference counting cannot detect cycles, Python also runs a cyclic garbage collector that periodically finds and collects groups of objects that refer only to each other.

How to think about it

This question has two halves, and interviewers want both. Half one is reference counting — the fast, deterministic mechanism that does almost all the work. Half two is the cyclic garbage collector, which exists for one reason: reference counting has a single, well-known blind spot.

Reference counting

Every Python object carries an ob_refcnt field. Binding a name, appending it to a list, passing it as an argument — each increments the count; letting one of those go decrements it. The instant the count hits zero, CPython runs the object’s deallocator — no GC pause, no delay, the memory comes back right away.

import sys

x = []
print(sys.getrefcount(x))   # 2: x itself + the temporary getrefcount argument

y = x
print(sys.getrefcount(x))   # 3

del y
print(sys.getrefcount(x))   # back to 2

sys.getrefcount always reads one higher than you’d expect, because passing the object into the function is itself a reference. Picture it as names pointing at one object:

name "x" ──► list object  [ ob_refcnt = 2 ]
name "y" ──► (the same object)

del y  →  ob_refcnt drops to 1
del x  →  ob_refcnt drops to 0  →  deallocator runs immediately

The blind spot: cycles

Reference counting can’t free a cycle:

a = []
b = [a]
a.append(b)     # a points at b, b points at a
del a, b        # both refcounts drop to 1, not 0 — leaked without a second mechanism

After del a, b, neither object is reachable from your code, yet each one’s count is stuck at 1 because they hold each other. On reference counting alone they’d leak forever. So CPython adds a generational mark-and-sweep collector that scans container objects (lists, dicts, instances), finds these unreachable islands, and frees them. Objects that survive a sweep are promoted to older generations and scanned less often — most objects die young, which makes that strategy cheap.

import gc

gc.collect()           # force a full collection
print(gc.get_count())  # (young, middle, old) tallies

What you can actually control

  • gc.disable() — worth it in a long-running server if you can prove there are no cycles; it removes GC pauses entirely.
  • __slots__ — drops the per-instance __dict__, saving dozens of bytes per object at scale.
  • weakref — a reference that doesn’t bump ob_refcnt, which is how you point at something without keeping it alive (and how you break cycles on purpose):
import weakref

class Node:
    def __init__(self, value):
        self.value = value

n = Node(42)
ref = weakref.ref(n)
print(ref())   # the Node — still alive
del n
print(ref())   # None — the last real reference dropped, so the object is gone
Learn it properly The GIL

Keep practising

All Python questions

Explore further

Skip to content