Get AI summaries of any video or article — Sign up free
Mutability revisited - Python 3 Programming Tutorial p.8 thumbnail

Mutability revisited - Python 3 Programming Tutorial p.8

sentdex·
5 min read

Based on sentdex's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

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)?

Reassignment only changes the local name inside the function. For immutable types like strings, there’s no in-place modification—so when the function does something like `game =

Why does reassigning a function parameter fail to update the caller when the value is immutable (e.g., a string)?

Reassignment only changes the local name inside the function. For immutable types like strings, there’s no in-place modification—so when the function does something like `game =

Why does reassigning a function parameter fail to update the caller when the value is immutable (e.g., a string)?

Reassignment only changes the local name inside the function. For immutable types like strings, there’s no in-place modification—so when the function does something like `game = ...`, it points the local parameter to a new object. The caller’s `game` still points to the original string object, which can be confirmed by comparing `id(game)` before and after the function call.

How does `id()` help distinguish “contents changed” from “reference changed”?

`id()` returns the identity of an object (a proxy for its memory identity during runtime). If the caller’s `id(game)` stays the same after the function runs, the caller’s reference didn’t change. If the function reassigns its local parameter, the parameter’s `id()` may change while the caller’s `id()` remains unchanged. In the tutorial, the string case shows the caller’s `id()` repeating, proving the function never modified the caller’s reference.

What’s the key difference between mutating a list and reassigning it inside a function?

Mutating a list means changing its contents (e.g., assigning to an index). If the caller and function share the same list object reference, those content changes are visible outside the function. Reassigning the list variable inside the function (e.g., `game = new_list`) only changes the local name; it doesn’t replace the caller’s object. The tutorial demonstrates that list contents can change, but the caller still won’t get a replacement unless the updated object is returned and assigned back.

Why does returning the updated object and assigning it at the call site fix the problem?

Returning the updated value makes the new object explicit to the caller. The caller then does something like `game = game_map(game)` (or `game = game_board(game)` depending on naming). That assignment updates the caller’s reference to the returned object, which works for immutable types (where you must create a new value) and also keeps behavior consistent for mutable types.

When does `global` make reassignment work, and why is it considered a workaround?

Declaring `global game` inside the function makes the function operate on the module-level variable name rather than a local parameter. That means reassignment updates the global variable directly, so the caller sees the new value without returning it. The tutorial treats this as less clean because it couples the function to external state and can make code harder to reason about.

Review Questions

  1. In Python, what observable difference would you expect between a string update attempt and a list update attempt inside a function?
  2. How would you use `id()` to test whether a function changed an object’s contents or only changed a local reference?
  3. What calling pattern ensures that a function can replace an immutable value in the caller (return + assignment), and why?

Key Points

  1. 1

    Python distinguishes between mutating an object’s contents and reassigning a variable name; only the former affects shared mutable objects.

  2. 2

    Immutable types like strings can’t be modified in place, so reassigning inside a function won’t update the caller’s reference.

  3. 3

    Comparing `id()` values before and after a function call helps confirm whether the caller’s reference changed.

  4. 4

    Lists (and other mutables) allow in-place content changes, so callers can observe updates when the same object is shared.

  5. 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. 6

    Using `global` can force reassignment to affect outer scope, but it’s a risky workaround compared with returning values.

  7. 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.

Highlights

The caller’s variable doesn’t change when a function only reassigns its local parameter—`id()` reveals the reference stays the same for immutable strings.
Mutable objects can be changed in place, so list contents updates can “leak” out of a function without returning anything.
Returning the updated object and assigning it back at the call site is the reliable fix for replacement behavior.
`global` makes reassignment work by targeting the outer variable name directly, but it bypasses cleaner data-flow patterns.

Topics

  • Mutability
  • Immutability
  • Function Scope
  • Object Identity
  • Return Values