DEV Community

Anton Shcherbatov
Anton Shcherbatov

Posted on

Why You Shouldn't Use `def func(items=[])`

We assume you already know how to write simple Python programs and understand basic syntax (if, for, functions, lists). Here, we're not discussing how to use the language, but why it works the way it does.

Try to guess what this code prints:

def add(item, items=[]):
    items.append(item)
    return items

print(add(1))
print(add(2))
print(add(3))
Enter fullscreen mode Exit fullscreen mode

If you've never encountered this behavior before, you might expect something like:

[1]
[2]
[3]
Enter fullscreen mode Exit fullscreen mode

Each function call looks independent from the previous one.

However, Python produces a very different result:

[1]
[1, 2]
[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

It looks as if the list somehow "remembers" previous function calls.

But why does this happen?


If you look at this example in isolation, it might seem like Python functions have some kind of hidden memory.

In reality, there is nothing magical going on.

To understand this behavior, we only need to recall two ideas from previous articles:

  • variables don't store objects, they refer to them;
  • lists are mutable objects.

Together, these two facts fully explain what's happening.


When Python encounters a function definition:

def add(item, items=[]):
    ...
Enter fullscreen mode Exit fullscreen mode

the default list is created exactly once.

Not on every function call.

Not each time add() is executed.

But at the moment the function is defined.

We can think of it like this:

add
 │
 └────► items ───► []
Enter fullscreen mode Exit fullscreen mode

The function parameter items is bound to a single list object.

Now the first call happens:

add(1)
Enter fullscreen mode Exit fullscreen mode

Since no second argument is provided, Python uses the existing default list.

After append(), it looks like this:

items ───► [1]
Enter fullscreen mode Exit fullscreen mode

The function finishes execution.

But the list does not disappear.

It continues to exist.


Now the second call happens:

add(2)
Enter fullscreen mode Exit fullscreen mode

One might expect Python to create a new empty list.

But it doesn't.

The same object is reused.

Now the list becomes:

items ───► [1, 2]
Enter fullscreen mode Exit fullscreen mode

The third call behaves the same way.

The same list keeps being modified:

items ───► [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

This is why each call appears to "remember" previous results.


It's important to notice that the issue is not with functions themselves.

If we write the same logic without a function, nothing surprising happens:

items = []

items.append(1)
items.append(2)
items.append(3)

print(items)
Enter fullscreen mode Exit fullscreen mode

The result is:

[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

We already know why this happens.

A list is a mutable object.

Each append() modifies the same object instead of creating a new one.

The function behaves exactly the same way.

The only difference is that the list was created earlier than many people expect.


So how do we write this correctly?

Python has a widely accepted pattern for this case.

Instead of using a mutable default value, we use None:

def add(item, items=None):
    if items is None:
        items = []

    items.append(item)
    return items
Enter fullscreen mode Exit fullscreen mode

Now each call without an explicit list creates a new one.

The behavior becomes predictable:

print(add(1))
print(add(2))
print(add(3))
Enter fullscreen mode Exit fullscreen mode
[1]
[2]
[3]
Enter fullscreen mode Exit fullscreen mode

You might wonder why Python behaves this way at all.

Why not create a new list automatically for each function call?

The reason is simple: default argument values are objects like everything else in Python.

They are created once at function definition time and then reused.

This makes the behavior consistent and efficient.

For immutable objects like numbers or strings, this behavior is usually invisible.

But once a mutable object like a list, dictionary, or set is used as a default value, the effect becomes observable.


From everything we've learned so far, nothing new is actually happening here.

There is an object.

There is a reference to that object.

The object is mutable.

So every function call modifies the same shared list.

This is why experienced Python developers almost never use mutable objects as default argument values.


At this point, we know enough to move one step further.

We've repeatedly said that lists can be modified in place.

But is that always true?

For example, what happens here?

numbers = [1, 2]

numbers += [3]
Enter fullscreen mode Exit fullscreen mode

Is the existing list modified, or is a new list created?

And what if we replace the list with a number?

On the surface, the operation looks the same - but the behavior can be very different.

That's exactly what we'll explore in the next article.

Top comments (0)