If you’ve ever seen a function or class in Python behave as if it’s “remembering” values from earlier calls when it shouldn’t, you’ve likely run into this issue.

Let’s break down what’s happening, why it’s a problem, and how to fix it.

The Bug: Unexpected Shared State Between Function Calls or Class Instances

You may have seen bugs where lists or dictionaries inside a function or class seem to “remember” data from previous calls — even when you expected them to be empty.

Take this example:

def collect_items(item, items=[]): items.append(item) return items
print(collect_items("apple")) # ['apple']
print(collect_items("banana")) # ['apple', 'banana'] ← Wait, what?

Or:

class Tracker: def __init__(self, logs=[]): self.logs = logs
a = Tracker()
a.logs.append("start")
b = Tracker()
print(b.logs) # ['start'] ← Unexpected!

What’s going on? Each call or instance seems to be a sharing state — which shouldn’t happen.

Root Cause: Default Argument Values Are Evaluated Once

In Python, default argument values are evaluated only once, when the function or class is defined — not each time it’s called. If that default is a mutable object (like a list or dictionary), it persists across calls or object instances.

So when you modify that object (e.g. items.append(…)), it modifies the same list in memory every time.

This is one of the most infamous “gotchas” in Python — especially in beginner-to-intermediate level code.

How to Fix It: Using Safe Patterns for Default Arguments

Step 1: Never Use Mutable Types as Default Arguments

Instead, use None and initialize the object inside the function or method:

def collect_items(item, items=None): if items is None: items = [] items.append(item) return items

This ensures that a new list is created each time you call the function.

Step 2: Apply the Same Pattern in Classes

Fix your class constructor like this:

class Tracker: def __init__(self, logs=None): if logs is None: logs = [] self.logs = logs

Now each instance of Tracker() has its own independent list.

Step 3: Be Cautious When Using Mutable Defaults in Third-Party Code

If you’re using a library and noticing strange shared behavior, inspect the source (if possible) or check the documentation. They might be using mutable default arguments internally.

As a workaround, you can explicitly pass your own clean list or dict as an argument if supported.

Mutable default arguments in Python can lead to shared state across function calls or class instances — causing bugs that are hard to trace. This happens because the default object is only created once during function definition.

Best Practices:

  • Always use None as the default value when expecting a list, dict, or set
  • Create a new mutable object inside the function or method
  • Apply this pattern consistently in both functions and class constructors
  • Be mindful of mutable defaults in third-party libraries

Conclusion

This issue can be subtle but dangerous. Shared state across calls or instances can lead to confusing bugs and unexpected behavior. Understanding how Python handles default arguments is an important step in writing clean, reliable code. If you’re building something complex or reviewing shared-state bugs, it’s a smart move to hire Python developers who are familiar with these kinds of edge cases and can write safer, more predictable code.