Use immutable sentinels, usually None, for defaults that might otherwise hold mutable state. Then create a fresh list or dictionary inside the function when no value is passed. This prevents values from persisting across calls, which is the source of the bug. It also makes the function’s behavior predictable and easier to test.
Why mutable defaults create shared state across calls
The bug comes from when Python evaluates default parameter values, at function definition time, not each time the function is called. If that default is a list, dictionary, or other mutable object, every call that omits the argument reuses the same object. A later append, update, or nested mutation can therefore leak into future calls and make behavior look random.
This is not just a style issue. It changes the function’s contract: the absence of an argument should usually mean “use a fresh value,” not “reuse prior call history.” The safest mental model is that default arguments are part of the function object itself, so any mutable default becomes shared state unless you deliberately replace it inside the function.
For Python developers, the practical consequence is that bugs often appear only after several calls, which makes them easy to miss in simple tests. Code review should treat any mutable default as a red flag, especially in helper functions, recursive code, and utility methods that are reused in many places.
How to write the function so each call gets a fresh object
The standard pattern is to use an immutable sentinel, usually OWASP Cheat Sheet Series is not the source of the bug, but the same disciplined habit applies here: default to a harmless placeholder such as None, then create the list or dictionary inside the function when the caller passed nothing. That keeps each call isolated and makes the code’s intent explicit.
When the default is genuinely optional, the internal branch should be simple and predictable. If the parameter is still the sentinel, allocate a new container and use it only for that invocation. If the caller supplied a value, use that value as-is, while still validating that it is the expected type or shape before mutating it.
This pattern also helps with readability because the default value and the working value are clearly separated. A reviewer can immediately see that the function starts with no shared state, then constructs its own local state when needed. That is much easier to reason about than a function that silently accumulates data across calls.
What to watch for in tests, refactors, and code review
Developers should pay special attention to functions that mutate their arguments, cache results, or build up collections over time. Those are the places where a mutable default is most likely to cause hidden coupling between calls. The problem can also reappear during refactors when a harmless-looking parameter is changed from a scalar to a list or dict without revisiting the default.
Tests should cover repeated calls, not just one call at a time. A good check is to call the function twice with no argument and confirm that the second result does not contain leftovers from the first. That kind of test catches the exact failure mode that mutable defaults introduce.
If a default must remain mutable for design reasons, the team should treat that as an intentional exception rather than an accident. In practice, that means documenting the behavior clearly, limiting mutation to narrow cases, and being prepared for extra review because the function now depends on persisted state.
Practitioner Guidance
What to verify: Review every function definition for list, dict, or set defaults, and confirm whether the code relies on persistence between calls. If persistence is not intentional, replace the default with None or another immutable sentinel and instantiate the working object inside the function.
Common mistake: Developers often fix the symptom by copying the mutable default inside the function, but that still leaves the shared object in the signature and makes the behavior harder to audit. The cleaner rule is to ensure the default itself cannot accumulate state.
Practitioner takeaway: Treat mutable defaults as shared hidden state, not as a convenience. If a caller should get a fresh container, make that freshness explicit in the function body rather than relying on the parameter default.
Related resources from NHI Mgmt Group
- Why does leaving default database settings in place create risk for web applications?
- What do teams get wrong about making security ownership shared across developers and CTOs?
- Why does code signing become risky when developers have broad access to signing keys?
- What breaks when developers rely on route guards instead of server-function checks for tenant data?
Deepen Your Knowledge
Reviewed and updated by the NHIMG editorial team on September 26, 2026.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org