Pass by assignment (also known as “call by object reference” or “call by sharing”) is Python’s unique argument-passing mechanism — when you pass an argument to a function, you’re not passing the object itself nor a copy of its value, but a reference (the name) bound to the same object in memory. If the object is mutable (list, dict, set, custom class instance), modifications inside the function affect the original object outside. If immutable (int, float, str, tuple, frozenset), rebinding the parameter name creates a new object and leaves the original unchanged. In 2026, understanding pass by assignment is essential — it explains many “unexpected” behaviors (e.g., why lists change but integers don’t), prevents subtle bugs in data pipelines, shared state in ML workflows, and mutable default arguments traps, and helps write predictable, safe Python code.
Here’s a complete, practical guide to pass by assignment in Python: how it works, mutable vs immutable behavior, common pitfalls (mutable defaults, shared references), real-world patterns, and modern best practices with type hints, defensive copying, immutability, and pandas/Polars integration.
Basic behavior — parameters are new names bound to the same object; rebinding creates new objects, mutation modifies the shared object.
def modify(n, lst):
n += 1 # rebinds local name ? new int object
lst.append(999) # mutates the shared list object
print(f"Inside: n={n}, lst={lst}")
x = 5
mylist = [1, 2, 3]
print(f"Before: x={x}, mylist={mylist}")
modify(x, mylist)
print(f"After: x={x}, mylist={mylist}")
# Before: x=5, mylist=[1, 2, 3]
# Inside: n=6, lst=[1, 2, 3, 999]
# After: x=5, mylist=[1, 2, 3, 999]
Mutable default arguments — classic gotcha: defaults are evaluated once at definition time, shared across calls.
def bad_append(item, lst=[]): # Dangerous: lst is created once
lst.append(item)
return lst
print(bad_append(1)) # [1]
print(bad_append(2)) # [1, 2] ? shared list!
print(bad_append(3)) # [1, 2, 3]
# Fix: use None as sentinel
def safe_append(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
print(safe_append(1)) # [1]
print(safe_append(2)) # [2]
Real-world pattern: safe mutation in pandas/Polars pipelines — avoid unintended side effects when passing DataFrames or lists.
import pandas as pd
def add_column(df: pd.DataFrame, col_name: str, values) -> pd.DataFrame:
# Defensive copy prevents modifying caller's DataFrame
df = df.copy()
df[col_name] = values
return df
# Safe usage
data = pd.DataFrame({'A': [1, 2, 3]})
new_data = add_column(data, 'B', [10, 20, 30])
print(data) # unchanged: only 'A'
print(new_data) # has 'A' and 'B'
Best practices make pass by assignment safe, predictable, and performant. Prefer immutable objects when possible — int, str, tuple, frozenset, frozen dataclasses — changes create new objects, no side effects. Use defensive copying for mutable arguments you don’t want to modify — lst.copy(), dict.copy(), df.copy(). Modern tip: use Polars for immutable-by-default DataFrames — most operations return new frames, naturally DRY and safe. Add type hints — def func(df: pd.DataFrame) -> pd.DataFrame — signals intent and helps mypy catch misuse. Avoid mutable defaults — use None sentinel pattern. Use context managers (with) for resource cleanup — DRY setup/teardown. Prefer generators over lists for large data — lazy, memory-efficient. Use copy.deepcopy() only when necessary — expensive; prefer shallow copy or immutable alternatives. Combine with functional style — avoid mutation when possible (map, filter, reduce, list comprehensions). Test for side effects — assert original objects unchanged after function calls. Use frozen dataclasses or attrs with frozen=True — immutable objects with nice syntax.
Pass by assignment in Python binds names to objects — mutable objects change in-place, immutable ones get rebound. In 2026, prefer immutability, defensive copying, type hints, Polars for safe dataframes, and sentinel defaults. Master pass by assignment, and you’ll write predictable, bug-resistant code that avoids the classic Python gotchas.
Next time you pass a list or DataFrame to a function — remember pass by assignment. It’s Python’s cleanest way to say: “Here’s a reference — mutate carefully.”