0

I have this code snippet

def fun(i = []):
    print("i is", i)
    i.append(1)
    
for k in range(5):
    print("k is", k)
    fun()

And its output

k is 0
i is []
k is 1
i is [1]
k is 2
i is [1, 1]
k is 3
i is [1, 1, 1]
k is 4
i is [1, 1, 1, 1]

My understanding till today was, For every function call the parameter will use its default value if no explicit value provided. But here even after not providing any value for i while calling fun from the loop you can see i is getting updated with respect to the previous function call.

I was expecting something like this as the answer:

k is 0
i is []
k is 1
i is []
k is 2
i is []
k is 3
i is []
k is 4
i is []

Don't we have scope of a function parameter within that function call ?

Hari Krishnan
  • 5,992
  • 9
  • 37
  • 55

1 Answers1

1

Beware of default mutable arguments!

The default mutable arguments of functions in Python aren't really initialized every time you call the function. Instead, the recently mutated values are used as the default value.

The first time that the function is called, Python creates a persistent object for the mutable container. Every subsequent time the function is called, Python uses that same persistent object that was created from the first call to the function.

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg
Output:

>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']

When we explicitly passed [] to some_func as the argument, the default value of the default_arg variable was not used, so the function returned as expected.

Output:

>>> some_func.__defaults__ #This will show the default argument values for the function
([],)
>>> some_func()
>>> some_func.__defaults__
(['some_string'],)
>>> some_func()
>>> some_func.__defaults__
(['some_string', 'some_string'],)
>>> some_func([])
>>> some_func.__defaults__
(['some_string', 'some_string'],)

A common practice to avoid bugs due to mutable arguments is to assign None as the default value and later check if any value is passed to the function corresponding to that argument. Example:

def some_func(default_arg=None):
    if default_arg is None:
        default_arg = []
    default_arg.append("some_string")
    return default_arg

Source: wtfpython

Thanks to TomKerzas for providing another excellent resource for information on this topic here

Alexander
  • 16,091
  • 5
  • 13
  • 29
  • 1
    The real issue is that default argument values are evaluated once, when the function is defined. After that, the same default argument values are used in every call to the function that omits those arguments. This results in shared references to those default values. – Tom Karzes May 01 '23 at 04:23
  • 1
    Note that saying "the recently assigned value to them is used as the default value" is incorrect. There is no assignment. These are bindings that are made when the function is defined. Since they cannot be changed, the concept of "recent" is inapplicable. – Tom Karzes May 01 '23 at 04:27
  • I'm sure this has come up on SO before, but I didn't find the link. But it's described [here](https://docs.quantifiedcode.com/python-anti-patterns/correctness/mutable_default_value_as_argument.html). – Tom Karzes May 01 '23 at 04:28
  • @TomKarzes I think you are splitting hairs here, and you should probably take it up with the maintainers of `wtfpython` if you feel that strongly about it. The link to the repo is at the bottom of my answer. – Alexander May 01 '23 at 04:30
  • @Alexander Thanks for the answer, So the scope of this parameter will not end even after the function call id done? That is contradicting with some basics that I have studied.. Anyway this is a new information for me. Thanks again.. – Hari Krishnan May 01 '23 at 04:37
  • 1
    @HariKrishnan In my experience it is best to think of default arguments as global variables that can't be used from the global scope. The two behave similarly though. – Alexander May 01 '23 at 04:39
  • I tried this def fun(i = []): print("i is", i) i = [2] for k in range(5): print("k is", k) fun() and the answer is k is 0 i is [] k is 1 i is [] k is 2 i is [] k is 3 i is [] k is 4 i is [] So it is about updating the parameter I guess. – Hari Krishnan May 01 '23 at 04:39
  • @Alexander The term "assignment" is pretty well defined and doesn't apply here. Modifying a list that a default argument references is not an assignment to the default argument. I can't state it any more clearly than that. – Tom Karzes May 01 '23 at 04:42
  • @HariKrishnan *this has nothing to do with scope*, this is important to understand. **Scope** applies to *variables*. The *variable ceases to exist and is out of scope when the function terminates*. But that doesn't mean a *new object is created each time you call the function*. IOW, **objects don't have scopes** – juanpa.arrivillaga May 01 '23 at 04:45