Pass Objects to Python Functions

Understand how Python passes objects to functions, the difference between passing mutable and immutable types, and how to avoid accidental data modification.

7 min read

Python's argument-passing model is a frequent source of confusion for programmers coming from other languages. Some languages use pass-by-value, where the function receives a copy of the argument and modifications inside the function have no effect outside. Other languages use pass-by-reference, where the function receives a direct alias to the caller's variable and modifications are immediately visible. Python uses neither of these models. Instead, Python uses a mechanism called pass-by-assignment, also known as pass-by-object-reference, and understanding it is essential for writing functions that interact with data correctly and predictably.

The core idea is simple: when you call a function with an argument, Python binds the parameter name to the object that the argument refers to. The parameter becomes an additional name for that object, not a copy of it and not an alias for the caller's variable. This distinction matters because what the function can do to the object depends on whether the object is mutable or immutable, a concept introduced in the article on immutable and mutable data types in Python. Immutable objects like integers, strings, and tuples cannot be changed, so any operation on them creates a new object. Mutable objects like lists, dictionaries, and sets can be modified in place, and those modifications are visible to the caller because both the caller's variable and the function's parameter refer to the same underlying object.

Reassigning parameters does not affect the caller

The most important rule to internalize is that reassigning a parameter name inside a function, using the equals sign to point the name at a different object, has no effect on the caller's variable. The parameter is a local variable in the function's scope, and reassigning it simply changes which object that local name refers to:

pythonpython
def reset(value):
    value = 0
    print(f"Inside: {value}")
 
x = 42
reset(x)
print(f"Outside: {x}")  # Still 42

Inside the function, the parameter value is initially bound to the integer 42, the same object that x refers to. The assignment value = 0 rebinds the local name value to a new integer 0, but it does not change x because x is a separate name in a separate scope. The connection between x and value is that they initially referred to the same object, not that they are aliases for each other. This behavior is identical for all types, mutable and immutable alike. Reassigning a parameter never affects the caller's variable, period.

This rule explains why a function that receives a list and assigns a new list to the parameter does not change the caller's list. The caller still holds a reference to the original list object, and the function's parameter now points to a different list that the caller has never seen and cannot access. Understanding this is the first step toward understanding why some list modifications do propagate to the caller while others do not.

Modifying mutable objects does affect the caller

When a function receives a mutable object and modifies it through methods or element assignment, the modification is visible to the caller. Both the caller's variable and the function's parameter refer to the same object, so changes made through either name are changes to the same underlying data:

pythonpython
def append_item(items):
    items.append("new")
 
my_list = ["a", "b"]
append_item(my_list)
print(my_list)  # ['a', 'b', 'new']

The append method modifies the list object in place. It does not create a new list, and it does not rebind the parameter name items. It simply changes the content of the existing list object that both my_list and items refer to. When the function returns, my_list still refers to the same list, but that list now contains an additional element. This behavior is what people mean when they say Python passes lists by reference, though the more precise description is that Python passes a reference to the list object, and in-place modifications to that object are visible through any reference to it.

The same principle applies to dictionary updates, set additions, attribute assignments on objects, and any other operation that mutates an existing object rather than creating a new one. The key distinction is between operations that change the object (append, update, add, index assignment) and operations that create a new object and rebind the name (assignment with =, augmented assignment like += on immutable types). The article on common Python function mistakes covers several real-world bugs caused by confusing these two categories of operations.

Immutable types and the illusion of modification

When a function performs an operation on an immutable argument that appears to modify it, Python actually creates a new object. The parameter name is rebound to the new object, and the caller's original object is untouched because immutable objects cannot be modified by any operation:

pythonpython
def add_exclamation(text):
    text = text + "!"
    print(f"Inside: {text}")
 
greeting = "Hello"
add_exclamation(greeting)
print(f"Outside: {greeting}")  # Still "Hello"

The expression text + "!" creates a new string "Hello!", and the assignment rebinds the local name text to that new string. The original string "Hello" that greeting refers to is unchanged because strings are immutable. This is consistent with the rule that reassignment never affects the caller, but the difference from the mutable case is that strings provide no methods to modify them in place, so the only way to change what text refers to is through reassignment, and reassignment is local.

Numbers, strings, tuples, frozensets, and bytes are all immutable in Python. Functions that receive these types as arguments can read them, compute new values from them, and return those new values, but they cannot modify the caller's original data. This guarantee is valuable because it means you never need to worry about a function accidentally corrupting a string or a number that you passed to it. You can pass immutable data freely without defensive copying.

The practical implications for function design

Functions that modify their mutable arguments are said to have side effects: observable changes to the state of the program beyond returning a value. Side effects are not inherently bad, but they should be intentional and documented. A function called sort_items that modifies a list in place is doing exactly what its name suggests. A function called validate_data that unexpectedly removes invalid entries from a list is surprising and likely to cause bugs when callers do not anticipate the modification.

When you write a function that modifies a mutable argument, make the modification explicit in the function name. Words like sort, update, append, remove, and filter commonly signal in-place modification. When you write a function that should not modify its arguments but needs to work with mutable data internally, create a copy at the start of the function. This protects the caller's data and makes the function easier to reason about because its only effect is the return value.

The cleanest pattern, and the one recommended by most Python style guides, is for functions to return new values rather than modifying arguments in place, unless the function's entire purpose is mutation. A function that processes a list should return a new list with the changes applied rather than modifying the caller's list. This makes the function testable and composable because its behavior depends only on its inputs and its effect is captured entirely in its return value. The article on pure functions and side effects in Python explores this functional style in detail.

Rune AI

Rune AI

Key Insights

  • Python passes arguments by assignment: the parameter name is bound to the same object the caller provided.
  • Reassigning a parameter inside a function does not affect the caller's variable; it only changes what the local name points to.
  • Modifying a mutable argument through its methods or by assigning to its elements affects the original object visible to the caller.
  • Immutable types like int, str, and tuple cannot be modified in place; any operation creates a new object.
  • To prevent a function from modifying a list, pass a copy: func(my_list.copy()) or func(list(my_list)).
RunePowered by Rune AI

Frequently Asked Questions

Does Python use pass-by-value or pass-by-reference?

Python uses a model called pass-by-assignment or pass-by-object-reference. When you pass an argument to a function, Python binds the parameter name to the same object that the caller passed. The function gets a reference to the object, not a copy. However, reassigning the parameter inside the function does not affect the caller's variable because the parameter is a separate name, not an alias for the caller's variable.

Why did my list change inside the function but my integer did not?

Lists are mutable, so when a function modifies the list object through methods like append or by assigning to an index, it modifies the original object that both the caller and the function reference. Integers are immutable, so any operation that appears to change an integer actually creates a new integer object and rebinds the local parameter to it, leaving the caller's original integer unchanged.

How can I prevent a function from modifying a list I pass to it?

Pass a copy of the list instead of the original. You can create a shallow copy with list(my_list) or my_list.copy(), or a deep copy for nested structures with copy.deepcopy(my_list). If you control the function, document whether it modifies its arguments so callers know when to pass copies.

Conclusion

Python's argument-passing model, pass-by-assignment, is consistent and predictable once you understand the distinction between rebinding a name and modifying an object. Functions receive references to the caller's objects, not copies, which means mutable arguments can be modified in place. Understanding this model is what lets you write functions that intentionally modify arguments when appropriate, and avoid accidental modifications when they are harmful.