Mid-level 3 min · March 06, 2026

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.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

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.'

io/thecodeforge/core/mutability_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# io.thecodeforge: Demonstrating the Mutability Trap
def append_to_list(val, my_list=[]):  # DANGEROUS: Default args are evaluated once
    my_list.append(val)
    return my_list

# Production-grade approach
def safe_append(val, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(val)
    return my_list

print(f"Unsafe 1: {append_to_list(1)}")
print(f"Unsafe 2: {append_to_list(2)}") # Unexpectedly contains [1, 2]
Output
Unsafe 1: [1]
Unsafe 2: [1, 2]
Forge Tip:
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick. In interviews, always mention that default arguments in Python are evaluated at definition time, not execution time.
Production Insight
Mutability defaults cause silent data corruption that surfaces weeks later.
Only way to debug is to print(id(mylist)) across calls — painful.
Rule: never use mutable literals as defaults; use None and instantiate inside.
Key Takeaway
Default args are evaluated once at function definition.
Mutable defaults are shared across all calls.
Always use None as sentinel and create fresh mutable inside.

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.

io/thecodeforge/concurrency/gil_workaround.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
# io.thecodeforge: Parallelism via Multiprocessing
from multiprocessing import Pool
import os

def cpu_intensive_task(n):
    # Simulate heavy computation
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    numbers = [10**6, 10**6, 10**6, 10**6]
    with Pool(processes=os.cpu_count()) as pool:
        results = pool.map(cpu_intensive_task, numbers)
    print(f"Task completed across {os.cpu_count()} cores.")
Output
Task completed across 8 cores.
Strategy Note:
If the interviewer asks about I/O-bound tasks (like web scraping), emphasize that threading is still useful because the GIL is released during I/O operations.
Production Insight
Using threading for CPU work causes performance worse than single-threaded.
The GIL context-switching overhead kills throughput.
Rule: threading for I/O, multiprocessing for CPU, asyncio for both when I/O dominates.
Key Takeaway
GIL lets one thread run Python bytecode at a time.
Threading adequate for I/O — GIL released during blocking calls.
Multiprocessing gives true parallelism via separate processes.

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 next(). This makes them memory-efficient for large datasets and infinite sequences. Iterators are objects that implement __iter__() and __next__(). Python's for loop works by calling iter() on the target and then next() repeatedly until StopIteration is raised. Generators are a special case of iterators generated by functions with yield.

io/thecodeforge/iterators/range_simulator.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# io.thecodeforge: Custom iterator vs generator for debouncing events
class EventTimestampIterator:
    def __init__(self, timestamps):
        self._timestamps = timestamps
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index >= len(self._timestamps):
            raise StopIteration
        ts = self._timestamps[self._index]
        self._index += 1
        return ts

# Generator equivalent – more concise
def debounced_events(timestamps, interval_ms=500):
    previous = None
    for ts in timestamps:
        if previous is None or (ts - previous) >= interval_ms:
            previous = ts
            yield ts

# Both produce the same output but generator uses less boilerplate
Mental Model: Generators as Paused Functions
  • yield freezes the function frame and returns a value to the caller.
  • The generator object stores the frame and local state on the heap.
  • Calling next() un-freezes and continues until the next yield or end.
  • This is exactly how async/await works in Python — it's syntactic sugar over generators.
Production Insight
Reading a 10GB file line by line with a generator works; loading it all into a list crashes.
Always prefer generators for streams, logs, and database cursors.
Rule: if you iterate once, use a generator. If you need random access, use a list.
Key Takeaway
Generators produce items on demand — memory footprint is O(1).
They maintain state across yield points without extra class code.
Use them for any sequence that could grow unbounded.

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.

io/thecodeforge/decorators/timing_decorator.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# io.thecodeforge: Production-grade timing decorator with @wraps
import functools
import time

def measure_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@measure_time
def fetch_user_data(user_id):
    time.sleep(0.2)  # simulate DB call
    return {"id": user_id, "name": "Alice"}

print(fetch_user_data(1))
print(f"Function name preserved: {fetch_user_data.__name__}")
Output
fetch_user_data took 0.2001s
{'id': 1, 'name': 'Alice'}
Function name preserved: fetch_user_data
Common Mistake: Missing @wraps
Without @functools.wraps, fetch_user_data.__name__ becomes 'wrapper'. This breaks tools like pytest, logging, and debuggers that rely on function identities.
Production Insight
Decorators without @wraps cause silent failures in middleware routing and test fixtures.
Always apply @wraps to the inner function — it copies __name__, __doc__, __module__.
Rule: any decorator you write for production must include @functools.wraps.
Key Takeaway
Decorators add pre/post processing without modifying the original function.
@functools.wraps preserves function identity — never skip it.
Use decorators for cross-cutting concerns only; don't overuse them for business logic.

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.

io/thecodeforge/context_managers/db_session.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# io.thecodeforge: Context manager for database sessions
from contextlib import contextmanager

@contextmanager
def db_session(connection_string):
    conn = create_connection(connection_string)  # not shown
    try:
        yield conn
        conn.commit()  # commit if no exception
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

# Usage — resource is automatically closed
with db_session("postgresql://localhost/mydb") as conn:
    conn.execute("INSERT INTO users ...")
Interview Hook:
When the interviewer asks 'How would you implement a context manager?', start with the class-based approach (__enter__/__exit__), then mention the @contextmanager shortcut for simple cases. This shows depth.
Production Insight
A missing try/finally around explicit close() calls leaks database connections.
Context managers eliminate that class of bug entirely.
Rule: any function that acquires a resource must use with statement or be wrapped in one.
Key Takeaway
Context managers guarantee cleanup even when exceptions occur.
__exit__ can suppress exceptions by returning True — only do that intentionally.
Use @contextmanager for simple cases; class-based for complex state.
● Production incidentPOST-MORTEMseverity: high

The Shared-State Disaster: Mutable Default Arguments

Symptom
Users get data from previous requests appended to their payload — personal data leaks occur intermittently.
Assumption
The developer assumed that the default argument my_list=[] creates a fresh list on every call, as it would in most languages.
Root cause
Python evaluates default arguments once at function definition time, not at call time. All calls share the same list object.
Fix
Replace mutable defaults with None and create a new list inside the function if None is passed.
Key lesson
  • 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.
Production debug guideFour common Python production failures and the exact commands to diagnose them4 entries
Symptom · 01
Memory usage grows without bound; process OOM-killed
Fix
Run tracemalloc.start() and snapshot; diff snapshots to find leak — or inspect with gc.get_objects()
Symptom · 02
Application hangs on a CPU-bound task
Fix
Attach with py-spy or strace to see which thread is spinning; check if you accidentally used threading instead of multiprocessing
Symptom · 03
Function returns stale data (mutable default bug)
Fix
Inspect the function's __defaults__ attribute; compare object IDs with id() to confirm shared state
Symptom · 04
ImportError after deployment: module not found
Fix
Run python -c 'import sys; print(sys.path)' and verify the module is in one of those paths; check for missing __init__.py
★ Quick Debug Cheat Sheet: Python Production IssuesFive high-signal commands for the most common Python interview topics that break in production
Recursion depth exceeded in production
Immediate action
Increase recursion limit temporarily for diagnosis
Commands
import sys; sys.setrecursionlimit(5000)
sys.getrecursionlimit()
Fix now
Rewrite the algorithm iteratively or adjust the recursion limit carefully — but never rely on high limits in production.
GIL causing latency spikes in I/O-heavy service+
Immediate action
Swap threads for asyncio or multiprocessing
Commands
import asyncio; asyncio.run(async_main())
from multiprocessing import Pool; with Pool() as p: p.map(func, data)
Fix now
Use asyncio for high-concurrency I/O; use multiprocessing for CPU-bound parallel work.
Decorated function loses its name and docstring+
Immediate action
Check if @functools.wraps is applied
Commands
print(my_func.__name__) # returns 'wrapper' without wraps
import functools; @functools.wraps(func) def wrapper(...)
Fix now
Always apply @functools.wraps to the inner function of any decorator you write.
Generator ran out of items (StopIteration)+
Immediate action
Wrap usage in a for loop or next(gen, default)
Commands
next(gen, 'fallback')
list(gen) # careful: exhausts the generator
Fix now
Use for item in gen: loop which handles StopIteration automatically; never manually call next() without a default.
Context manager resource not released (file handle leak)+
Immediate action
Check if object implements __enter__/__exit__ or use contextlib.closing
Commands
type(obj).__exit__ # verify method exists
from contextlib import closing; with closing(thing):
Fix now
Always use the with statement for resources; if the object doesn't support it, wrap with contextlib.closing().
FeatureListTuple
MutabilityMutable (Can change)Immutable (Cannot change)
Memory UsageHigher (Over-allocation for growth)Lower (Fixed size)
PerformanceSlower iterationFaster iteration
Use CaseHomogeneous data, dynamic sizingHeterogeneous data, fixed records/keys

Key takeaways

1
You now understand that Python's 'Pass-by-Object-Reference' behaves differently for mutable vs. immutable types.
2
You've seen how to bypass the GIL using the multiprocessing module for CPU-bound performance.
3
Generators and context managers reduce memory footprint and ensure resource cleanup.
4
Decorators with @functools.wraps preserve function identity for production tooling.
5
The 'Forge' standard requires safe handling of default arguments to ensure production stability.
6
Practice daily
the forge only works when it's hot 🔥

Common mistakes to avoid

5 patterns
×

Using mutable default arguments in functions

Symptom
Function results unexpectedly accumulate data across calls; test suite fails when run in import order.
Fix
Replace default value [] or {} with None, then create the mutable inside the function body if None.
×

Confusing 'is' vs '==' equality

Symptom
Comparing two integer variables with 'is' works for small ints (caching) but fails unexpectedly for large ints or strings.
Fix
Use '==' for value equality, 'is' only for testing identity (e.g., x is None).
×

Failing to use context managers for file I/O or database connections

Symptom
ResourceWarning: unclosed file or database connection pool exhaustion in production.
Fix
Always use 'with open() as f:' or 'with db_session() as conn:' to guarantee cleanup.
×

Assuming threading speeds up CPU-bound tasks

Symptom
CPU utilisation remains low while threads contend for the GIL; throughput worse than single-threaded.
Fix
Use multiprocessing.Pool for CPU-bound work; reserve threading for I/O-bound tasks.
×

Forgetting @functools.wraps on decorators

Symptom
Decorated function appears with name 'wrapper' in logs and debugger; __doc__ lost.
Fix
Import functools, apply @functools.wraps to the inner function before returning it.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between `__str__` and `__repr__` in Python.
Q02SENIOR
How does Python's garbage collection handle reference cycles?
Q03JUNIOR
What is the difference between `@staticmethod` and `@classmethod`?
Q04SENIOR
Explain how Python resolves attribute lookups in classes (MRO).
Q05SENIOR
What is the purpose of `__slots__` and when should you use it?
Q01 of 05SENIOR

Explain the difference between `__str__` and `__repr__` in Python.

ANSWER
__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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between a Deep Copy and a Shallow Copy in Python?
02
How does Python's Garbage Collection work?
03
What are Python Decorators and when should you use them?
04
Explain the difference between `yield` and `return` in a generator.
05
How do you handle large files that don't fit into memory in Python?
🔥

That's Python Interview. Mark it forged?

3 min read · try the examples if you haven't

Previous
Spring Boot Interview Questions
1 / 4 · Python Interview
Next
Python OOP Interview Questions