Skip to content

Reference Counting#

CPython manages most object lifetimes with reference counting: every object tracks how many references point to it. When the count drops to zero, the object is destroyed immediately. A separate cycle detector (gc module) handles reference cycles that refcount alone cannot break. Understanding both layers is essential for debugging memory leaks, writing correct __del__ methods, and reasoning about performance.

How to use this page

Pair with Global Interpreter Lock for allocator/thread interactions and Classes and OOP Basics for instance lifecycle. Use gc and weakref in production diagnostics before reaching for external profilers.

At a glance
Track Python Advanced → Memory Management and Performance
Sections 10 major topics
Outline Use the right-hand TOC to jump

Topics: How reference counting works · Immediate deallocation example · Reference cycles and the cycle collector · __del__ pitfalls · Interning and small integer caching · weakref — references without keeping objects alive · Memory views and buffer sharing · Performance implications · … (+2 more)

  1. How reference counting works
  2. Immediate deallocation example
  3. Reference cycles and the cycle collector
  4. __del__ pitfalls
  5. Interning and small integer caching
  6. weakref — references without keeping objects alive
  7. Memory views and buffer sharing
  8. Performance implications
  9. Debugging memory issues
  10. C extensions and refcounting

How reference counting works#

Every Python object has a header with ob_refcnt. Operations that create or destroy references adjust the count:

Operation Effect on refcount
x = obj obj refcount +1
del x obj refcount −1
Pass obj to a function +1 on entry, −1 on return
Store in container lst.append(obj) +1
Remove from container −1
Function returns obj +1 for the returned reference
import sys

class Node:
    def __init__(self, value: int) -> None:
        self.value = value

a = Node(1)
print(sys.getrefcount(a))  # typically 2: `a` + temporary arg to getrefcount

getrefcount adds a temporary reference

The argument to sys.getrefcount(obj) itself increments the count by one during the call. Expect count ≥ 1 for any live object.

When refcount reaches 0, CPython calls the object's destructor (tp_dealloc), which may invoke __del__ if defined, then frees memory.


Immediate deallocation example#

class Resource:
    def __init__(self, name: str) -> None:
        self.name = name

    def __del__(self) -> None:
        print(f"destroyed: {self.name}")

def scope_demo() -> None:
    r = Resource("temp")  # refcount 1
    # r goes out of scope → refcount 0 → __del__ runs

scope_demo()  # prints: destroyed: temp

Destruction is deterministic for acyclic objects — unlike Java's GC, you can often predict when __del__ fires (but should not rely on it for critical cleanup; use with and context managers).


Reference cycles and the cycle collector#

Refcounting cannot reclaim cycles:

class Linked:
    def __init__(self) -> None:
        self.ref: Linked | None = None

a = Linked()
b = Linked()
a.ref = b
b.ref = a
del a, b  # neither refcount reaches 0 — memory leak without gc

CPython's generational garbage collector (gc module) periodically scans for unreachable cycles.

import gc

gc.collect()           # force a collection cycle
print(gc.get_count())  # objects per generation (0, 1, 2)
gc.set_threshold(700, 10, 10)  # tune collection frequency (advanced)
Generation Typical contents Collection frequency
0 New objects Most often
1 Survived one collection Less often
2 Long-lived objects Least often
import gc

class Cycle:
    def __init__(self) -> None:
        self.loop: Cycle | None = None

c = Cycle()
c.loop = c
print(gc.get_referrers(c))  # debugging: who points to c?

Disable gc only with extreme care

gc.disable() avoids collection pauses in latency-sensitive C extensions but risks unbounded growth if your code creates cycles. Re-enable in tests.


__del__ pitfalls#

class Bad:
    def __del__(self) -> None:
        open("log.txt", "a").write("bye\n")  # fragile — interpreter may be shutting down
Problem Why
Order undefined Other globals may already be None
Resurrection __del__ can recreate references, reviving the object
Cycles with __del__ Objects in cycles with __del__ may become uncollectable
import gc

class Leaky:
    def __del__(self) -> None:
        pass

a = Leaky()
b = Leaky()
a.ref = b
b.ref = a
del a, b
print(gc.garbage)  # may list uncollectable cycle if __del__ present

Prefer context managers (with) and explicit .close() over __del__ for resource cleanup. See File Handling.


Interning and small integer caching#

CPython interns some immutable objects for performance:

a = "hello"
b = "hello"
print(a is b)  # True — same object (string interning, compile-time literals)

x = 256
y = 256
print(x is y)  # True — small int cache (−5 to 256)

x = 257
y = 257
print(x is y)  # May be False — separate objects outside cache
Cached range Type
−5 to 256 int
Single-character strings (some cases) str
True, False, None singletons

This affects is vs == interview questions but not refcount correctness — interned objects simply have higher refcounts shared across the runtime.


weakref — references without keeping objects alive#

import weakref

class Cache:
    def __init__(self) -> None:
        self._data: dict[str, weakref.ref] = {}

    def get(self, key: str, factory):
        ref = self._data.get(key)
        obj = ref() if ref else None
        if obj is None:
            obj = factory()
            self._data[key] = weakref.ref(obj)
        return obj
API Use case
weakref.ref(obj) Callable proxy; returns None when obj dies
weakref.WeakValueDictionary Cache that evicts dead values
weakref.WeakSet Set of objects without preventing collection
weakref.finalize(obj, callback) Preferred over __del__ for cleanup
import weakref

def cleanup(name: str) -> None:
    print(f"finalized: {name}")

obj = object()
weakref.finalize(obj, cleanup, "my-obj")
del obj  # cleanup runs when obj is collected

Memory views and buffer sharing#

Objects supporting the buffer protocol (bytes, array, NumPy arrays) can share memory without copying:

data = bytearray(b"hello world")
mv = memoryview(data)
mv[0:5] = b"HELLO"
print(data)  # bytearray(b'HELLO world') — same underlying buffer

Slices of memoryview increment refcounts on the underlying object until the view is released.


Performance implications#

Factor Impact
High churn of short-lived objects Frequent inc/dec overhead; generational gc helps
Large object graphs Traversing refs during collection pauses
__slots__ Fewer per-instance dicts → lower memory
__dict__ on every instance Extra dict per object → higher memory
class Slim:
    __slots__ = ("x", "y")

    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

Use __slots__ when you have millions of homogeneous instances and do not need dynamic attributes.


Debugging memory issues#

import tracemalloc

tracemalloc.start()
# ... run code ...
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:10]:
    print(stat)
tracemalloc.stop()
Tool Purpose
tracemalloc Track allocations by line
gc.get_objects() Enumerate all tracked objects (heavy)
objgraph (third-party) Visualize reference graphs
sys.getsizeof() Shallow size (not recursive)
import sys

d = {"a": [1, 2, 3]}
print(sys.getsizeof(d))       # dict overhead only
# Does not include size of keys/values — use recursive helpers for totals

C extensions and refcounting#

C API functions use Py_INCREF / Py_DECREF. Incorrect balancing causes leaks or premature frees (segfaults). Python code rarely touches this, but extension bugs manifest as "random" crashes under load.

With the GIL (default CPython), refcount updates are not atomic across threads — another reason shared mutable objects across threads need care. See Global Interpreter Lock.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
Relying on __del__ for cleanup Missed on cycles, shutdown races Context managers, weakref.finalize
Assuming del frees memory immediately Only drops a reference Understand refcount semantics
is for value comparison Wrong for integers > 256, constructed strings Use == for equality
Ignoring reference cycles Slow memory leak Break cycles with weakref or redesign
getrefcount off-by-one Misread live references Account for temporary ref
sys.getsizeof for total footprint Reports shallow size only Recursive measurement or profiler

Mental model checklist#

  1. What happens to refcount when you pass an object to a function?
  2. Why can't pure refcounting collect a.ref = b; b.ref = a?
  3. When is __del__ a bad choice for closing files or sockets?
  4. How does weakref.ref(obj)() behave after obj is collected?
  5. What is the small-integer cache range in CPython?
  6. What is the difference between gc.collect() and refcount reaching zero?

What's next#

Topic Page
GIL and threading Global Interpreter Lock
Resource cleanup patterns File Handling
Context managers Error Handling
Advanced file buffering File IO