If you're coming to Python from languages like C++ or Java, you might be searching for Python's "call by reference" mechanism. Maybe you want to increment a counter in a function and have that change reflected outside. Here's the thing: Python doesn't have call by reference in the traditional sense.
But don't worry! Once you understand what Python actually does, you'll see it's both elegant and practical. Let's dive in.
First, we need to shift our mental model. In many languages, variables are like boxes that hold values. In Python, variables are more like name tags that point to objects in memory.
When you write:
count = 5
You're not creating a box labeled "count" that contains the number 5. Instead, you're:
This distinction is crucial for understanding how function arguments work in Python.
Python uses something called "pass by assignment" or "call by object reference." Here's what happens when you pass an argument to a function:
Think of it like this: you're giving the object a second name tag, not making a copy of the object.
Let's see why you can't directly modify an integer in a function:
def try_to_increment(count):
print(f"Inside (before): id={id(count)}, value={count}")
count = count + 1 # This is the key line!
print(f"Inside (after): id={id(count)}, value={count}")
my_count = 5
print(f"Outside (initial): id={id(my_count)}, value={my_count}")
try_to_increment(my_count)
print(f"Outside (after): id={id(my_count)}, value={my_count}")
Output:
Outside (initial): id=140707765997200, value=5
Inside (before): id=140707765997200, value=5
Inside (after): id=140707765997232, value=6
Outside (after): id=140707765997200, value=5
Notice the id
values (memory addresses). Here's what happened:
my_count
and count
pointed to the same integer object 5
(same ID)count = count + 1
, Python:
5 + 1 = 6
6
(different ID!)count
name point to this new objectmy_count
still points to the original 5
objectWhy a new object? Because integers in Python are immutable - they can't be changed after creation. You can't modify a 5 to become a 6; you can only create a new 6.
The simplest and most Pythonic approach is to return the new value:
def increment_count(current_count: int) -> int:
"""Increments a count and returns the new value."""
return current_count + 1
def decrement_count(current_count: int) -> int:
"""Decrements a count and returns the new value."""
return current_count - 1
my_count = 5
print(f"Initial: {my_count}") # Output: Initial: 5
my_count = increment_count(my_count)
print(f"After increment: {my_count}") # Output: After increment: 6
my_count = decrement_count(my_count)
print(f"After decrement: {my_count}") # Output: After decrement: 5
This makes the data flow explicit and clear. It's the preferred approach for simple immutable values like integers, strings, or tuples.
Since Python passes object references, if you pass a mutable object (one that can be modified in place), changes made inside the function will be visible outside. This is because both names point to the same mutable object.
def increment_count_list(count_wrapper: list):
"""Increments the count inside a list wrapper."""
count_wrapper[0] += 1
def decrement_count_list(count_wrapper: list):
"""Decrements the count inside a list wrapper."""
count_wrapper[0] -= 1
my_count_list = [10] # Wrap the count in a list
print(f"Initial: {my_count_list[0]}") # Output: Initial: 10
increment_count_list(my_count_list)
print(f"After increment: {my_count_list[0]}") # Output: After increment: 11
decrement_count_list(my_count_list)
print(f"After decrement: {my_count_list[0]}") # Output: After decrement: 10
Why this works: Lists are mutable. Both my_count_list
and count_wrapper
point to the same list object. When we modify count_wrapper[0]
, we're modifying the contents of that shared list, so the change is visible outside.
def increment_count_dict(counter: dict):
"""Increments the count in a dictionary."""
counter['value'] += 1
def decrement_count_dict(counter: dict):
"""Decrements the count in a dictionary."""
counter['value'] -= 1
my_counter = {'value': 10}
print(f"Initial: {my_counter['value']}") # Output: Initial: 10
increment_count_dict(my_counter)
print(f"After increment: {my_counter['value']}") # Output: After increment: 11
decrement_count_dict(my_counter)
print(f"After decrement: {my_counter['value']}") # Output: After decrement: 10
For more complex scenarios, a custom class often makes the most sense:
class Counter:
def __init__(self, initial_value: int = 0):
self.value = initial_value
def __str__(self):
return f"Counter(value={self.value})"
def increment_counter(counter: Counter):
"""Increments a Counter object."""
counter.value += 1
def decrement_counter(counter: Counter):
"""Decrements a Counter object."""
counter.value -= 1
my_counter = Counter(10)
print(f"Initial: {my_counter}") # Output: Initial: Counter(value=10)
increment_counter(my_counter)
print(f"After increment: {my_counter}") # Output: After increment: Counter(value=11)
decrement_counter(my_counter)
print(f"After decrement: {my_counter}") # Output: After decrement: Counter(value=10)
This approach is clean, readable, and makes your intent clear.
You can use the global
keyword to modify a global variable from within a function:
count = 5
def increment_count_global():
global count
count += 1
def decrement_count_global():
global count
count -= 1
print(f"Initial: {count}") # Output: Initial: 5
increment_count_global()
print(f"After increment: {count}") # Output: After increment: 6
decrement_count_global()
print(f"After decrement: {count}") # Output: After decrement: 5
Warning: Global variables can make code harder to understand, test, and debug. They create hidden dependencies between different parts of your code. Use this approach only when absolutely necessary.
Here's my guide for choosing the right pattern:
Use return values when:
Use mutable containers when:
Use custom classes when:
Avoid global variables except when:
Python's approach might seem strange at first, but it aligns with the language's philosophy:
Once you internalize that everything in Python is an object reference and understand the mutable vs. immutable distinction, the behavior becomes intuitive and predictable.
Immutable types (can't be modified in place):
Mutable types (can be modified in place):
Python doesn't have "call by reference" because it doesn't need it. The combination of pass-by-assignment with mutable and immutable types provides a clean, consistent model that works well once you understand it.
For your counter use case, I'd recommend:
The key is understanding that you're not fighting against Python's design - you're working with it. And once it clicks, you'll find it's actually quite elegant.
Happy coding!