Reference Counting

CPython tracks, for every object, how many references currently point to it — and reclaims the object's memory the moment that count hits zero.

Reference counting is CPython's primary memory-reclamation strategy: every object carries a running count of how many references currently point to it, incremented whenever a new reference is made and decremented whenever one goes away. When the count reaches zero, nothing in the program can possibly reach the object anymore, so CPython deallocates it right then. This is a CPython implementation detail — the Python language only guarantees that unreachable objects are eventually reclaimed, not by what mechanism.

What Reference Counting Tracks

CPython implementation detail: every Python object has, as part of its C-level header, an integer field recording how many references to it currently exist — variables bound to it, container slots holding it, attributes pointing to it, and even temporary references held during a function call. This is the object's reference count.

sys.getrefcount() is itself a CPython-specific introspection tool and always reports one more than you might intuitively expect, because passing x as an argument to getrefcount() creates one extra temporary reference for the duration of the call.

import sys

x = [1, 2, 3]
print(sys.getrefcount(x))  # includes: x itself, PLUS the temporary
                            # argument reference getrefcount() holds —
                            # so the number is always at least 2, not 1

Increments and Decrements Conceptually

Conceptually — without needing to know CPython's exact C-level function names — a reference count goes up whenever a new binding to the object is created, and goes down whenever a binding is removed or reassigned.

Action                                     Effect on refcount
------------------------------------------  -------------------
a = SomeObject()                            +1 (name 'a' now refs it)
b = a                                       +1 (name 'b' now refs it too)
container.append(a)                         +1 (container slot refs it)
del b                                       -1 (name 'b' no longer refs it)
a = something_else                          -1 (name 'a' now refs a
                                              different object instead)
container.clear()                           -1 (container slot released)
function ends, local variable goes         -1 for each local name that
out of scope                                referenced the object
class Tracked:
    def __del__(self):
        print("deallocated")

def demo():
    t = Tracked()      # refcount reaches 1
    return None         # t goes out of scope when the function returns

demo()
print("after demo() returned")
# "deallocated" prints before "after demo() returned" — the object's
# refcount hit zero the instant the local variable t disappeared

Immediate Deallocation in the Common Case

The headline behavior reference counting gives you: in the common case (no reference cycles involved), an object is deallocated the instant its last reference disappears — not at some later, unpredictable garbage-collection pause. This is why, in ordinary CPython code, resources often appear to be cleaned up 'immediately' when a variable goes out of scope, even without an explicit context manager (though relying on this is still bad practice, since it's implementation-specific behavior).

class NoisyFile:
    def __init__(self, name):
        self.name = name
        print(f"opened {name}")
    def __del__(self):
        print(f"closed {name}".format(name=self.name))

def use_file():
    f = NoisyFile("data.txt")
    print("using file")
    # f goes out of scope here

use_file()
print("after use_file()")
# opened data.txt
# using file
# closed data.txt          <- happens immediately, before this line runs
# after use_file()

What Reference Counting Cannot Handle Alone

Reference counting has one fundamental blind spot: reference cycles. If object A references object B and B references A (directly or through a longer chain), each keeps the other's count above zero forever, even after nothing outside the cycle can reach either of them.

This gap is exactly why CPython supplements reference counting with a second mechanism, the cyclic garbage collector, covered in its own dedicated topic — reference counting and cyclic GC work together, not as alternatives to each other.

class Node:
    def __init__(self, name):
        self.name = name
        self.partner = None

a = Node("a")
b = Node("b")
a.partner = b
b.partner = a       # a cycle: a -> b -> a

del a
del b
# Neither Node's refcount reaches zero from this alone — each Node is
# still referenced by the other. Reference counting cannot reclaim
# this cycle by itself; that job belongs to the separate cyclic
# garbage collector (see the Cyclic Garbage Collection topic).

Implementation-Specific Nature — Not a Language Guarantee

It is essential to keep this distinction sharp: reference counting, and the 'refcount hits zero -> immediate __del__ call' behavior it produces, is a property of CPython specifically. The Python language specification does not require any particular implementation to use reference counting at all — it only requires that unreachable objects eventually get reclaimed.

Code that depends on __del__ running at a precise, predictable moment (to release a lock, flush a buffer, close a socket) is implicitly depending on CPython's reference-counting behavior — and will behave differently, sometimes badly, on other implementations. Always prefer explicit cleanup via context managers (with statements) over relying on deallocation timing.

Implementation   Uses reference counting?   __del__ timing
---------------  -------------------------  ------------------------------
CPython          Yes (+ cyclic GC backup)   Usually immediate on last-ref-drop
PyPy             No (tracing GC)            Deferred, unpredictable timing
Jython/IronPython Host platform's GC        Deferred, unpredictable timing

Reference Counting's Cost: A Trade-off, Not a Free Lunch

Reference counting is not free — every reference creation and destruction requires an increment or decrement operation, which adds constant overhead throughout a running program, and traditional CPython builds have historically tied this bookkeeping to the Global Interpreter Lock (GIL) for thread safety. Ongoing CPython work (e.g. PEP 703's free-threaded builds) explores alternative approaches, but that is an evolving CPython implementation area, not something the base language guarantees or requires you to design around today.

Observing Reference Counts

sys.getrefcount() is the standard way to peek at an object's current count in CPython, useful mainly for debugging reference-lifetime questions — never for production logic, since the exact numbers are an implementation detail that can shift between Python versions.

import sys

data = [1, 2, 3]
print(sys.getrefcount(data))   # baseline (includes getrefcount's own temp ref)

holder = data
print(sys.getrefcount(data))   # one higher — 'holder' is a new reference

del holder
print(sys.getrefcount(data))   # back down — 'holder' binding removed

Mental Model

Think of each object as holding a small counter, like a library book with a tally of how many people currently have it checked out on their card. Every time someone (a name, a container slot, a function frame) borrows it, the tally goes up; every time they return it, the tally goes down. The moment the tally hits zero, the library reshelves the book immediately — except for one flaw: two people who each hold a book the other one wants can keep each other's tally above zero forever, which is exactly the reference-cycle problem the cyclic collector exists to solve.

Quick Reference

sys.getrefcount(obj)     -> current refcount (CPython-only; +1 for the call itself)
Refcount +1               new name/container/attribute references the object
Refcount -1               a reference is removed/reassigned/goes out of scope
Refcount reaches 0         object deallocated immediately (CPython, non-cyclic case)
Reference cycles           NOT reclaimed by refcounting alone; needs cyclic GC
Language guarantee          "unreachable objects eventually reclaimed" — NOT
                            "reference counting" specifically

Key Takeaways

Reference counting tracks, per object, how many references currently point to it — a CPython implementation detail, not a Python language guarantee.

References are created by assignment, container storage, attribute binding, and function calls, and removed by del, reassignment, or scope exit.

When an object's refcount reaches zero, CPython deallocates it immediately (in the common, non-cyclic case) — this immediacy is exactly what makes 'refcounting' distinct from a batching tracing collector.

Reference counting alone cannot reclaim reference cycles — two or more objects referencing each other keep each other's counts above zero forever without help.

Never write code that depends on precise deallocation timing (e.g. for resource cleanup); use context managers instead, since other Python implementations don't use reference counting at all.

import sys x = [] print(sys.getrefcount(x)) # "should be 1" -- but prints 2, and confusion follows import sys x = [] count = sys.getrefcount(x) # remember: this always includes +1 for the temporary argument # reference held by getrefcount() itself during the call print(count) sys.getrefcount() always reports one more than the 'intuitive' count because passing the object as an argument to the function creates a temporary extra reference; don't treat the raw number as the count of references your code actually holds.

class Resource: def __del__(self): self.release_lock() def use_resource(): r = Resource() do_work(r) # relying on r going out of scope to release the lock "immediately" class Resource: def __enter__(self): return self def __exit__(self, *exc_info): self.release_lock() def use_resource(): with Resource() as r: do_work(r) # lock is released deterministically here, on every Python implementation

Relying on __del__ firing at a precise moment depends on CPython's reference-counting behavior specifically; other implementations delay collection unpredictably. Use __enter__/__exit__ with a with block for deterministic resource release.

a = Node() b = Node() a.next = b b.prev = a del a del b # assuming both Nodes are freed immediately because "refcounting is immediate" import gc a = Node() b = Node() a.next = b b.prev = a del a del b gc.collect() # the cyclic collector, not refcounting, reclaims this cycle

A reference cycle keeps each object's count above zero even after external references are dropped; reference counting alone cannot reclaim it — only the separate cyclic garbage collector (run periodically, or explicitly via gc.collect()) can.

In plain terms, what does an object's reference count represent, and is it a Python language guarantee or a CPython implementation detail?

It represents the number of currently existing references (from names, containers, attributes, or temporary call-frame bindings) to that object; it is a CPython implementation detail — the Python language only guarantees that unreachable objects are eventually reclaimed, not that reference counting is the mechanism used.

Why does sys.getrefcount(x) typically report a number one higher than you'd naively expect?

Because calling sys.getrefcount(x) passes x as an argument to the function, which itself creates one additional temporary reference to x that exists for the duration of the call — so the reported count always includes that extra +1.

What happens to an object's memory in CPython the instant its reference count reaches zero (in the non-cyclic case)?

CPython deallocates it immediately, right at that point in the program's execution, rather than waiting for a later batch collection pass — this immediate reclamation on last-reference-drop is the defining behavior of reference counting.

Why can't reference counting alone reclaim two objects that reference each other in a cycle, even after nothing outside the cycle refers to either of them?

Each object in the cycle still has a reference from the other object in the cycle, so neither one's count ever reaches zero purely from refcounting bookkeeping, even though the pair as a whole has become unreachable from the rest of the program; only a separate mechanism that can detect unreachable cycles (the cyclic garbage collector) can reclaim them.

Why is it risky to write a __del__ method that releases a lock or closes a socket, assuming it will run at a predictable moment?

Because immediate deallocation on last-reference-drop is a CPython-specific behavior of reference counting, not a language guarantee; on implementations without reference counting (e.g. PyPy's tracing collector), __del__ may run much later or with different timing, so relying on it for deterministic cleanup can leak locks or sockets — use a context manager (with/__enter__/__exit__) instead.

List two distinct events that increment an object's reference count and two that decrement it.

Increments: binding a new name to the object (b = a) and storing it in a container (list.append(a)) or as an attribute. Decrements: deleting a name (del b), reassigning a name to a different object, and a local variable going out of scope when its function returns.

Write a small demonstration using sys.getrefcount that shows a list's reference count increasing when it is stored in a container and decreasing again when it is removed.

import sys data = [1, 2, 3] baseline = sys.getrefcount(data) print("baseline:", baseline) holder = [] holder.append(data) print("after append:", sys.getrefcount(data)) # baseline + 1 holder.pop() print("after pop:", sys.getrefcount(data)) # back to baseline

Appending data to holder creates a new reference (the container slot), raising the count by one above the baseline sys.getrefcount already includes (which itself has a +1 for its own temporary argument reference); popping it back out removes that reference, returning the count to baseline.

In what order do the print statements execute, and why doesn't 'bye' print right after the first del a?

Output order: 'still here?', 'bye', 'after f()'. del a only removes the name a; b still references the same Loud object, so its refcount is still above zero. Only when del b removes the last reference does the count reach zero, triggering __del__ immediately at that point — which happens before f() returns.

Write two classes A and B that reference each other (A.partner = b, B.partner = a) to form a reference cycle, delete both names, then use the gc module to show the cycle is only reclaimed after an explicit gc.collect() call (using __del__ prints to observe timing).

import gc gc.disable() # disable automatic cyclic collection for this demo class A: def __del__(self): print("A deallocated") class B: def __del__(self): print("B deallocated") a, b = A(), B() a.partner = b b.partner = a del a, b print("after del - nothing printed yet, because it's a reference cycle") gc.collect() print("after gc.collect() - the cycle has now been reclaimed")

With automatic cyclic collection disabled, deleting both names removes the only external references, but a and b still reference each other, so refcounting alone cannot reclaim either object — no __del__ output appears. Calling gc.collect() explicitly runs the cyclic collector, which detects the unreachable cycle and reclaims both objects, printing their __del__ output at that point.

class Loud:
    def __del__(self):
        print("bye")

def f():
    a = Loud()
    b = a
    del a
    print("still here?")
    del b

f()
print("after f()")