On day 78 of our Python Backend Development series, we're revisiting a set of Python interview questions: fundamentals for production that separate developers who've shipped code under pressure from those still learning from tutorials. These aren't trick questions—they're about the gotchas and design decisions baked into Python that come back to haunt production systems.
Mutable default arguments and why they bite
This is the single most common Python footgun we've seen cause bugs in production. When you define a function with a mutable default argument, that object is created once at function definition time, not each time the function is called. Every invocation shares the same object.
Here's what goes wrong:
def append_item(item, container=[]):
container.append(item)
return container
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] — unexpected!
print(append_item(3)) # [1, 2, 3] — definitely wrongThe list object is created once when the function is defined. Every call mutates that same list. In a real backend, this might mean user data from one request leaks into another, or cache state persists across what should be isolated operations.
The fix is to use None as the sentinel:
def append_item(item, container=None):
if container is None:
container = []
container.append(item)
return container
print(append_item(1)) # [1]
print(append_item(2)) # [2]
print(append_item(3)) # [3]Or use a factory pattern if you're building ORM models or other framework objects. This pattern shows up in function signatures across Django, SQLAlchemy, and most serious production frameworks because teams have learned the lesson the hard way.
is vs ==
== checks value equality. is checks object identity—whether two variables point to the same object in memory. In production code, confusing these leads to subtle concurrency bugs and cache misses.
Consider a service that checks configuration:
config_a = {"env": "production"}
config_b = {"env": "production"}
print(config_a == config_b) # True — same values
print(config_a is config_b) # False — different objectsFor small integers and strings, Python caches them for performance, so identity appears to work:
a = 5
b = 5
print(a is b) # True — cached
c = 256
d = 256
print(c is d) # True — integers -5 to 256 are cached
e = 257
f = 257
print(e is f) # False — outside cache rangeNever rely on this. The caching is an implementation detail of CPython. In production, we saw a payment service fail because it was checking transaction_type is PENDING instead of transaction_type == PENDING. When the enum was deserialized from a database or API, it was a new object, and the identity check failed.
The rule: use == for values, is only for singletons like None, True, and False:
if config is None:
raise ValueError("config required")
if status == "active":
process_request()How dicts preserve insertion order
As of Python 3.7, dict insertion order is guaranteed. This is now part of the language spec, not just an implementation detail. Understanding why matters for production systems that serialize and deserialize data.
Before 3.7, if you needed ordered data structures, you'd reach for collections.OrderedDict. Now, a plain dict maintains insertion order:
user_data = {}
user_data["id"] = 42
user_data["name"] = "Alice"
user_data["email"] = "alice@example.com"
for key in user_data:
print(key)
# Output:
# id
# name
# emailThis matters because APIs often expect fields in a specific order, and logging systems benefit from consistent ordering for debugging. In a microservice that logs JSON payloads, insertion order can make diffs and alerts more readable.
However, dict ordering is insertion order, not sort order. If you need sorted output, you still call sorted() or use json.dumps(sort_keys=True):
import json
data = {"zebra": 1, "apple": 2, "banana": 3}
print(json.dumps(data))
# {"zebra": 1, "apple": 2, "banana": 3}
print(json.dumps(data, sort_keys=True))
# {"apple": 2, "banana": 3, "zebra": 1}In production, this guarantees that when you iterate over a dict in a loop or serialize it to JSON for an API response, the order is stable and predictable across Python versions 3.7+.
Shallow vs deep copy
The copy module gives us two operations, and they have very different semantics. Shallow copy creates a new object, but references to nested objects are still shared. Deep copy recursively copies everything.
Shallow copy scenario—a cache layer that duplicates config objects:
import copy
original = {
"db": {"host": "localhost", "port": 5432},
"cache": ["redis1", "redis2"]
}
shallow = copy.copy(original)
# Modify nested data
shallow["db"]["host"] = "production-db"
shallow["cache"].append("redis3")
print(original)
# {'db': {'host': 'production-db', 'port': 5432}, 'cache': ['redis1', 'redis2', 'redis3']}
# Original was mutated!The shallow copy duplicated the outer dict, but the nested dict and list are still references to the same objects. This is a common source of bugs when passing config around in distributed systems.
Deep copy solves this:
import copy
original = {
"db": {"host": "localhost", "port": 5432},
"cache": ["redis1", "redis2"]
}
deep = copy.deepcopy(original)
# Modify nested data
deep["db"]["host"] = "production-db"
deep["cache"].append("redis3")
print(original)
# {'db': {'host': 'localhost', 'port': 5432}, 'cache': ['redis1', 'redis2']}
# Original is untouchedIn production, we've seen microservices pass config objects between functions and accidentally mutate shared state because someone used shallow copy without realizing nested lists were being shared. The fix is to reach for deepcopy when you know nested structures are involved, or better yet, use immutable data structures where possible.
One caveat: deepcopy is slower and can fail on objects with circular references or non-copyable resources like file handles or database connections. If you're copying thousands of records, profile it. Often a factory function or a copy-on-write pattern is faster and clearer.
Common mistakes
- Mutable defaults in class methods: Team members define
def __init__(self, items=[])and wonder why all instances share the same list. Use the sentinel pattern everywhere. - Checking singletons with ==: Code that does
if response is Trueinstead ofif response == Truewill fail when response comes from an API or database deserialization. - Assuming dict order before 3.7: While we're well past Python 3.6, some legacy systems still exist. If your backend targets older versions, document the expectation explicitly.
- Using shallow copy in cache layers: Caching a config object and then modifying it in one code path leaks mutations to all consumers. Defensive deep copy, or freeze the data structure.
Key takeaways
- Never use mutable default arguments. Use
Noneas a sentinel and initialize inside the function. - Use
isonly for identity checks against singletons likeNone. Use==for value comparison. - Dict insertion order is guaranteed in Python 3.7+, but rely on it only when you control the Python version across your stack.
- Shallow copy creates a new container but shares nested references. Use
deepcopyfor nested structures, but be mindful of performance. - These aren't edge cases—they're production failure modes. Test them explicitly in code review and integration tests.






Comments
Loading comments…