Python Mutable Default Arguments — Data-Leak Gotcha
Intermittent personal data leaks? Python's mutable default args cause shared state at definition - avoid this gotcha that FAANG interviews test..
20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.
- Python interviewers test reasoning, not rote syntax: mutability, GIL, decorators, and generators appear in every senior round
- Mutable default args cause shared-state bugs — use None and create fresh defaults inside the function
- The GIL serialises CPU-bound threads — use multiprocessing for parallelism, threading for I/O
- Decorators wrap functions for cross-cutting concerns; @functools.wraps preserves metadata
- Generators yield lazily — use them for streaming data or infinite sequences to save memory
- Context managers (with statement) acquire/release resources cleanly; implement with __enter__/__exit__ or @contextmanager
Think of a Python interview like a driving test. The examiner doesn't just want to see you turn the wheel — they want to know you understand WHY you check mirrors, signal, and brake in that order. These 50 questions are the 'manoeuvres' every Python examiner tests. Know the reasoning behind each one and you'll pass confidently, not by luck.
Python interviews trip up even experienced developers — not because the language is hard, but because interviewers aren't just testing syntax. They're testing whether you understand memory management, mutability traps, the CPython internals that explain quirky behaviour, and whether you can apply the right tool for the right job under pressure. A candidate who can recite list comprehension syntax but can't explain WHY it's faster than a for-loop won't get the role.
This article exists because most interview prep resources give you a list of questions with shallow one-line answers. That's fine for a quiz, but it won't help you when the interviewer follows up with 'why does that happen?' or 'what would you use instead in a production system?' Every answer here explains the mechanism, not just the result — so you can handle follow-ups confidently.
By the end of this article you'll be able to answer all 50 questions with depth, spot the common traps interviewers deliberately set, write clean Python that demonstrates seniority, and walk into any intermediate-to-senior Python interview with genuine confidence rather than crossed fingers.
Why Mutable Default Arguments Are a Data Leak
In Python, default arguments are evaluated once at function definition time, not each time the function is called. This means if you use a mutable object like a list or dict as a default, that single object is shared across all calls. The core mechanic: the default value is bound to the function object itself, stored in __defaults__, and reused every invocation.
When you mutate that default argument inside the function — appending to a list, adding to a dict — the change persists across calls. This is not a bug; it's how Python's object model works. The function signature is compiled once, and the default objects live as long as the function exists. This behavior is deterministic and reproducible, but it violates the assumption most developers make that each call gets a fresh default.
Use this pattern intentionally for caching, memoization, or accumulating state across calls — but never for a parameter that should start empty each time. In real systems, this gotcha silently corrupts data: a list that should be empty for each request accumulates entries from previous requests, leading to hard-to-debug state leaks. The fix is to use None as the default and create a fresh mutable inside the function body.
Memory Management: Mutability and Identity
One of the most frequent 'gotcha' questions in Python interviews concerns the difference between mutable and immutable objects. In Python, everything is an object. Immutable objects (like strings, ints, and tuples) cannot be changed after creation; any 'modification' actually creates a new object in memory. Mutable objects (like lists and dicts) can be changed in place. This distinction is critical when passing arguments to functions, as Python uses 'Pass-by-Object-Reference.'
Concurrency and the GIL (Global Interpreter Lock)
For senior roles, you must explain why Python threads don't speed up CPU-bound tasks. The Global Interpreter Lock (GIL) is a mutex that protects access to Python objects, preventing multiple native threads from executing Python bytecodes at once. While this makes memory management simpler, it means for true parallelism in CPU-intensive work, you must use the multiprocessing module to bypass the GIL by using separate memory spaces.
Generators and Iterators: Lazy Evaluation in Action
Generators are functions that use yield to produce a sequence of values lazily. They pause execution at each yield, preserving state until the next value is requested via . This makes them memory-efficient for large datasets and infinite sequences. Iterators are objects that implement next()_ and _iter__()_. Python's _next__()for loop works by calling on the target and then iter() repeatedly until next()StopIteration is raised. Generators are a special case of iterators generated by functions with yield.
yieldfreezes the function frame and returns a value to the caller.- The generator object stores the frame and local state on the heap.
- Calling
un-freezes and continues until the nextnext()yieldor end. - This is exactly how async/await works in Python — it's syntactic sugar over generators.
Decorators: Wrapping Functions for Cross-Cutting Concerns
A decorator is a function that takes another function as argument, extends its behaviour, and returns a new function. Python's @decorator syntax is syntactic sugar for func = decorator(func). Common use cases: logging, authentication, caching, timing, validation. Critical detail: unless you use @functools.wraps, the decorated function loses its original __name__ and __doc__, which breaks introspection and tooling.
Context Managers and Resource Management
Context managers handle acquiring and releasing resources automatically using the with statement. Built-in examples: file I/O, database connections, locks. You can create custom context managers by defining __enter__ and __exit__ methods on a class, or by decorating a generator function with @contextlib.contextmanager. The __exit__ method receives exception type, value, and traceback — you can choose to suppress exceptions or log them.
close() calls leaks database connections.Pass by Object Reference: Why Your Function Mutates the Caller's Data
Junior devs love to argue pass-by-value vs pass-by-reference. The truth is messier: Python uses pass-by-object-reference. The variable name you pass is a reference to an object. If you assign a new value to that name inside the function, the caller doesn't see it. But if you mutate the object the name points to, the caller sees the change. This is why list.append() modifies the original list, but list = new_list does not. Understanding this prevents production bugs where a function unexpectedly corrupts a shared cache or database session state. Always copy mutable objects before modifying them unless you explicitly want side effects.
def foo(items=[]). Every call without that argument shares the same list object. Use None and create a new list inside the function.The `__slots__` Directive: Slash Memory Usage Without Changing Logic
Every Python object carries a __dict__ that stores its attributes in a hash map. For thousands of small objects, this overhead kills memory and cache locality. __slots__ tells the interpreter to allocate a fixed-size array for attributes instead of a dictionary. Each slot attribute becomes a descriptor, so attribute access is faster. Use it for data classes, ORM models, or any place you create many instances. The downside: you cannot add new attributes dynamically, and you must declare every attribute in the class hierarchy. Measure before optimizing, but when you have a million objects, __slots__ can cut memory from 50 bytes per instance to under 20.
__slots__ when profiling shows memory pressure from many instances. It breaks inheritance if a parent doesn't also define __slots__. Test your hierarchy.__slots__ removes per-instance dictionaries, cutting memory by up to 50% for object-heavy code, at the cost of dynamic attribute creation.The Shared-State Disaster: Mutable Default Arguments
- Never use mutable objects (list, dict, set) as default argument values.
- Use None as the default and instantiate the mutable inside the function body.
- Always check default arguments during code review — this bug is nearly invisible in log files.
tracemalloc.start() and snapshot; diff snapshots to find leak — or inspect with gc.get_objects()id() to confirm shared stateimport sys; sys.setrecursionlimit(5000)sys.getrecursionlimit()Key takeaways
Common mistakes to avoid
5 patternsUsing mutable default arguments in functions
Confusing 'is' vs '==' equality
Failing to use context managers for file I/O or database connections
open() as f:' or 'with db_session() as conn:' to guarantee cleanup.Assuming threading speeds up CPU-bound tasks
Forgetting @functools.wraps on decorators
Interview Questions on This Topic
Explain the difference between `__str__` and `__repr__` in Python.
__repr__ aims to be unambiguous — ideally returning a string that could be used to recreate the object (e.g., Fraction(3, 4)). __str__ aims to be readable for end users. If only __repr__ is defined, __str__ falls back to it. Senior engineers always define __repr__ first, then __str__ when a pretty-print is needed.Frequently Asked Questions
20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.
That's Python Interview. Mark it forged?
4 min read · try the examples if you haven't