Reference Counting in CPython: How It Works

Reference Counting in CPython: How It Works — cover image

Reference Counting in CPython, Explained

Key Highlights

One of CPython's fundamental memory management techniques is reference counting. It keeps track of object references and has the ability to deallocate an object when its reference count drops to zero.

References can be produced by assignments, aliases, containers, function parameters, closures, and temporary operations. Rebinding, deleting names, removing objects from containers, and terminating a function frame are ways to get rid of them.

Reference counting can quickly manage a large number of objects, but it is unable to recover unreachable reference cycles on its own. Thus, cyclic trash collection is also used in CPython.

The key idea is:

Objects are kept alive by references; an object may become reclaimable if the final reference is removed.

Introduction

What happens when the last name pointing to a Python object disappears?

data = [10, 20, 30]
alias = data

data = None

The list is still alive because alias still refers to it. Once alias is also removed, CPython can reclaim the list if no other references exist.

This is reference counting in action. CPython tracks references to objects, not simply variable names. Names, containers, function calls, closures, and temporary operations can all create references. Understanding how those references increase, decrease, and eventually reach zero explains how CPython manages object lifetime, and why reference cycles require garbage collection.

Names, References, and Objects

Names, References, and Objects

Before discussing the counter itself, separate three concepts.

items = [1, 2, 3]

Conceptually:

items ───────▶ list object
                               [1, 2, 3]

items is a name bound to the list object. It is not a box containing the list.

This is the foundation of Names and References.

Now:

other = items

The list is not copied.

Instead:

items ─────┐
                      ▼
                list object
                      ▲
other ─────┘

The same object is now referenced more than once. Because reference counting keeps track of those references, this distinction is important.

What Is Reference Counting?

What Is Reference Counting?

In CPython, an object has reference-count information associated with it.

A simplified mental model is:

object
   │
   └── reference count

When another reference to that object is created, the count increases. When a reference is released, the count decreases.

Conceptually:

new reference
      ↓
count increases

reference removed
      ↓
count decreases

count reaches zero
      ↓
object can be deallocated

The reference chapter describes this as the core mechanism behind CPython's immediate cleanup of many objects.

CPython offers methods for gaining and releasing strong references at the C API level, such as Py_INCREF() and Py_DECREF(). If the count is zero, Py_DECREF() starts the object's deallocation process.

Python programmers normally never call these functions directly.

Why Does CPython Use Reference Counting?

Python creates and discards objects constantly:

numbers = [1, 2, 3]
name = "Ada"
result = 10 + 20
data = {"active": True}

Memory occupied by objects that are no longer needed eventually has to become available again.

Reference counting provides a direct question:

Does this object still have references?

If the answer becomes "no" and its reference count reaches zero, CPython can generally deallocate the object immediately.

This gives CPython an important characteristic: many objects do not need to wait for a later garbage-collection pass once their last reference disappears.

There is a trade-off, though. Reference counts need to be maintained as references are created and released, and reference counting alone cannot deal with cycles.

What Increases an Object's Reference Count?

Several operations can generate references.

1. Assignment

items = []

A name now refers to the list.

Then:

other = items

creates another reference to the same list.

The important point:

other = items

does not mean:

copy list

It means:

create another reference

The source chapter explicitly uses this example to distinguish assignment from copying.

2. Storing an Object in a Container

References are kept in containers themselves.

name = "Ada"
items = [name]

Conceptually:

name ───────────▶ "Ada"

items ──────────▶ list
                    │
                    └──▶ "Ada"

As a result, both the name and the list relate to the string.

The same principle applies to:

  • Lists
  • Tuples
  • Dictionaries
  • Sets
  • Other container objects

Removing an object from the container removes that container-held reference.

Dictionaries

A dictionary holds references to both its keys and values.

key = "name"
value = "Ada"
data = {key: value}

Conceptually:

key ───────▶ "name"
value ─────▶ "Ada"

data ──────▶ dict
                             ├── key reference ───▶ "name"
                             └── value reference ─▶ "Ada"

When an entry is deleted, all references to those objects are removed from the dictionary.

Assignment vs Rebinding

Many explanations become confusing at this point.

Consider:

items = []
other = items

The two names refer to the same list.

Now:

items = None

The name items is rebound.

Before:

items ─────┐
                      ▼
                     list
                     ▲
other ─────┘

After:

items ──▶ None

other ──▶ list

The old list loses one reference, but it does not disappear because other still refers to it.

This is why:

items = []
other = items

items = None

print(other)

still prints:

[]

Rebinding is therefore different from simply "changing the value of a variable." It changes which object the name refers to.

What Does del Actually Do?

Consider:

items = []
alias = items

del items

The name binding is removed when you del items. It does not immediately destroy the list.

The list remains accessible through:

alias

Only after:

del alias

And, assuming no additional references exist, can the list's reference count be zero?

So the accurate mental model is:

del removes a reference; it does not directly mean "delete the object from memory."

Function Calls Temporarily Add References

Function arguments provide another important reference.

def show(items):
    print(items)

values = []
show(values)

During the call, conceptually:

caller frame
└── values ──▶ list

show frame
└── items ───▶ same list

The parameter items is another reference to the same object while the function is running.

Both the function's frame and the parameter reference vanish when it returns.

But:

values

still exists.

As a result, the list is still active.

Because of this, function calls have the ability to momentarily alter reference counts without altering the object's identity or making a copy.

Why Returned Objects Survive Function Calls

Consider:

def make_list():
    items = [1, 2, 3]
    return items

result = make_list()

During the function call:

make_list frame
└── items ───▶ list object

When the function returns, that local name disappears.

However, the following now refers to the returned object:

caller frame
└── result ──▶ same list object

So the object survives even though the function's frame does not.

This demonstrates an important distinction:

A function's local name can disappear while the object it referred to remains alive.

If the caller ignores the result:

make_list()

the returned list may become unreachable immediately after the relevant temporary reference disappears.

Temporary Objects Also Affect References

Python expressions are capable of producing transient objects.

For example:

result = [1, 2] + [3, 4]

Conceptually, Python creates:

[1, 2]
   +
[3, 4]
   ↓
[1, 2, 3, 4]

The intermediate objects may become unreachable after the expression finishes.

These short-lived references are one reason you should not try to predict an exact reference count by simply counting visible variable names.

What Happens When the Reference Count Reaches Zero?

Suppose:

def make():
    data = [1, 2, 3]

make()

The list is referenced by the local name data.

When make() returns:

The function frame goes away.

The local reference to the list disappears.

If no other references exist, the reference count reaches zero.

CPython can deallocate the object.

At the C level, releasing a reference with Py_DECREF() can trigger the object's type-specific deallocator when the count reaches zero. That deallocator handles the object's destruction and can release references held by compound objects.

This is why CPython often appears to clean up simple unreachable objects immediately.

However, object memory cleanup is not the same thing as external resource management.

For files, sockets, database connections, and similar resources, use explicit mechanisms such as:

with open("data.txt") as file:
    data = file.read()

Do not depend on reference-count timing for resource cleanup.

Why Reference Counting Cannot Handle Cycles

Reference counting has a fundamental limitation.

Consider:

a = []
b = []

a.append(b)
b.append(a)

Now:

a ──▶ List A ──▶ List B
                 ▲              │
                 └──────┘

Remove the names:

del a
del b

Direct names are no longer getting to the lists.

But the lists still reference each other.

Therefore:

List A → List B
List B → List A

The reference count for each object stays above zero.

Reference counting sees references and therefore cannot conclude that the objects are unreachable as a group.

This is cyclic garbage.

Reference Counting and Garbage Collection

CPython therefore uses another mechanism alongside reference counting: cyclic garbage collection.

The division is easier to understand this way:

Reference counting
        ↓
object loses its last reference
        ↓
count reaches zero
        ↓
can be deallocated immediately


Cyclic garbage collection
        ↓
objects still reference one another
        ↓
reference counts stay non-zero
        ↓
but the group is unreachable
        ↓
cycle detector can reclaim it

Many simple scenarios are handled rapidly using reference counting. The situations that reference counting is unable to resolve on its own are handled via cyclic garbage collection.

The gc module exposes tools such as:

import gc
gc.collect()

But manually calling gc.collect() should not be treated as a normal replacement for Python's automatic memory management.

Why sys.getrefcount() Can Be Surprising

CPython exposes:

import sys

items = []

print(sys.getrefcount(items))

You might expect the result to represent exactly the number of references you can see.

It does not.

The call itself:

sys.getrefcount(items)

passes items as an argument, creating an additional temporary reference for the call. That is why the reported number is generally one higher than the count you might intuitively expect.

For example:

items = []
alias = items

print(sys.getrefcount(items))

The exact number is not something your application should depend on.

Modern CPython also has immortal objects, whose reference counts can be intentionally very large and do not represent the ordinary number of live references. The official documentation therefore warns that reference counts are not stable, well-defined values to rely on across versions.

Use sys.getrefcount() for learning and debugging, not application logic.

Reference Counting and Closures

A function can keep an object alive even after the function that created the object has returned.

def make_holder():
    data = []

    def add(value):
        data.append(value)
    return add

holder = make_holder()

The make_holder() frame vanishes. However, data persists because a reference to it is kept in the closure of the returned function.

Conceptually:

holder
  │
 ▼
function object
  │
 ▼
closure
  │
 ▼
data list

Reference counting rules are not different in that case. The list lives on because another reference exists.

Containers Can Keep Objects Alive

Consider:

items = []

items.append({"name": "Ada"})

The dictionary does not have its own name.

The dictionary is still alive, tho:

items ──▶ list
                    │
                    └──▶ {"name": "Ada"}

The list itself holds the reference.

If you then do:

items.pop()

and nothing else refers to that dictionary, it can become reclaimable.

This is why long-lived containers matter when investigating memory usage.

A cache, registry, or global collection can keep objects alive simply because it continues to reference them.

Reference Counting: Benefits and Trade-Offs

Benefit

Trade-off

Many objects can be reclaimed immediately

Reference counts must be updated

Object lifetime can often appear predictable in CPython

Exact behavior is implementation-specific

Simple unreachable objects need not wait for a later GC pass

Cycles require another mechanism

Works directly with CPython's object model

Reference-management work has runtime cost

These trade-offs are why reference counting is best understood as one part of CPython's memory-management system, not the entire system.

CPython Detail vs Python Language Rule

This distinction is essential.

Python the language defines objects, names, references, scopes, and other semantics.

CPython's reference-counting implementation is an implementation detail.

So this statement:

"When an object's reference count reaches zero, CPython can deallocate it."

is appropriate.

But this statement:

"Every Python implementation immediately destroys an object when its last variable disappears."

is not.

Other Python implementations can use different memory-management strategies. The reference chapter explicitly limits its reference-counting discussion to CPython.

Common Misconceptions

"Reference counting counts variables."

Not precisely. It keeps track of references, which can originate from temporary operations, names, containers, function frames, and closures.

"del destroys the object."

No. del removes a name or reference. The object can remain alive if another reference exists.

"A function returning destroys its local objects."

No. If the caller keeps a reference, the activation frame disappears but the objects that are returned survive.

"Reference counting handles all garbage."

No. Reference cycles may keep the reference count above zero although the object graph is no longer reachable.

"sys.getrefcount() gives the exact number I should rely on."

No. First, really, the call itself will create a temporary reference, which is already a problem. On top of that, maybe implementation details, like immortal objects in modern CPython, can render the value reported unsuitable to be used in application logic.

Final Thoughts

Reference counting is a real-life technique adopted by CPython that stems from the idea that, "references keep objects alive".

Aliases and assignments produce references. They can be eliminated by rebinding, del, container removal, and the end of function frames. CPython can typically deallocate an object instantly when its reference count drops to zero. But reference counting is not the whole memory-management system. Cycles can keep objects alive despite being unreachable, so CPython also uses cyclic garbage collection.

The most useful mental model is:

names / containers / frames / closures
                ↓
           references
                ↓
             object
                ↓
      reference count changes
                ↓
       count reaches zero
                ↓
       CPython can reclaim it

And the most important distinction to remember is:

A name can disappear without the object disappearing. An object can disappear only when the references keeping it alive are gone.

This is the foundation for understanding CPython's garbage collection, object lifetime, closures, weak references, and memory behavior.

Frequently Asked Questions

What is reference counting in CPython?

It is the main memory management system for CPython that keeps track of object references. In general, CPython can deallocate an object when its reference count drops to zero.

What increases a reference count?

The reference count may rise as a result of activities like assignment, placing an object in a container, sending it to a function, retaining it thru a closure, and some temporary interpreter operations.

What decreases a reference count?

Rebinding or deleting names, removing objects from containers, ending function frames, and releasing temporary references can decrease the count.

Why does an object survive after a function returns?

The function's frame and local references disappear, but another reference, such as the caller's reference to the returned object, can keep the object alive.

Why can't reference counting handle cycles?

In a cycle, objects keep references to one another. Their counts therefore remain above zero even though no live part of the program can reach the cycle. CPython's cyclic garbage collector handles these cases.