Pass by Assignment in Python 2026 – Understanding References and Mutability
Python does not use "pass by value" or "pass by reference" like other languages. Instead, it uses pass by assignment. Understanding this concept is crucial for writing correct and efficient functions, especially when working with mutable objects.
TL;DR — Key Takeaways 2026
- Python passes objects by assignment: the parameter name is bound to the same object
- Immutable objects (int, str, tuple, etc.) appear to be passed by value
- Mutable objects (list, dict, set, custom classes) can be modified inside functions
- Use
copy()ordeepcopy()when you need to protect original data
1. Core Concept: Pass by Assignment
def modify_list(lst):
lst.append(99) # Modifies the original list
def reassign_list(lst):
lst = [1, 2, 3] # Only rebinds the local name
my_list = [10, 20, 30]
modify_list(my_list)
print(my_list) # [10, 20, 30, 99] ← Changed!
reassign_list(my_list)
print(my_list) # [10, 20, 30, 99] ← Not changed!
2. Immutable vs Mutable Behavior
# Immutable → behaves like pass by value
def increment(n):
n += 1 # Creates a new integer object
x = 5
increment(x)
print(x) # Still 5
# Mutable → can be modified in place
def add_item(items):
items.append("new")
my_list = ["a", "b"]
add_item(my_list)
print(my_list) # ['a', 'b', 'new']
3. Best Practices in 2026
- Be explicit about whether you intend to mutate or rebind
- Use copy() or deepcopy() when you want to protect the original object
- Prefer immutable data structures (tuples, frozensets, NamedTuple) when possible
- Document mutation behavior clearly in function docstrings
- Avoid side effects in pure functions when possible
What Pass by Assignment Means in Practice
When you call a function, Python binds each parameter name to the same object the caller passed. Nothing is copied. This model is called pass by assignment, or pass by object reference, because it works exactly like an assignment statement: param = argument.
Two consequences follow. If the function mutates the object, for example by appending to a list or setting a key in a dictionary, the caller sees the change because both names point to one object. If the function rebinds the parameter to a new object with =, only the local name changes and the caller's variable is untouched.
This explains the behaviour of immutable values too. Integers, strings and tuples cannot be changed in place, so any operation that seems to modify them actually creates a new object and rebinds the local name. The caller never sees a difference.
The id() function, which returns an object's identity, is a simple way to see whether two names refer to the same object.
Worked Example: Mutation Versus Rebinding
We pass the same list to two functions, one that mutates it and one that rebinds the parameter.
def add_item(items):
print("inside, same object:", id(items) == id(basket))
items.append("tea") # mutation: caller sees it
def replace_items(items):
items = ["coffee"] # rebinding: only the local name changes
return items
basket = ["milk"]
add_item(basket)
print(basket)
new = replace_items(basket)
print(basket, new)
Inside add_item the parameter is the same object as basket, and after the call basket is ['milk', 'tea']. replace_items returns ['coffee'] but leaves basket unchanged, because assigning to items only rebinds the local name.
Practical Tips
If a function must change an argument, say so in its name or docstring, for example update_totals_in_place. Otherwise return a new object and leave the input alone.
When you need a safe working copy, use list(items), dict(d) or copy.deepcopy for nested data, and do it at the top of the function so the intent is clear.
Common Mistakes
- Expecting a function to change the caller's integer or string.
- Mutating a list argument when the caller did not expect it.
- Thinking that = inside a function changes the caller's variable.
- Copying large objects defensively when a clear return value would do.
Quick Summary
- Parameters are bound to the caller's objects.
- Mutation is visible to the caller.
- Rebinding with = is local.
- Return new values instead of mutating when in doubt.
Conclusion
Understanding "pass by assignment" is fundamental to writing predictable and bug-free Python functions. In 2026, developers who master this concept write clearer code, avoid unexpected mutations, and make better decisions about when to copy data versus mutate in place.
Next steps:
- Review your functions and make mutation behavior explicit