Filter concepts by levelShowing all levels.

Python · What a 5-Year Python Engineer Should Be Able to Explain

Python

Concepts
1

Seventeen of eighteen roadmap questions here are already taught in depth by existing concepts (mutability, is vs ==, default arguments, LEGB, closures, late binding, decorators, iterators/generators, context managers, MRO/super(), descriptors, __getattribute__, __slots__, garbage collection, reference counting) — credited via alsoCovers rather than re-derived. The one genuine synthesis gap, how Python actually passes objects to functions, gets its own concept: one binding rule, not two calling conventions.

Python overview

Python

The one synthesis question with no existing single-concept home — everything else in this subheading is a recap of concepts already taught elsewhere in this topic.

How Python passes objects to functions

standardintermediate

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

python
def f(x):
    x = x + [1]     # rebinds the LOCAL name x — caller's list is untouched

def g(x):
    x.append(1)      # mutates the SHARED object — caller sees it too

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.

argument_passing.pypython
def reassign(x):
    x = x + [99]          # rebinds the local name x to a NEW list
    print("inside reassign:", x)


def mutate(x):
    x.append(99)           # mutates the object x and caller both point at
    print("inside mutate:", x)


numbers = [1, 2, 3]
reassign(numbers)
print("after reassign:", numbers)

mutate(numbers)
print("after mutate:", numbers)
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.
Output
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

python
# "strings and ints are pass-by-value, lists are pass-by-reference" — WRONG MODEL
def confusing(n, items):
    n = n + 1          # looks like "value" semantics
    items.append(n)     # looks like "reference" semantics
    # but both lines follow the exact same rule: bind, then either
    # rebind (line 1) or mutate in place (line 2)

Better

python
# one rule, no exceptions: binding is always "same object, new name"
def consistent(n, items):
    n = n + 1           # rebinds local n — ints are immutable, so this is the ONLY option
    items.append(n)      # mutates the shared list — lists offer this option, ints do not

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

Advertisement