How Python passes objects to functions
standardintermediatePython is neither "pass-by-value" nor "pass-by-reference" — calling a function binds the parameter name to the SAME object the argument expression evaluated to, the same way a plain assignment would. What happens next depends only on whether that object is mutable, and whether the function reassigns the parameter or mutates the object in place.
Think of it as
A function call is assignment in disguise: def f(x) followed by f(obj) runs x = obj in a fresh local namespace. No copy is made, and no pointer-to-the-variable is handed over either — just one more name for the same object. Reassigning x inside f only repoints that local name; mutating x in place (x.append(...)) changes the one object every other name still sees.
What we're doing: Show the same argument-passing model producing two different outcomes for the caller, depending only on reassignment vs in-place mutation.
- 2
- x + [99] builds a brand-new list; x = ... rebinds the LOCAL name to it — numbers outside is never touched.
- 6
- x.append(99) mutates the list object in place — the same object numbers refers to.
- 10
- reassign(numbers) binds x to the same list as numbers, exactly like x = numbers would.
- 11
- numbers is unchanged after reassign — proof that rebinding a parameter never reaches the caller.
inside reassign: [1, 2, 3, 99]
after reassign: [1, 2, 3]
inside mutate: [1, 2, 3, 99]
after mutate: [1, 2, 3, 99]Why this works: Both functions receive the same kind of binding — x becomes another name for the list numbers already points at. reassign only ever does x = something, which repoints the local name x and leaves every other name (including numbers) exactly where it was. mutate instead calls a method on the object itself, and there is only one object, so every name pointing at it sees the change. The call convention never changed between the two calls; only what the function body did with x did.
Assuming immutable arguments are "passed by value" and mutable ones "by reference"
Wrong
Better
What you see: Code review comments like "pass a copy of the list since Python passes lists by reference" — the fix (list(items) or items[:]) is correct, but the reasoning generalizes wrongly to "immutable types are safe from this," which is only true because they have no in-place mutation, not because they were passed differently.
Why: There is exactly one binding rule in Python, not two. It looks like "pass-by-value" for an int only because ints have no in-place mutation to offer — n = n + 1 is the sole way to change what n refers to, so it always rebinds. It looks like "pass-by-reference" for a list only because list.append is available — but list arguments can also be rebound (x = [] inside the function) with zero effect on the caller, which pass-by-reference would not allow. The type's mutability determines what a function CAN do with the shared object, not how the object was passed.
Remember: A call is assignment: parameter = argument. Mutable + mutated in place -> caller sees it. Reassigned -> caller never sees it, mutable or not.
See also: names and references · mutable vs immutable · default arguments · mutability and immutability

