Any meaningful function or procedure needs some sort of data from its calling environment to produce meaningful results. What happens if the data sent to the function gets changed within the function? A function is said to have a side-effect if the supplied values or anything in function's environment like global variable gets updated within the function. This article attempts to explain the concept of mutability or side effect of values passed to a function or procedure in Python.
Let’s start with a few fundamental terms before we attempt to answer Python’s side-effect behavior.
What are parameters?
Data that gets passed to a function as input are called parameters. num1 and num2 in the below function are parameters.
def sum(num1, num2):
return num1 + num2
What are arguments?
Values that are supplied during function invocation are called arguments. So val1 and val2 in the below function call are arguments. Although many use these terms interchangeably.
val1 = 10
val2 = 20
ans = sum(val1, val2)
What are passing arguments by value and reference?
If a new copy of arguments are made during a function call for the supplied parameter, then arguments are said to be passed by value. If a reference of the same variable is passed to a function then arguments are passed by reference.
Are the arguments passed by value or by reference in Python?
Python uses the mechanism pass arguments by sharing object reference during function calls. Let’s examine Python’s behavior using below function example:
def ref_copy_demo(x):
print(f"x = {x}, id = {id(x)}")
x += 45
print(f"x = {x}, id = {id(x)}")
num = 10
print(f"before function call - num = {num}, id = {id(num)}")
ref_copy_demo(num)
print(f"after function call - num = {num}, id = {id(num)}")
# Output
before function call - num = 10, id = 140704100632512
x = 10, id = 140704100632512
x = 55, id = 140704100633952
after function call - num = 10, id = 140704100632512
Here is the illustration of above function call:
Let’s analyze the function call and its output:
- We have used the id function in the above call to get the identity value of the object. Id function returns an integer which is unique and constant for this object during its lifetime. In the above context, it helps us in tracking whether the same object is passed by reference to the function.
- Please note that the id function’s value has changed for the parameter passed before and after variable value change.
- So this means the parameter inside the function remains the same as the argument until there is no change in parameter’s value. Python keeps the reference of the argument passed. But as soon the parameter gets updated, a local copy of the parameter is made leaving the argument value unchanged.
What is a side effect?
Function is said to have a side effect if it changes anything outside of its function definition like changing arguments passed to the function or changing a global variable. For example:
def fn_side_effects(fruits):
print(f"Fruits before change - {fruits} id - {id(fruits)}")
fruits += ["pear", "banana"]
print(f"Fruits after change - {fruits} id - {id(fruits)}")
fruit_list = ["apple", "orange"]
print(f"Fruits List before function call - {fruit_list} id - {id(fruit_list)}")
fn_side_effects(fruit_list)
print(f"Fruits List after function call - {fruit_list} id - {id(fruit_list)}")
# Output
Fruits List before function call - ['apple', 'orange'] id - 1904767477056
Fruits before change - ['apple', 'orange'] id - 1904767477056
Fruits after change - ['apple', 'orange', 'pear', 'banana'] id - 1904767477056
Fruits List after function call - ['apple', 'orange', 'pear', 'banana'] id - 1904767477056
So this function clearly has side effect due to below reasons:
- Id value argument and parameter are exactly the same.
- Argument has additional values added after the function call.
How to create a similar function without side effect?
def fn_no_side_effects(fruits):
print(f"Fruits before change - {fruits} id - {id(fruits)}")
fruits = fruits + ["pear", "banana"]
print(f"Fruits after change - {fruits} id - {id(fruits)}")
fruit_list = ["apple", "orange"]
print(f"Fruits List before function call - {fruit_list} id - {id(fruit_list)}")
fn_no_side_effects(fruit_list)
print(f"Fruits List after function call - {fruit_list} id - {id(fruit_list)}")
# output
Fruits List before function call - ['apple', 'orange'] id - 2611623765504
Fruits before change - ['apple', 'orange'] id - 2611623765504
Fruits after change - ['apple', 'orange', 'pear', 'banana'] id - 2611625160320
Fruits List after function call - ['apple', 'orange'] id - 2611623765504
With explicit call to the assignment during fruits list update, list value changed only within the function as it supposed to be. So this function has no side effect.
Please note we could have also avoided side-effect in the first function call if we would have explicitly made a copy of the list - fn_side_effects(fruit_list[:])
Why is it important to write functions without side-effects?
- Functions with side effects especially when it is unintended could lead to a lot of potential bugs which are harder to debug.
- It is easier to write tests for functions with no side-effects.
- If the function is supposed to change anything in the environment, it must be clearly documented to avoid confusion.
- Care must be taken in writing function definitions containing mutable data types (like Lists, Sets, Dictionary etc…) as their function parameter.
Top comments (3)
That last function still has a risk of a side effect. You shouldn't modify fruits at all, just return the result, so return fruits + [other fruits]... Also use different names, redefining in this way is likely to cause mypy to not be happy.
There's an error in your What are arguments? section. you have the following definition:
You then try to use the
sum
function you defined:However, you defined
sum
to be a function that takes exactly two arguments, and you called it as a function that takes exactly one argument, so that call tosum
won't work:So, you'll need to call it as:
sum(val1, val2)
Thanks for pointing out the issue! Now it is fixed.