Python Defaults and Copies Share More State Than Their Syntax Suggests
A practical model for function defaults, shallow copies, nested aliases, and defensive API boundaries.
Python variables hold references to objects. Assignment copies a reference, not the object, and two containers can be independent at the outer level while sharing every nested value.
Function defaults make this visible because default expressions are evaluated when the def statement executes, not on every call.
def collect(value, output=[]):
output.append(value)
return output
Calls that omit output share the same list. The conventional fix uses an immutable sentinel and creates a list inside the function:
def collect(value, output=None):
if output is None:
output = []
output.append(value)
return output
Use a private sentinel object instead of None when None is itself meaningful input.
Immutable containers make safe defaults
A tuple is immutable, so any tuple is safe as a function default even when it contains mutable objects.
shared = []
def record(values=(shared,)):
values[0].append('event')
The tuple cannot replace its element, and that immutability recursively protects the list from changes through the tuple. Repeated calls therefore cannot communicate through shared.
Shallow copying duplicates one container
List slicing, list.copy(), dict.copy(), and copy.copy() create a new outer container while retaining references to nested objects.
original = {'options': {'retries': 2}}
duplicate = original.copy()
duplicate['options']['retries'] = 5
print(original['options']['retries'])
The print shows 5 because both dictionaries reference the same nested options dictionary.
Despite its name, dict.copy() recursively copies nested built-in containers but leaves instances of user-defined classes shared. The example therefore prints 2; only custom objects require copy.deepcopy.
Deep copy is a policy, not a guarantee
Some objects should not be duplicated: modules and functions are returned unchanged, while file handles and sockets are not meaningfully cloneable. Classes can define __copy__ and __deepcopy__ to control behaviour.
Deep copying can also duplicate identity-bearing domain entities when sharing would be correct. A targeted constructor or serialisation boundary is often clearer than cloning an arbitrary graph.
State ownership should be visible
Defensive copying is useful when an API promises that later caller mutation will not affect stored configuration. Copy at that boundary and document whether it is shallow. Elsewhere, immutable values or explicit ownership can avoid both copying cost and aliasing surprises.
The central question is always which identities are shared. Syntax such as a literal default or a .copy() call is only a clue; the object graph determines the behaviour.