Object References, Mutability, and Garbage Collection
Notes based on Fluent Python, summarized and rephrased in my own words.[file:15]
Names vs objects
A central idea in Python is that names are not boxes that hold values; they are labels attached to objects.[file:15]
- Objects live in memory and have identity, type, and value.
- Variables (names) simply refer to those objects.
- The same object can have many names (aliases).
This is why assignment like b = a does not copy a complex object; it just creates another reference to the same object.[file:15]
a = [1, 2, 3]
b = a
a.append(4)
print(b) # [1, 2, 3, 4] – b sees the changeFor immutable basic values like small integers, assigning to a new variable often creates or reuses separate immutable objects instead of sharing state.[file:15]
a = 1
b = a # integers are immutable; rebinding a doesn’t affect b
a = 2
print(b) # 1Identity, equality, and aliasing
Python distinguishes between:
- Identity: "is this the same object?" — checked with
is/is notorid().[file:15] - Equality: "do these objects have the same value?" — checked with
==and!=.[file:15]
Example:
charles = {'name': 'Charles L. Dodgson', 'born': 1832}
lewis = charles
lewis is charles # True — same object
id(charles) == id(lewis) # True
lewis['balance'] = 950
print(charles) # {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
alex == charles # True — same contents
alex is charles # False — different objectsGuidelines:[file:15]
Use
==when you care about value.Use
isonly for identity checks, especially with singletons likeNone:pythonif x is None: ... if x is not None: ...
is is faster than == because it cannot be overloaded and just compares object IDs.[file:15] By contrast, == may involve complex logic (e.g. deep structural comparisons).
Tuples and "relative immutability"
Tuples are immutable containers, but they can hold mutable objects. That means the tuple itself cannot change length or have its references replaced, but the objects it refers to can still mutate.[file:15]
Shallow copies by default
For most built-in mutable containers (like list, dict, set), the simplest way to "copy" is to call the type constructor or use slicing.[file:15]
l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1) # or l1[:] for lists
l2 == l1 # True — same nested contents
l2 is l1 # False — different outer list objectsThis is a shallow copy:[file:15]
- The outer container is new.
- The elements inside point to the same objects as in the original.
For immutable elements, this is fine and memory-efficient. For nested mutable structures, though, changes to inner objects will be visible through all references.
For arbitrary objects, the copy module provides:
copy.copy(obj)— shallow copy.copy.deepcopy(obj)— attempts a deep recursive copy of the entire structure.[file:15]
Function arguments and call-by-sharing
Python uses call by sharing (also called call by object sharing).[file:15]
- Function parameters receive copies of the references to the original objects.
- Inside the function, the parameter names are aliases for the original objects.
Consequences:[file:15]
- The function can mutate any mutable arguments it receives (e.g. lists, dicts).
- The function cannot rebind the caller’s name — assigning to the parameter only changes the local reference.
def append_item(seq, item):
seq.append(item) # mutates the caller's list
my_list = [1, 2]
append_item(my_list, 3)
print(my_list) # [1, 2, 3]A classic pitfall: never use a mutable object as a default argument.[file:15]
def bad_func(arg, cache=[]): # cache is shared across calls!
cache.append(arg)
return cacheThe default list cache is created once at function definition time and reused on every call, which leads to surprising behavior.
del and garbage collection
Objects are kept alive by references. When an object's reference count drops to zero, it becomes eligible for garbage collection.[file:15]
del nameremoves a name binding; it does not directly destroy the object.[file:15]- If that was the last reference to the object (or all remaining references become unreachable), the object can then be collected as garbage.[file:15]
- Rebinding a name to a new object can also drop the reference count of the old object.[file:15]
x = [1, 2, 3]
y = x
# Now both x and y refer to the same list
del x # removes the name x, but list is still referenced by y
# When y is later rebound or goes out of scope, the list may be collectedWeak references
Sometimes you want to refer to an object without extending its lifetime. This is common in caches or object registries. Weak references make this possible.[file:15]
- A
weakref.ref(obj)holds a reference that does not increase the reference count.[file:15] - When the object is collected, the weak reference automatically becomes dead (returns
None).[file:15]
import weakref
a_set = {0, 1}
wref = weakref.ref(a_set)
print(wref) # weak reference to the set
# Rebind a_set so the original set may be collected
a_set = {2, 3, 4}
print(wref()) # None — the original set was collectedWeakValueDictionary
weakref.WeakValueDictionary is a mutable mapping whose values are stored as weak references.[file:15]
- When a value object is no longer strongly referenced elsewhere, it is collected.
- The corresponding key is then automatically removed from the dictionary.
This makes it a natural fit for caches, where you don't want cached objects to outlive the rest of the program’s references.[file:15]
Limitations of weak references
Not all Python objects can be weakly referenced.[file:15]
- Plain
listanddictinstances cannot be direct weak reference targets. - Subclassing these built-ins can make instances weak-referenceable.
Tricks with immutable types
Because immutable objects cannot change, Python sometimes reuses them instead of creating copies.[file:15]
For example, for tuples:
t1 = (1, 2, 3)
t2 = tuple(t1)
t3 = t1[:]
print(t2 is t1) # True
print(t3 is t1) # TrueThe constructor tuple(t1) and the slice t1[:] both return the exact same tuple object—not a new copy—because there’s no need to duplicate an immutable object.[file:15]
Similar behavior occurs with str, bytes, and frozenset under some circumstances.[file:15] For example, fs.copy() on a frozenset can also return the same object.[file:15]
The key takeaway: with immutable objects, creating “copies” often just returns another reference to the same object, which is safe because the object cannot change.