How to pass by reference in Python
Learn how to pass by reference in Python. This guide covers methods, tips, real-world applications, and how to debug common errors.

To manage data and avoid bugs, you must understand how Python handles object references. The language uses a 'pass by object reference' model, which has unique behaviors.
In this article, we'll explore techniques to simulate pass by reference behavior. You'll find practical tips, real world applications, and advice to debug common issues with mutable and immutable objects.
Basic understanding of Python's parameter passing
def modify_value(x):
x += 10
print(f"Inside function: x = {x}")
original = 5
modify_value(original)
print(f"After function call: original = {original}")--OUTPUT--Inside function: x = 15
After function call: original = 5
When you pass an integer like original to the modify_value function, its value outside the function call remains unchanged. Inside the function, x is a local variable that initially points to the same integer object as original.
Because integers are immutable, the += operation doesn't alter the original object. It creates a new integer with the value 15 and reassigns the local variable x to point to it. The original variable is never affected and continues to point to 5.
Working with mutable objects
Unlike with immutable types, you can use mutable objects like lists, dictionaries, and custom classes to modify data directly from within a function.
Using lists to simulate pass by reference
def append_value(my_list):
my_list.append(42)
numbers = [1, 2, 3]
append_value(numbers)
print(f"Modified list: {numbers}")--OUTPUT--Modified list: [1, 2, 3, 42]
The append_value function successfully modifies the numbers list because lists are mutable. When you pass the list to the function, you're passing a reference to the original object. Any in-place modifications will affect the list outside the function's scope. Understanding copying lists in Python helps distinguish between reference passing and creating independent copies.
- The
append()method is an in-place operation, meaning it alters the list directly instead of creating a new one. - This is why the change is reflected in the
numbersvariable after the function call, effectively simulating pass-by-reference behavior.
Using dictionaries for reference-like behavior
def update_user(user_dict):
user_dict["age"] += 1
user_dict["status"] = "updated"
user = {"name": "Alice", "age": 30}
update_user(user)
print(user)--OUTPUT--{'name': 'Alice', 'age': 31, 'status': 'updated'}
Dictionaries, much like lists, are mutable, making them excellent for this purpose. When you pass the user dictionary to the update_user function, you're giving the function a direct link to the original object, not a copy. This enables efficient accessing dictionary values in Python.
- This means any modifications—like incrementing the
"age"or adding a new"status"key—directly alter the originaluserobject.
As a result, the changes are reflected outside the function, providing a powerful way to manage and update complex data structures across your code.
Using custom objects for reference semantics
class Counter:
def __init__(self, value=0):
self.value = value
def increment(counter_obj, amount=1):
counter_obj.value += amount
counter = Counter(5)
increment(counter, 3)
print(f"Counter value: {counter.value}")--OUTPUT--Counter value: 8
Custom objects, like instances of the Counter class, are mutable. When you pass an object to a function, you're giving that function a direct reference to it, which allows for in-place modifications.
- The
incrementfunction accesses and changes thevalueattribute of the originalcounterobject. - Because the function operates on the object itself—not a copy—the change persists outside the function's scope.
This makes custom classes a clean, object-oriented way to manage state that needs to be updated across different parts of your code. Understanding creating custom classes in Python is essential for building robust object-oriented applications.
Advanced techniques
Beyond the standard mutable types, Python offers more specialized techniques for managing state and achieving reference-like behavior in tricky situations.
Using list wrappers for immutable types
def modify_number(num_wrapper):
num_wrapper[0] += 10
x = [5] # Wrapping the integer in a list
modify_number(x)
print(f"Modified value: {x[0]}")--OUTPUT--Modified value: 15
This clever trick lets you modify an immutable value, like an integer, from within a function. By wrapping the number in a list—a mutable container—you pass a reference to the list itself. The function then operates on the list's contents, not a copy of the number.
- The
modify_numberfunction accesses the integer using its index,num_wrapper[0]. - Since the list is mutable, the
+=operation changes the value stored at that index directly. - The change persists outside the function because the original list object was modified.
Using function attributes to store state
def create_stateful_function():
def stateful(increment=1):
stateful.counter += increment
return stateful.counter
stateful.counter = 0
return stateful
counter_func = create_stateful_function()
print(counter_func(5))
print(counter_func(3))--OUTPUT--5
8
Since functions in Python are objects, you can attach attributes to them to store a persistent state. This technique uses a factory function, create_stateful_function, to build and return a function that remembers information between calls. Understanding the fundamentals of creating functions in Python is essential before exploring advanced techniques like function attributes.
- The factory defines a nested function named
stateful. - It then attaches a
counterattribute directly to thestatefulfunction object, initializing its value. - The returned function,
counter_func, carries this state with it. Each call modifies thecounterattribute on the function itself, allowing the value to persist.
Using the nonlocal keyword with closures
def create_counter():
count = 0
def increment(amount=1):
nonlocal count
count += amount
return count
return increment
counter = create_counter()
print(counter(2))
print(counter(3))--OUTPUT--2
5
The nonlocal keyword lets a nested function modify variables from its enclosing scope. In this example, create_counter sets up a count variable and returns an increment function. This returned function—a closure—remembers the count variable even after create_counter has finished running.
- The
nonlocal countstatement is the key. It tells theincrementfunction to modify thecountfrom the outer scope instead of creating a new local one. - This allows the returned
counterfunction to maintain a persistent state, updating the samecountvariable across multiple calls.
Move faster with Replit
Replit is an AI-powered development platform that comes with all Python dependencies pre-installed, so you can skip setup and start coding instantly. This environment helps you move from learning individual techniques to building complete applications faster.
Instead of piecing together functions, you can use Agent 4 to build the entire app. Describe the tool you want to build, and Agent will take it from idea to working product. For example, you could build:
- A real-time analytics tool that uses a stateful function, like the
create_counterexample, to track live events. - A configuration manager that updates a user's settings stored in a dictionary, applying the same logic as the
update_userfunction. - A batch processing utility that modifies a list of custom objects in-place to update inventory levels.
Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.
Common errors and challenges
Navigating Python's object reference model means sidestepping common pitfalls with mutable arguments, variable scope, and unexpected reassignments.
One of the most frequent traps is using a mutable object, like a list or dictionary, as a default argument. Because Python creates default arguments only once—when the function is defined—that same object is reused across all subsequent calls that don't specify a value. This can lead to unexpected shared state and bugs that are hard to trace.
- To avoid this, you should use
Noneas the default and create a new mutable object inside the function if no argument is provided.
It's also easy to get confused about variable scope. When you pass an object to a function, the parameter name inside the function becomes a new reference to that same object. If you reassign the parameter to a completely new object, the original variable outside the function remains unaffected. The link is broken.
- True pass-by-reference behavior only occurs when you modify the object's internal state, not when you reassign the variable name itself.
The += operator can be another source of confusion because its behavior depends on the object's type. For immutable types like numbers, it creates a new object and reassigns the variable. For mutable types like lists, it typically modifies the object in-place, which is why it works for simulating pass-by-reference.
- This dual behavior means you must always be aware of whether you're working with a mutable or immutable type to predict the outcome.
Avoiding the mutable default argument trap
Using a mutable object like a list as a default argument is a classic Python pitfall. The default list is created only once when the function is defined—not each time it's called—leading to unexpected shared state between function calls.
The following code demonstrates this behavior with an add_item function. Notice how the second call doesn't start with a fresh list, producing a surprising result.
def add_item(item, shopping_list=[]):
shopping_list.append(item)
return shopping_list
list1 = add_item("apple")
print(f"First list: {list1}")
list2 = add_item("banana")
print(f"Second list: {list2}")
Because the default shopping_list is created only once, both calls to the add_item function modify the same list object. This is why the second call appends to the first list. The corrected implementation below shows how to fix this.
def add_item(item, shopping_list=None):
if shopping_list is None:
shopping_list = []
shopping_list.append(item)
return shopping_list
list1 = add_item("apple")
print(f"First list: {list1}")
list2 = add_item("banana")
print(f"Second list: {list2}")
The corrected add_item function avoids the trap by setting the default to None. An if statement then creates a new list only when one isn't provided. This simple change guarantees that each function call operates independently, preventing unexpected shared state.
- Always use this
None-as-default pattern when your function needs a mutable default argument, like a list or dictionary. It's a crucial habit for writing predictable, bug-free Python code.
Debugging variable scope issues with parameters
Debugging variable scope issues with parameters
When a function like update_total tries to modify a global variable, Python's scope rules can cause unexpected errors. Because the += operator attempts an in-place modification, you'll get an UnboundLocalError instead of the expected result. Check out the example.
total = 100
def update_total(amount):
total += amount
return total
print(update_total(50))
The update_total function fails because the += operator makes Python treat total as a local variable. Since it isn't defined within the function's scope before being modified, an UnboundLocalError is raised. The corrected code below shows how to fix this.
total = 100
def update_total(amount):
global total
total += amount
return total
print(update_total(50))
The corrected code works because the global total statement explicitly tells the update_total function to modify the total variable from the global scope. This prevents Python from creating a new, local variable and resolves the UnboundLocalError, allowing the += operator to update the original value as intended through code repair techniques.
- This is a common pattern when a function needs to modify a global state, such as a shared counter or a configuration setting.
Understanding modification vs. reassignment with the += operator
The += operator can be tricky. It doesn't always modify an object in-place. Its behavior changes depending on whether the object is mutable or immutable. The following code demonstrates this with the modify_string function, which attempts to alter a string.
def modify_string(text):
text += " world"
print(f"Inside function: {text}")
greeting = "hello"
modify_string(greeting)
print(f"Outside function: {greeting}")
Inside modify_string, the += operator doesn't alter the original string. It creates a new one and reassigns the local variable text. The original greeting variable remains untouched, pointing to its initial value. The following example demonstrates a common pattern for achieving the intended modification.
def modify_list(items):
items += ["world"]
print(f"Inside function: {items}")
greeting = ["hello"]
modify_list(greeting)
print(f"Outside function: {greeting}")
The modify_list function works because lists are mutable. Unlike with strings, the += operator modifies the list in-place, extending the original object instead of creating a new one. This is why the change to the greeting list persists outside the function's scope.
- You'll need to watch for this dual behavior whenever using augmented assignment operators like
+=, as the outcome depends entirely on whether the object is mutable.
Real-world applications
These pass-by-reference techniques are not just theoretical; they are fundamental to building common features like shopping carts and data pipelines.
Implementing a simple shopping cart
A shopping cart is a classic real-world example where a mutable dictionary is passed to a function like add_to_cart to manage its state in-place.
def add_to_cart(cart, item, quantity=1):
if item in cart:
cart[item] += quantity
else:
cart[item] = quantity
cart = {}
add_to_cart(cart, "apple", 3)
add_to_cart(cart, "banana", 2)
add_to_cart(cart, "apple", 2) # Adding more apples
print(f"Shopping cart contents: {cart}")
The add_to_cart function works by directly modifying the cart dictionary you pass to it. This approach is efficient because it avoids creating new dictionaries with every call, instead operating on the original object.
- The function first checks if an
itemis already in thecart. - If the item exists, it updates the quantity using the
+=operator. Otherwise, it adds the new item.
Because these changes happen on the original dictionary, the cart object correctly reflects all updates after each function call, like when more apples are added.
Building a data processing pipeline
Data processing pipelines chain together a series of functions to transform data in stages, showcasing a powerful way to pass behavior as an argument.
def process_data(data_pipeline, raw_data):
result = raw_data
for processor in data_pipeline:
result = processor(result)
return result
def clean_data(data):
return [item for item in data if item > 0]
def double_values(data):
return [item * 2 for item in data]
pipeline = [clean_data, double_values]
data = [1, -3, 5, 0, 8, -2]
result = process_data(pipeline, data)
print(f"Original data: {data}")
print(f"Processed data: {result}")
This example demonstrates how functions in Python can be treated like any other data. The pipeline is a simple list that holds the clean_data and double_values functions. The process_data function then iterates over this list, calling each function in sequence.
- The result from
clean_datais passed directly as the input todouble_values. - This creates a modular workflow where each step is a self-contained function.
This approach is powerful because you can easily reorder, add, or remove steps from the pipeline without changing the core logic using vibe coding.
Get started with Replit
Put these concepts into practice with Replit Agent. Describe what you want to build, like “a shopping cart app that uses a dictionary” or “a data pipeline that cleans and doubles values in a list.”
Replit Agent writes the code, tests for errors, and deploys your application. You just provide the instructions. Start building with Replit.
Describe what you want to build, and Replit Agent writes the code, handles the infrastructure, and ships it live. Go from idea to real product, all in your browser.
Describe what you want to build, and Replit Agent writes the code, handles the infrastructure, and ships it live. Go from idea to real product, all in your browser.



