Welcome to our third lesson. In our previous discussion, we explored the crucial difference between mutable and immutable objects and saw how multiple variable names can refer to the same object—a concept known as aliasing. This naturally leads to a critical question for any language runtime: if multiple names can point to an object, how does Python know when an object is no longer in use and its memory can be safely reclaimed?
This lesson answers that question by diving into CPython's primary memory management strategy: reference counting. You will learn precisely how Python tracks the "liveness" of every object, what events cause an object's reference count to change, and what happens when that count drops to zero. Understanding this mechanism is fundamental to a senior developer's mental model of Python, as it clarifies object lifetimes, the behavior of the del statement, and sets the stage for understanding more complex memory management scenarios.
The Core Mechanism: Counting References
The principle of reference counting is straightforward: every object in Python's memory maintains a counter that tracks how many references (variables, container elements, etc.) are currently pointing to it. This process is automatic and deterministic.
- An object's life begins with a reference count of 1 when it is created.
- The count is incremented whenever a new reference is created (e.g., assignment to a new variable, passing as a function argument, adding it to a list).
- The count is decremented whenever a reference is destroyed (e.g., a variable goes out of scope, is reassigned, or is deleted with
del). - When the reference count drops to zero, the object is no longer accessible. CPython immediately deallocates the object, freeing its memory.
To see this in action, we can define a simple class with a __del__ special method. This method acts as a finalizer, which Python calls just before an object's memory is reclaimed.
import sys
class MyObject:
def __del__(self):
print("MyObject instance is being destroyed!")
# 1. Object is created, refcount starts at 1
obj = MyObject()
print(f"Refcount after creation: {sys.getrefcount(obj)}")
# 2. A new reference is created, refcount is incremented
another_ref = obj
print(f"Refcount after new assignment: {sys.getrefcount(obj)}")
# 3. A reference is destroyed, refcount is decremented
del another_ref
print(f"Refcount after deleting one reference: {sys.getrefcount(obj)}")
# 4. The final reference is destroyed, refcount drops to 0, object is destroyed
print("Deleting the final reference...")
del obj
# The "__del__" message will be printed here
You'll notice sys.getrefcount() reports a count that is one higher than you might expect. This is because passing the object to sys.getrefcount() itself creates a temporary reference for the duration of the function call.
The following video provides a concise demonstration of this lifecycle, showing how an object is garbage collected when its reference count drops to zero.
python: what is weakref? (intermediate - advanced) anthony explains #366
This segment from a video by Anthony Sottile illustrates the basics of reference counting.
Watch from the beginning until the discussion on weak references. Pay attention to how creating new assignments (d = c) increases the reference count and how un-assigning them (del d) decreases it, eventually triggering the __del__ method.
The article below provides a more detailed walkthrough, reinforcing the distinction that del removes a name (a reference), not the object itself. The object is only destroyed as a consequence of its reference count reaching zero.
When You No Longer Need That Object
This article from The Python Coding Stack uses a custom class to clearly demonstrate the relationship between del, reference counts, and object destruction.
Read the sections Reference Counting and Destroying Objects and the subsequent part, which shows the object being destroyed when the last reference is removed.
Under the Hood: The PyObject Struct
Given your background in computer engineering, you'll appreciate that this abstract "reference count" has a very concrete implementation in the CPython source code. Every object in Python is represented at the C level by a struct. The very first member of this struct is always a PyObject header.

This PyObject header contains the reference count field, typically named ob_refcnt. The CPython interpreter uses a pair of macros, Py_INCREF(obj) and Py_DECREF(obj), to perform atomic increment and decrement operations on this ob_refcnt field. When Py_DECREF finds that the count has reached zero, it triggers the deallocation process.
The following reading provides a deeper look into these C-level internals.
How CPython Implements Reference Counting
This article from Coding Confessions dives into the CPython source code to show you exactly how reference counting is implemented.
Read the section CPython Internals of Reference Counting. Focus on the structure of PyObject and the simplified logic of Py_INCREF and Py_DECREF. This connects the high-level concept directly to its low-level implementation.
Reference Counting in a Multithreaded World
A crucial aspect for production systems is thread safety. What happens if two threads try to modify an object's reference count at the same time? For example, one thread assigns it to a new variable (Py_INCREF) while another finishes using it and decrements the count (Py_DECREF). This could lead to a race condition, resulting in a corrupted reference count and either a memory leak or a crash from trying to access a deallocated object.
This is one of the primary problems solved by Python's Global Interpreter Lock (GIL). The GIL is a mutex that ensures only one thread can execute Python bytecode at any given time within a single process. This serializes access to object memory, thereby protecting data structures like reference counts from concurrent modification.

While the GIL simplifies memory management, it has significant performance implications for CPU-bound multithreaded programs, a topic we will explore in detail in the module on Concurrency & Parallelism. For now, it's important to recognize its role in making reference counting thread-safe.
The Limitation: Cyclic References
Reference counting is efficient and deterministic, but it has one major weakness: it cannot handle reference cycles (or cyclic references). A reference cycle occurs when two or more objects refer to each other.
Consider two objects, A and B.
Ahas a reference toB.Bhas a reference toA.
Even if no other variables in our program refer to A or B, their reference counts will each be at least 1 because they are keeping each other "alive." The reference counting algorithm alone will never see their counts drop to zero. These objects become unreachable garbage that reference counting cannot clean up, leading to a memory leak.
Let's see a practical example:
class Node:
def __init__(self, name):
self.name = name
self.neighbor = None
print(f"Node '{self.name}' created.")
def __del__(self):
print(f"Node '{self.name}' destroyed.")
# Create two nodes
n1 = Node("A")
n2 = Node("B")
# Create a reference cycle
n1.neighbor = n2
n2.neighbor = n1
# Get initial reference counts (will be 2: one from the variable, one from its neighbor)
print(f"Refcount for n1: {sys.getrefcount(n1)}")
print(f"Refcount for n2: {sys.getrefcount(n2)}")
print("\nDeleting n1 and n2 variables...")
del n1
del n2
# Notice that the __del__ methods are NOT called!
# The objects are still in memory because n1.neighbor holds a ref to n2
# and n2.neighbor holds a ref to n1. Their refcounts are both 1.
print("\nProgram finished. The objects were not destroyed.")
This scenario is the reason Python's memory management system needs a backup plan. The following article gives a superb, visual explanation of this problem.
When You No Longer Need That Object
This part of the article uses a fun example with Homer and Marge Simpson to build a reference cycle step-by-step.
Read the section Cyclic References and then continue through The Cyclic Garbage Collector section to see how deleting the external variables leaves the objects in a leaked state.
Key Takeaways
- Primary Mechanism: Reference counting is CPython's main strategy for memory management. It's fast, simple, and deterministic.
- How it Works: Every object has a reference count (
ob_refcnt) stored in itsPyObjectheader. The count is incremented for new references and decremented when references are destroyed. - Object Destruction: When an object's reference count drops to zero, CPython immediately calls its finalizer (
__del__) and deallocates its memory. - Thread Safety: The Global Interpreter Lock (GIL) protects reference counts from race conditions in multithreaded programs by serializing bytecode execution.
- Core Limitation: Reference counting alone cannot detect and collect objects involved in reference cycles, which can lead to memory leaks.
You now understand both the power and the critical limitation of Python's primary memory management model. In our next lesson, we will explore Python's solution to this problem: the cyclic garbage collector, a secondary mechanism that periodically runs to find and clean up the unreachable cycles that reference counting leaves behind.