Reference Counting in CPython
Overview
In CPython, every object keeps a count of how many labels (variables, list elements, attributes) point to it. This is called the reference count. When a reference count drops to zero, the object is immediately destroyed and its memory is reclaimed. import sys my_list = [1, 2, 3] # Returns 2 (1 for my_list, 1 for getrefcount argument) print(sys.getrefcount(my_list)) Think of an object as a balloon and references as strings holding it down. Every time you assign a variable or append it to a list, you tie a new string to the balloon. Every time a variable goes out of scope, you cut a string. When the last string is cut, the balloon flies away. This deterministic memory management ensures resources are freed the exact moment they are no longer needed. node_a = {} node_b = {} node_a['next'] = node_b node_b['prev'] = node_a del node_a, node_b **Edge Case**: Circular references prevent the reference count from ever reaching zero. The objects keep each other alive until the generational garbage collector eventually intervenes.
Understanding reference counting explains: - When Python cleans up objects - Why `__del__` is called (or not called) - How circular references can cause memory leaks
Where used: Memory optimization, Writing C extensions, Debugging with `objgraph`
Why learn this
- You can predict exactly when objects are destroyed, avoiding surprising resource leaks with files or network sockets.
- It forms the foundation for understanding circular references and the generational garbage collector.
Code walkthrough
import sys
data = {'key': 'value'}
print(sys.getrefcount(data))
alias = data
print(sys.getrefcount(data))
del alias
print(sys.getrefcount(data))
Focus: The reference count explicitly increments when `alias` is created and decrements when `alias` is deleted.
Aha moment
class Tracked:
def __del__(self):
print('Destroyed!')
obj = Tracked()
alias = obj
del obj
print('End of script')
Prediction: When does `'Destroyed!'` print? Before or after `'End of script'`?
Common guess: Before `'End of script'`, because we called `del obj`.
It prints AFTER `'End of script'`. `del obj` removes the name `obj` and decrements the reference count to `1` (because `alias` still points to it). The object isn't destroyed until the script ends and `alias` is cleaned up.
Common mistakes
- Assuming `del` deletes the object: `del x` only removes the variable name `x` and decrements the object's reference count. The object itself is only deleted if its reference count reaches zero.
- Creating circular references: If object `A` points to object `B`, and object `B` points to `A`, their reference counts will never drop to zero even if no variables point to them. The garbage collector eventually cleans this up, but it delays memory release.
Glossary
- reclaimed
- The process where a computer takes back memory that is no longer being used by a program so it can be used for something else. Example: memory is freed.
- circular references
- When two or more objects hold references to each other, preventing the system from realizing they are no longer needed. Example: `a.child = b; b.parent = a`.
Recall questions
- What happens immediately when an object's reference count drops to zero in CPython?
- Does using `del` on a variable guarantee that the object it points to is deleted?
- How does reference counting behave if two objects only refer to each other?
Understanding checks
What does this script print?
It prints `'Done!'` followed by `'Closed!'`.
`del conn` only removes the label `conn` and drops the reference count by `1`. Since `backup` still points to the object, its reference count is not zero. The object is finally destroyed and prints `'Closed!'` when the script ends and `backup` is cleaned up.
Why might memory not be freed immediately when two objects hold references to each other, even if no variables point to them?
They form a circular reference, preventing their reference counts from ever reaching zero.
Reference counting only reclaims an object when its count hits exactly `0`. In a circular reference, each object keeps the other's count at `1`, so the memory won't be freed until the generational garbage collector runs.
Practice tasks
Observe reference counting with `sys.getrefcount`
Modify the starter code to create a list `[1, 2, 3]` and assign it to a variable `a`. Print the reference count of `a` using `sys.getrefcount(a)`. Then, create a second variable `b` that points to `a`, and print the reference count again. Note: `sys.getrefcount` temporarily increases the count by `1` because the function argument itself creates a reference.
Challenge
Tracking Object Destruction
Create a class `Resource` with an `__init__` method that prints `'Created'` and a `__del__` method that prints `'Destroyed'`. Write a short script where you instantiate a `Resource` and assign it to a variable `x`. Then assign `x` to another variable `y`. Delete `x` and print `'x deleted'`. Finally, delete `y` and print `'y deleted'`. Observe exactly when `'Destroyed'` is printed.
Continue learning
Previous: Object identity: id() and memory references
Previous: Everything is an object in Python