The most common source of subtle bugs in Python code
Most Python bugs in production come from unexpected mutation — a function silently modifies a list that was passed in, a mutable default argument accumulates state across calls, or two variables thought to be independent are actually pointing at the same object. Understanding how Python handles values and references prevents an entire category of bugs.
Mutability questions reveal whether you've truly internalised the language rather than just used it — they're quick to hit and reveal a lot.
Python variables are labels, not boxes. When you write x = [1, 2, 3], you're sticking a label x on a list object. If you then write y = x, you've put a second label on the same object. Changing the object through either label affects both.
Immutable objects (int, str, tuple) can't change in place — Python creates a new object instead. Mutable objects (list, dict, set) change in place, so all labels pointing at them see the change.
Python creates default argument values once when the function is defined — not on each call. So a mutable default like [] is shared across every call.
Fix: use None as a placeholder and create the list inside the body.
Rule: never use [], {}, or set() as a default. Always use None.
is checks identity — are both variables pointing to the same object in memory? == checks value — do they contain the same data? id() returns the object's memory address and is what is compares under the hood.
Assignment copies the reference, not the object. A shallow copy duplicates the outer container but keeps inner objects shared — only deep copy creates a fully independent structure.
Shallow copy only copies the outer container — inner objects are still shared. Use deepcopy when your list contains other lists or objects.