{}const=>[]async()letfn</>var
DevelopmentPython

Why is your Python code behaving strangely? Mutable default arguments trap

We analyze one of the most insidious Python errors, which causes functions to "remember" previous calls. Find out why an empty list suddenly becomes non-empty and how to work with default arguments correctly.

К

Kodik

Author

4 min read

What does the problem look like?

Imagine you are writing a function to add tasks to a to-do list:

def add_task(task, todo_list=[]):
    todo_list.append(task)
    return todo_list

# Add the first taskprint(add_task("Buy milk"))  # ['Buy milk']e second taskprint(add_task("Walk the dog"))  # Expected: ['Walk the dog']                             # We get: ['Buy milk', 'Walk the dog']

⚠️ What happened? We expected to get a new list with one task, but the second task was added to the first! Moreover, we did not save the previous list anywhere. Magic? No, these are mutable default arguments.

🔥 100,000+ students already with us

Tired of reading theory?
Time to code!

Kodik — an app where you learn to code through practice. AI mentor, interactive lessons, real projects.

🤖 AI 24/7
🎓 Certificates
💰 Free
🚀 Start learning
Joined today

Why is this happening?

When Python creates a function, it creates a default object once at the time of defining the function, and not at each call. This object is saved and reused on each subsequent call.

Let's see what's really going on:

def add_task(task, todo_list=[]):
    todo_list.append(task)
    print(f"List ID: {id(todo_list)}")
    return todo_list

add_task("First task")   # List ID: 140234567891234add_task("Second task")    # List ID: 140234567891234 (the same!)add_task("Third task")    # List ID: 140234567891234 (and again!)

💡 Key point: The same list object is used each time. All function calls work with the same list in memory, so all changes are accumulated.

What types of data are affected?

This problem applies to all mutable data types in Python. This includes lists, dictionaries, sets, and custom objects. But numbers, strings and tuples (immutable types) work normally, because they cannot be changed, you can only create new ones.

# Problem code with dictionarydef add_user(name, users_dict={}):
    users_dict[name] = True
    return users_dict

print(add_user("Alexey"))  # {'Alex': True}print(add_user("Maria"))    # {'Alex': True, 'Maria': True} - both are here!# Safe code with the string (immutable)def greet(name, greeting="Hello"):    greeting = greeting + ", " + name
    return greeting

print(greet("Ivan"))   # Hi Ivanprint(greet("Olga"))  # Hi Olga - everything works as expected

The right decision.

The classic solution to this problem is to use None as the default value, and then create a new object inside the function:

def add_task(task, todo_list=None):
    if todo_list is None:
        todo_list = []
    todo_list.append(task)
    return todo_list

# Now everything works correctlyprint(add_task("Buy milk"))        # ['Buy milk']print(add_task("Walk the dog"))   # ['Walk the dog']

✅ Rule: Each time we don't pass the list, a new empty list is created. Problem solved!

The same works for dictionaries:

def create_profile(name, data=None):
    if data is None:
        data = {}
    data['name'] = name
    data['created_at'] = 'today'
    return data

profile1 = create_profile("Alexey")
profile2 = create_profile("Maria")

print(profile1)  # {'name': 'Alex', 'created_at': 'today'}print(profile2)  # {'name': 'Maria', 'created_at': 'today'}

When can it be useful?

Interestingly, sometimes this behavior can be used specifically, for example, for caching results:

def get_config(config_cache={}):
    if 'data' not in config_cache:
        print("Loading configuration...")
        config_cache['data'] = "important settings"
    return config_cache['data']

print(get_config())  # Loading configuration... important settingsprint(get_config())  # important settings (no longer loading)

💡 Note: But in real code, it is better to use special tools like functools.lru_cache for caching, so that the code is clearer.

How to avoid this mistake?

Just remember a simple rule: never use mutable objects (lists, dictionaries, sets) as default values in function arguments. Always use None and create the desired object inside the function.

This error is so common that many linters (code validation tools) specifically warn about it. If you are using PyCharm, VSCode with pylint or flake8, you will see a warning when you try to write such code.

Learn Python correctly the first time!

This and many other important topics can be studied in detail in Codice - analyze all the nuances with clear explanations and consolidate them in practice with interesting tasks. Each topic is accompanied by examples from real development, and practical tasks help to immediately apply the acquired knowledge.

And if you have questions during the training or want to discuss complex issues, we already have more 2000 like-minded people in active Telegram channel, where they will always help you understand and support you on the way to programming!

🎯Stop procrastinating

Liked the article?
Time to practice!

In Kodik, you don't just read — you write code immediately. Theory + practice = real skills.

Instant practice
🧠AI explains code
🏆Certificate

No registration • No card