Say you wrote a small tagging function, def add_tag(tag, tags=[]). The first call returns ['python']. But the second call, add_tag('web'), returns ['python', 'web'] instead of ['web']. You passed nothing, yet the last call's value is still there. After reading this you will be able to explain why, and to choose what to write for an ordinary function, for values like the call time, for dataclass fields and for a cache you keep on purpose.
The examples were run on 2026-10-04 on macOS 27.0.1 with Python 3.12.14 (a build installed with uv). The first example was also run on the Python 3.9.6 that ships with macOS and printed the same output. All tag values are made-up data for illustration.
A default is made when def runs, not when you call
In Python, def is a statement that runs, not a declaration. When the module is loaded, the def line runs once and creates the function object, and the expression in the default position is evaluated at that moment too. With tags=[], a single empty list is created right then. The Function definitions section of the Python Language Reference says default parameter values are evaluated once, left to right, when the definition is executed, and every later call uses the same pre-computed value.
That list is kept in the function object's __defaults__ attribute. A call that does not pass tags binds the parameter to this object instead of creating a new list. So tags.append(tag) looks like it fills a local variable, but it actually changes the one list attached to the function.
Calling it four times
We called it four times, mixing calls that omit the argument with one that passes a list. The first call gave ['python'] and the second ['python', 'web']. The third, add_tag('ai', ['own']), changed only the list it was given, to ['own', 'ai'], and left the default alone. When the fourth call omitted the argument again, the result was ['python', 'web', 'git']. Printing add_tag.__defaults__ afterwards showed that the original ([],) had become (['python', 'web', 'git'],).
You can also confirm it is one object. Store the results of two calls without the argument in a and b: a is b is True, and so is a is add_tag.__defaults__[0]. That is why a, which held the first result, reads ['x', 'y'] after the second call. If you saved a result somewhere, later calls change that saved value too.
It is not only about lists. A log function with at=datetime.now().strftime("%H:%M:%S") as its default, called twice two seconds apart, printed the same time, 22:04:54, on both lines. A time string cannot change, so nothing piles up, but it is frozen at the moment the module was loaded for the same reason. A function call in the default position happens only once, at definition time.
![Python 3.12.14 output. defaults.py list: defaults before ([],), call 1 ['python'], call 2 ['python', 'web'], call 3 ['own', 'ai'], call 4 ['python', 'web', 'git'], defaults after (['python', 'web', 'git'],). ids: a is b True, a is __defaults__[0] True, a is ['x', 'y']. time: 22:04:54 first, and still 22:04:54 two seconds later.](/images/en/python-mutable-default-argument-run-01.png)
Usually, default to None
If each call needs a new list, default to None and create the list in the body. Both the Python tutorial and the programming FAQ recommend this.
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
The fixed function returned ['python'] on the first call and ['web'] on the second, and __defaults__ stayed (None,). The tags = [] in the body runs on every call, so each call gets a new list. For the call time, take at=None the same way and call datetime.now() in the body.
if tags is None: is more precise than if not tags:. not tags also replaces an empty list the caller passed on purpose. Checking with an empty list mine: after calling the if not tags: version, mine was still []; with the is None version it held ['python'].
One thing remains. The None pattern stops the default from being shared, but it still changes a list the caller passes in. In the run, after passing mine = ['own'], mine itself was ['own', 'ai'] and was the same object as the return value. If the function must not touch the original, copy it first in the body, for example tags = list(tags), then append.
dataclasses, and a cache kept on purpose
For dataclass fields, Python blocks the same mistake up front. Declaring tags: list = [] raised ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory as soon as the class was defined. According to the dataclasses documentation, since 3.11 it rejects any unhashable default rather than the specific types list, dict and set, and it calls this a partial protection that uses unhashability as an approximation of mutability. Use field(default_factory=list) instead and each instance gets its own list; in the run p.tags was ['python'] and q.tags was [], two different objects.
![Python 3.12.14 output. none: call 1 ['python'], call 2 ['web'], defaults (None,). copy: returned ['own', 'ai'], the caller's list is now ['own', 'ai'], same object True. dataclass: ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory. With default_factory p.tags is ['python'], q.tags is [], same object False.](/images/en/python-mutable-default-argument-run-02.png)
Sometimes this behaviour is used on purpose. The programming FAQ shows def expensive(arg1, arg2, *, _cache={}) as an example of memoizing, remembering the results of a slow function. The keyword-only parameter and the leading underscore signal that callers are not meant to pass it. For the same goal, functools.lru_cache from the standard library states the intent more clearly.
A linter can catch it before review. Ruff flags this pattern as rule B006 (mutable-argument-default) and offers a fix that switches to None, but marks the fix unsafe because the original intent might have been a cache. This comes from the Ruff documentation; neither Ruff nor Pylint was installed in this run's environment, so they were not run here.
When a function signature has something freshly built or called in a default position, such as =[], ={}, =set() or =datetime.now(), remember it is made only once, at definition time, and decide whether to switch to None or, if it really is a cache, to say so in its name.
Add your perspective.
Share a question, another approach, or something you have tried.
Checking sign-in…
Loading comments…