What Are Default Values
In Python, functions can have default values for their arguments. This can be VERY useful if you want to make one or more of your arguments optional. For example, say we had a function that printed a holiday greeting to our friends! We could set up our function to expect a name for our friend but give it a default value in case one isn’t provided:
def holiday_greeting(greeting, name = "friend"):
print(f"{greeting}, {name}!")
holiday_greeting("Happy Thanksgiving", "Dan")
# output --> Happy Thanksgiving, Dan!
holiday_greeting("Merry Christmas")
# output --> Merry Christmas, friend!
The first time we called our holiday_greating() function, we passed it a string of “Dan” as a value to our name variable. When our function is called the second time, no value is passed for name so our default value of “friend” is used. Setting default values can come in pretty handy when working in python, but there is something you should know that may prevent you from running into the debugging nightmare I found myself in. When deciding to set a default value make sure to use an immutable data type, such as int, float, decimal, bool, string, or tuple. If you try to use a mutable data type, such as a list, dictionary, or a set, you may run into some unexpected behavior that could prove frustrating and time consuming to troubleshoot! Let’s explore an example of using an empty list as a default value and see what happens.
MUTABLE DEFAULT VALUES
def function1(num, num_list = []):
num_list.append(num)
print(num_list)
In the function we defined above two arguments are expected. The first, num, is required while the second argument, num_list, is optional and expects a list. The function simply takes the number passed in as num and appends it to the list pass in as num_list. Let’s look at an example of our function being called twice with no second argument passed in:
function1(1)
# [1] <--- Expected Output
# [1] <--- Actual Output
function1(2)
# [2] <--- Expected Output
# [1, 2] <--- Actual Output
Below each function you can see what I expected the output to be versus what the actual output was. The first time the function got called it acted exactly as I expected it to, but when the function got called a second time something odd happened! There were two items, [1, 2], in my list when I expected to see a list with just the one number, 2, I passed in as an argument. The second number in the list is the number I passed into the function, but where did that first number come from? Upon closer inspection it seems like it’s the number I passed in the first time we called our function. How can that be? When I called function1() the second time without a second argument it should have defaulted to an empty list to append our number to. Why is our first number still in the list?
The answer lies in the way in which Python evaluates default arguments. Python’s default arguments are only evaluated once when the function is first defined, not every time the function is called. Since every call to our function points to the same value that was evaluated once when the function was defined, if our default value is a mutable type and we mutate it, it does exactly that! When we defined our function, the default value for num_list was evaluated and set to an empty list. The first time we called the function, we appended a number to that empty list and mutated it. When we called our function the second time, our number was then appended to the list we had just mutated and now includes the number from our first call! This took me FOREVER to debug!
So now that we know what happens when we set a mutable data type as a default value, how can we get around this issue?
What We Can Do Instead of Using Mutable Default Values
If you find yourself in a situation where you would like a function or class to have the option of passing in a mutable data type as an argument there is an easy solution to avoid the problem I ran into. Instead of setting the default value to a mutable type, set it equal to None and then put a conditional inside of your function that will create a new empty list if no list is passed in. Let’s see what this might look like:
def function2(num, num_list = None):
if num_list == None:
num_list = []
num_list.append(num)
print(num_list)
Here we see that if no second argument is passed in for num_list then the value defaults to None. We then check to see if num_list == None and if it does we create a new empty list right there and set that equal to num_list. From here we can append our number to the list and it should function as intended!
While Python encourages writing clean and expressive code, the use of mutable defaults can lead to hidden bugs and unpredictable behavior. Understanding the concept of shared mutable objects is crucial for writing robust and maintainable code. By following best practices and opting for immutable defaults, you can avoid the perils of mutable defaults and ensure the reliability of your Python code. Hopefully this blog can save you from a potentially frustrating debugging issue!
Top comments (0)