Mutability revisited - Python 3 Programming Tutorial p.8
Based on sentdex's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Python distinguishes between mutating an object’s contents and reassigning a variable name; only the former affects shared mutable objects.
Briefing
Mutability in Python can make it look like functions “modify” variables from the outside—until an immutable type (like a string) breaks that assumption. The core lesson is that changing the contents of a mutable object inside a function can affect the caller, but reassigning the parameter to a new object does not. That distinction matters because it’s a common source of baffling bugs: code appears logically correct, yet the outer variable never changes.
The tutorial starts with a practical setup: a variable (initially a string) is used to represent a game state, and a function called to “update” it. When the function tries to change the variable by reassigning it, the caller’s value stays the same—despite the function running. To make the behavior concrete, the code checks object identity using Python’s built-in `id()` function. The identity values show that the outer variable keeps pointing to the original object, while the function’s reassignment points the local parameter to a different object. In other words, the function can’t overwrite the caller’s reference when the type is immutable.
The discussion then contrasts this with mutable objects. When the game state is changed from a string to a list (and later a list-of-lists scenario), the function can mutate the underlying object—so the caller observes changes. But even with mutables, there’s still a boundary: the function can modify the object’s contents, yet it can’t “replace” the object itself in the caller unless the updated object is returned and assigned back. The tutorial demonstrates this by showing that after a function runs, the list’s contents may change, while the original object reference behavior remains constrained.
To address the replacement problem, the tutorial introduces two approaches. First, it shows that using `global` can make reassignment affect the outer scope, because the function directly targets the global variable name. That works, but it’s presented as risky and less clean for general programming.
Second—and as the preferred method—it recommends passing the object into the function, mutating it (or computing an updated version), then returning the result and assigning it back at the call site. The function is reframed as something like `game_map` (a renamed parameter/object), and the calling code does `game = game_map(game)` (or equivalent). This pattern ensures that when the object is immutable, the caller still receives the new value, and when it’s mutable, the caller gets consistent updates without relying on global state.
The takeaway is operational: treat mutability as “contents can change” rather than “variables can be reassigned across scopes.” When unsure, return the updated value and assign it back; otherwise, immutable types like strings will silently keep the caller stuck with the original object reference.
Cornell Notes
Python mutability has two different behaviors that often get confused: mutating an object’s contents versus reassigning a variable to a new object. Inside a function, changing a mutable object (like a list) can affect the caller because both names can point to the same underlying object. But reassigning a parameter (especially when the value is immutable like a string) doesn’t update the caller’s reference, which can be verified using `id()`. The safest general pattern is to pass the object into the function, modify or compute an updated value, then return it and assign it back at the call site. Using `global` can force reassignment to affect outer scope, but it’s treated as a less desirable workaround.
Why does reassigning a function parameter fail to update the caller when the value is immutable (e.g., a string)?
Why does reassigning a function parameter fail to update the caller when the value is immutable (e.g., a string)?
Why does reassigning a function parameter fail to update the caller when the value is immutable (e.g., a string)?
How does `id()` help distinguish “contents changed” from “reference changed”?
What’s the key difference between mutating a list and reassigning it inside a function?
Why does returning the updated object and assigning it at the call site fix the problem?
When does `global` make reassignment work, and why is it considered a workaround?
Review Questions
- In Python, what observable difference would you expect between a string update attempt and a list update attempt inside a function?
- How would you use `id()` to test whether a function changed an object’s contents or only changed a local reference?
- What calling pattern ensures that a function can replace an immutable value in the caller (return + assignment), and why?
Key Points
- 1
Python distinguishes between mutating an object’s contents and reassigning a variable name; only the former affects shared mutable objects.
- 2
Immutable types like strings can’t be modified in place, so reassigning inside a function won’t update the caller’s reference.
- 3
Comparing `id()` values before and after a function call helps confirm whether the caller’s reference changed.
- 4
Lists (and other mutables) allow in-place content changes, so callers can observe updates when the same object is shared.
- 5
Reassigning a parameter to a new object inside a function won’t replace the caller’s object unless the function returns the new object and the caller assigns it.
- 6
Using `global` can force reassignment to affect outer scope, but it’s a risky workaround compared with returning values.
- 7
A robust habit is: pass the object in, modify or compute an updated version, return it, and assign the result back at the call site.