Community Question Bundle
Python Gotchas That Trip Up Experienced Devs
A 4-question reference set on Python pitfalls that look obvious in retrospect: mutable default arguments, is-vs-equals identity, the GIL's actual behavior, and closure late-binding in comprehensions.
Python Gotchas That Trip Up Experienced Devs
A 4-question reference set on Python pitfalls that look obvious in retrospect: mutable default arguments, is-vs-equals identity, the GIL's actual behavior, and closure late-binding in comprehensions.
By CodeSnatch
January 20, 2026
·
Updated May 20, 2026
378 views
3
4.4 (13)
Default argument values in Python are evaluated once at function definition time, not at each call. What does this break, and what is the idiomatic fix?
Examples
Example 1:
Input: def append_to(item, target=[]): target.append(item); return target
print(append_to(1)) # called first time
print(append_to(2)) # called second time
Output: [1] then [1, 2] -- the default list is shared across calls
Explanation: target=[] creates ONE list at def-time; every call without an explicit target mutates it.Example 2:
Input: def safe_append(item, target=None):
if target is None: target = []
target.append(item); return target
Output: [1] then [2] -- each call gets a fresh list
Explanation: Using None as the sentinel and constructing the default inside the body sidesteps the trap.Python's is checks identity, == checks equality. CPython caches small integers and short strings, which makes is deceptively work for primitives. When does this break?
Examples
Example 1:
Input: a = 256; b = 256; print(a is b)
c = 257; d = 257; print(c is d)
Output: True, False
Explanation: CPython pre-interns small ints from -5 to 256; outside that range identity is not guaranteed.Example 2:
Input: s1 = "hello"; s2 = "hello"; print(s1 is s2)
s3 = "hello world"; s4 = "hello world"; print(s3 is s4)
Output: True, then implementation-dependent (typically False in REPL, True in module-load when interning kicks in)
Explanation: String interning is opportunistic; never rely on 'is' for value comparison.The Python GIL serializes Python bytecode execution across threads. What workload does threading still help with, and what is the correct alternative for CPU-bound work?
Examples
Example 1:
Input: 100 HTTP requests to different URLs, each blocking on network for ~200ms
Output: ThreadPoolExecutor(max_workers=20) finishes in ~1s; sequential takes ~20s
Explanation: GIL is released during I/O syscalls; threads make sense for I/O-bound work.Example 2:
Input: 4 CPU-bound numpy-light Python computations on a 4-core machine
Output: ThreadPoolExecutor gives ~1x speedup; ProcessPoolExecutor gives ~4x
Explanation: CPU-bound Python code needs separate processes (no GIL contention) to actually parallelize.Late binding in Python closures: what does this loop print, and how do you fix it without changing the loop structure?
Examples
Example 1:
Input: funcs = [lambda: i for i in range(3)]; [f() for f in funcs]
Output: [2, 2, 2] -- all closures share the same i, which is 2 when the comprehension finishes
Explanation: Python closures capture by reference; the lambda body looks up i at call time.Example 2:
Input: funcs = [lambda i=i: i for i in range(3)]; [f() for f in funcs]
Output: [0, 1, 2] -- default argument trick binds i at function-definition time
Explanation: Default args evaluate eagerly at def-time, so each lambda captures its own i.