Mutability and Aliasing Bugs
Aliasing bugs are the bug class where two names refer to the same mutable object, and a mutation through one name silently affects the other. The bug is endemic to languages with reference semantics — Python, Java, JavaScript, Go (for slices/maps), Ruby. Some bugs are so common they have nicknames: the default mutable argument trap (
def f(arr=[])), the shared-row trap (grid = [[0]*n]*n), the closure-capture trap (callbacks.append(lambda: i)inside a loop). These are the canonical Python interview gotchas — every senior interviewer has at least one in their pocket. The deep cause is that Python’s=is a name binding, not a copy:b = amakesbandatwo names for the same object, and any mutation through either is visible through both. Immutable types (int,str,tuple,frozenset) sidestep this entirely because they have no in-place mutation; mutable types (list,dict,set, custom classes) require active discipline. This note catalogs the bug patterns, the standard mitigations, and the Pythonic reasoning behind them.
1. Intuition — Names Are Not Boxes
The classic mental model from C is variable = labeled box: int x = 5 allocates a 4-byte slot and writes 5 into it; int y = x allocates a new slot and copies 5 into it; mutating y cannot affect x. This model is wrong for Python.
In Python, variables are names that bind to objects. a = [1, 2, 3] creates a list object on the heap and binds the name a to it. b = a does not copy the list; it creates another name b that binds to the same list object. The list is now aliased through two names.
The visible consequence: b.append(4) mutates the list. Since a is bound to the same list, a also sees the change. Print a: [1, 2, 3, 4]. Most Python beginners are stunned the first time this happens.
The conceptual analogy: in Python, every variable is a sticky note with the object’s address written on it. Saying b = a doesn’t copy what’s in a; it photocopies the address and sticks it to a new piece of paper labeled b. Both notes point to the same warehouse box. Anyone with either note can re-arrange the box’s contents, and both onlookers will see the rearrangement.
This is the foundational fact behind every aliasing bug in Python. (And in Java, JavaScript, Ruby. C-like languages with explicit pointers make this visible; Python hides the pointer but keeps the semantics.)
A simple demonstration:
a = [1, 2, 3]
b = a # b and a alias the same list
b.append(4) # mutates the list
print(a) # [1, 2, 3, 4] ← changed!
print(a is b) # True ← same object identityThe is operator tests object identity (same heap address), distinct from == (value equality). a is b is True; they’re the same object.
2. Bug Pattern 1 — The Default Mutable Argument Trap (THE Canonical Python Interview Gotcha)
This is the single most-asked Python aliasing question in interviews. Senior Python interviewers will hand you this snippet and ask “what’s wrong?”:
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← WAT?
print(append_to(3)) # [1, 2, 3] ← WAT WAT WATThe expected output (for someone unfamiliar with the trap) is [1], [2], [3] — three independent calls with three fresh empty lists. The actual output reveals the bug: every call shares the same list.
2.1 Why This Happens — Default Values Are Evaluated Once
The Python language reference (function definitions) specifies: default parameter values are evaluated at function-definition time, not at call time. When Python parses def append_to(item, target=[]), it evaluates [] exactly once — creating a single empty list — and binds it to the function’s defaults dictionary as the default value of target. Every subsequent call that doesn’t pass target shares that same list.
The first call appends 1 to the shared list, mutating it to [1]. The second call sees target defaulting to [1] (the same list as before), appends 2, mutating it to [1, 2]. And so on.
This is not a bug in Python; it’s the documented semantics. But it’s a near-universal source of bugs because the intuition “default value gives a fresh list each call” is wrong.
2.2 The Mitigation — Use None Sentinel
The Pythonic fix is the None sentinel pattern:
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [2] ← fresh list each call
print(append_to(3)) # [3]Now target defaults to the immutable None. The body checks if target is None (using is for identity, not ==) and creates a fresh [] at call time, not at definition time. Each call gets its own list.
Why is None and not if not target? Because not target is True for [], '', 0, False, and other falsy values — the user might legitimately pass an empty list to mean “start with empty,” and the not target check would erase that distinction. is None only matches the sentinel.
2.3 The 5-Line Counter-Example That Reveals The Trap
def f(x=[]): x.append('a'); return x
print(f()) # ['a']
print(f()) # ['a', 'a']
print(f()) # ['a', 'a', 'a']
print(f.__defaults__) # (['a', 'a', 'a'],) ← the shared default, mutatedThe last line is the smoking gun: the function’s __defaults__ tuple — the actual default values stored on the function object — contains the mutated list. The list is part of the function’s metadata, not regenerated per call. Once you’ve seen this, you can never un-see it.
2.4 The Same Trap For Dict and Set
def add_to(item, target={}): # mutable default dict — same bug
target[item] = True
return target
def add_set(item, target=set()): # mutable default set — same bug
target.add(item)
return targetAny mutable default value triggers the trap. Immutable defaults (None, 0, (), '', frozenset()) are safe because they cannot be mutated.
2.5 The Trap in Class Definitions
class Inventory:
def __init__(self, items=[]): # SAME BUG, in a class
self.items = items
# All Inventory() instances share the same self.items list
a = Inventory()
b = Inventory()
a.items.append('apple')
print(b.items) # ['apple'] ← b's items is a's items, aliasedClasses are functions in disguise (their __init__ is a function); the same trap applies. Always:
class Inventory:
def __init__(self, items=None):
self.items = items if items is not None else []2.6 Why Doesn’t Python Just Re-Evaluate Defaults?
Guido has been asked this many times. The answer: performance and consistency. Re-evaluating defaults per call would slow every function call. And the current behavior is useful in some patterns — memoization caches, counters, registries — that intentionally exploit the per-function shared state. The trap is the price of those features.
# Intentional shared-default pattern
def memoized(x, _cache={}):
if x in _cache:
return _cache[x]
result = expensive_computation(x)
_cache[x] = result
return resultThis is a quick-and-dirty memoization that uses the trap intentionally. Real code should use functools.lru_cache, but the pattern shows the design rationale.
3. Bug Pattern 2 — The Shared-Row Trap ([[0]*n]*n)
The second most-asked aliasing trap:
n = 3
grid = [[0] * n] * n
print(grid) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]] ← looks fine
grid[0][0] = 1
print(grid) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] ← every row changed!3.1 Why This Happens — * on a List Aliases
The expression [[0] * n] * n is evaluated in two steps:
[0] * ncreates one list[0, 0, 0].[...] * ncreates an outer list withnreferences to the same inner list.
So grid[0], grid[1], grid[2] are all aliases for the same [0, 0, 0] list. Mutating any one of them mutates all of them.
The fix is list comprehension, which creates fresh inner lists per iteration:
grid = [[0] * n for _ in range(n)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] ← only first row changedThe list comprehension [expr for ... in ...] re-evaluates expr on each iteration, so each row is a new list. This is the canonical way to construct a 2D grid in Python.
3.2 The Subtle Variant — [0] * n Is Fine
[0] * n aliases 0 itself, but 0 is immutable, so aliasing is harmless — you can never “mutate” 0. The trap only bites when the inner element is mutable. Aliases of integers, strings, tuples are safe; aliases of lists, dicts, sets, custom objects are dangerous.
3.3 DP Tables Are A Common Site
# BUG: every row is the same row
dp = [[0] * n] * m
# FIX: list comprehension
dp = [[0] * n for _ in range(m)]A common interview bug: candidate writes dp = [[0] * n] * m, runs through the algorithm, and gets garbage output. This is a Python-specific gotcha — the equivalent in Java (new int[m][n]) creates fresh sub-arrays correctly.
3.4 With NumPy, Use numpy.zeros
import numpy as np
grid = np.zeros((n, n), dtype=int)
grid[0, 0] = 1 # only this cell changes; numpy uses contiguous memory, not Python listsNumPy avoids the alias trap entirely because its arrays are dense C-style buffers, not lists of references.
4. Bug Pattern 3 — Shallow Copy vs Deep Copy
list.copy(), list[:], dict.copy(), and copy.copy(x) all create shallow copies — a new outer container, but the inner elements are still aliased.
import copy
original = [[1, 2], [3, 4]]
shallow = original.copy() # or original[:] or list(original)
shallow[0][0] = 99
print(original) # [[99, 2], [3, 4]] ← inner list is shared
print(shallow) # [[99, 2], [3, 4]]The shallow copy duplicates the outer list — shallow[0] and original[0] are now different list slots — but each slot still references the same inner list. Mutating through shallow[0] mutates the shared inner list, visible through original.
4.1 The Fix — copy.deepcopy
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original) # [[1, 2], [3, 4]] ← unchanged
print(deep) # [[99, 2], [3, 4]]copy.deepcopy recursively copies every level of the structure. Trade-off: it’s slow for large structures (O(total size) time and memory) and can fail or recurse forever on objects with cyclic references (though Python’s deepcopy handles cycles via a memo dict).
4.2 When Shallow Copy Is Enough
When the inner elements are immutable (numbers, strings, tuples), shallow copy is sufficient:
arr = [1, 2, 3]
arr_copy = arr.copy()
arr_copy.append(4)
print(arr) # [1, 2, 3] ← unchanged
print(arr_copy) # [1, 2, 3, 4]You cannot “mutate” an integer through arr[0] = 99 — that would replace the slot, which only affects arr_copy’s slot, not arr’s.
The general rule: shallow copy is safe iff every element is immutable. Otherwise use deep copy.
4.3 Slicing Is Shallow
arr[:] is a shallow copy:
matrix = [[1, 2], [3, 4]]
sliced = matrix[:]
sliced[0][0] = 99
print(matrix[0][0]) # 99 ← STILL aliasedSame trap. The “slicing creates a copy” intuition is correct for the outer list but doesn’t recurse.
5. Bug Pattern 4 — The Closure Capture Trap
A canonical Python interview question:
callbacks = []
for i in range(5):
callbacks.append(lambda: i)
for cb in callbacks:
print(cb()) # 4, 4, 4, 4, 4 ← all the same! (not 0, 1, 2, 3, 4)5.1 Why This Happens — Closures Capture Variables, Not Values
The lambda lambda: i captures the variable i, not its value at definition time. By the time the lambdas are called (after the loop finishes), i has the value 4 (the last value the loop assigned). All five lambdas see the same final value.
This is consistent with Python’s name-binding semantics: i is a name; the closure references the name; the value bound to that name at call time is 4.
5.2 The Fix — Default Argument Capture
The standard mitigation: make i a default argument of the lambda, which freezes its value at definition time:
callbacks = []
for i in range(5):
callbacks.append(lambda i=i: i) # default arg captures current value
for cb in callbacks:
print(cb()) # 0, 1, 2, 3, 4 ← correctThe i=i parameter says “create a new parameter i with default value equal to the current i (looked up in the enclosing scope right now).” Default values are evaluated at definition time (see §2.1), so each lambda’s default is a snapshot.
This trick exploits the very feature (default-eval-at-definition) that causes bug pattern §2. Two faces of the same Python design choice.
5.3 Alternative Fix — functools.partial
from functools import partial
def f(i): return i
callbacks = [partial(f, i) for i in range(5)]
print([cb() for cb in callbacks]) # [0, 1, 2, 3, 4]partial(f, i) binds i immediately when called.
5.4 Why This Bug Is Endemic to Loop-Lambda Patterns
Every dynamic UI library that builds event handlers in a loop (Tkinter, JavaScript, Vue) hits this. The classic JavaScript bug:
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // prints 5, 5, 5, 5, 5
}The fix in JavaScript is let (block scoping):
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // prints 0, 1, 2, 3, 4
}Python doesn’t have block-scoped let, so the default-argument trick is the standard idiom.
6. Bug Pattern 5 — Mutating a Dict / Set While Iterating
d = {'a': 1, 'b': 2, 'c': 3}
for k in d:
if d[k] == 2:
del d[k] # RuntimeError: dictionary changed size during iterationMutating a dict, set, or list while iterating it is undefined behavior in Python — and CPython explicitly raises an error for dicts/sets. Lists are tolerated but the iteration may skip or repeat elements.
6.1 The Fix — Iterate Over a Snapshot
for k in list(d): # iterate over a copy of the keys
if d[k] == 2:
del d[k]list(d) creates a snapshot of the keys at iteration time. The dict can then be safely mutated.
6.2 List Mutation While Iterating
arr = [1, 2, 3, 4, 5]
for x in arr:
if x % 2 == 0:
arr.remove(x)
print(arr) # [1, 3, 4, 5] ← '4' was skipped due to in-place mutationThe iterator advances by index. Removing an element shifts everything down, so the iterator skips the next element. Idiomatic fix: build a new list with comprehension or filter.
arr = [x for x in arr if x % 2 != 0] # [1, 3, 5]7. Bug Pattern 6 — Generators Are Single-Use
gen = (x * 2 for x in range(5))
print(list(gen)) # [0, 2, 4, 6, 8]
print(list(gen)) # [] ← generator exhausted!Generators are stateful and one-shot. Iterating “consumes” them. A function that takes a generator can iterate it once; subsequent code passing the same generator gets no elements.
7.1 Aliasing-Like Effect with Generators
def first_two(gen):
return [next(gen), next(gen)]
g = (x for x in range(10))
print(first_two(g)) # [0, 1]
print(list(g)) # [2, 3, 4, 5, 6, 7, 8, 9] ← state was mutated by first_twoThis isn’t aliasing in the same sense, but it has the same flavor: passing a generator to a function and assuming it’s “still fresh” afterward is a bug. Use list(gen) to snapshot the contents if you need to iterate twice.
8. Bug Pattern 7 — Class Attributes Shared Across Instances
class Counter:
instances_seen = [] # class attribute
def __init__(self, name):
self.name = name
Counter.instances_seen.append(name)
c1 = Counter('a')
c2 = Counter('b')
print(Counter.instances_seen) # ['a', 'b'] ← sharedClass attributes (defined at class body level, not in __init__) are one attribute shared across all instances. Often this is intentional (registry pattern, instance counters); when it’s accidental, you’ve introduced a hidden alias.
The trap deepens with mutable class attributes:
class Player:
inventory = [] # BUG: shared across all Player instances
p1 = Player()
p2 = Player()
p1.inventory.append('sword')
print(p2.inventory) # ['sword'] ← shared!The fix: initialize mutable state in __init__:
class Player:
def __init__(self):
self.inventory = [] # per-instance9. Pseudocode — The Aliasing Mitigation Recipe
when binding two names to a value:
if value is immutable (int, str, tuple, frozenset):
no precaution needed; aliasing is invisible
if value is mutable (list, dict, set, custom):
decide:
do you want shared state? — alias is fine
do you want independent copies? — copy.copy (shallow) or copy.deepcopy
when accepting a default argument:
if default value is mutable:
replace with None, set inside the function body
if default value is immutable:
safe to use directly
when capturing a loop variable in a closure:
if you need the value at definition time:
use default-argument trick: lambda x=loop_var: ...
or functools.partial
if you need late-binding (rare):
leave as-is
when iterating a mutable container:
if you need to mutate it:
iterate over list(container) snapshot
or build a new container via comprehension
10. Python Implementation — Demonstrations and Mitigations
10.1 Inspecting Object Identity
a = [1, 2, 3]
b = a
c = a.copy()
print(a is b) # True (same object)
print(a is c) # False (different object, even if equal)
print(a == c) # True (equal value)
print(id(a)) # e.g. 140234... (memory address)
print(id(b)) # same as id(a)
print(id(c)) # different from id(a)id(x) returns CPython’s identity (memory address) of the object. is is id-equal; == is value-equal.
10.2 Building DP Tables Correctly
# 1D table
dp = [0] * n # safe: ints are immutable
# 2D table — list comprehension
dp = [[0] * cols for _ in range(rows)]
# 3D table
dp = [[[0] * d for _ in range(c)] for _ in range(r)]10.3 Defensive Copying
import copy
def safe_concat(a, b):
"""Return a new list that's the concat of a and b, without mutating either."""
result = a.copy() # shallow; safe if a's elements are immutable
result.extend(b) # extend appends b's elements to result, not to a
return result
def safe_deep_concat(a, b):
"""Concat with full independence — even nested mutables are independent."""
result = copy.deepcopy(a)
result.extend(copy.deepcopy(b))
return result10.4 Frozen Dataclasses for Immutability
For complex objects where you want immutability:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
# p.x = 5 # FrozenInstanceError: cannot assign to field 'x'frozen=True makes the dataclass immutable. Aliasing Point instances is now safe by construction.
11. Diagram — Names, Bindings, and Aliases
flowchart LR subgraph "Aliasing: a = [1,2,3]; b = a" direction LR A["a (name)"] B["b (name)"] L1["heap: list [1,2,3]"] A -->|"binds to"| L1 B -->|"binds to"| L1 Mut["b.append(4)"] -.->|"mutates"| L1 L1 -.->|"now [1,2,3,4]"| ObsA["a sees [1,2,3,4]"] L1 -.->|"now [1,2,3,4]"| ObsB["b sees [1,2,3,4]"] end subgraph "Independent copy: a = [1,2,3]; b = a.copy()" direction LR A2["a (name)"] B2["b (name)"] L2a["heap: list [1,2,3]"] L2b["heap: list [1,2,3]"] A2 -->|"binds to"| L2a B2 -->|"binds to"| L2b Mut2["b.append(4)"] -.->|"mutates"| L2b L2a -.->|"still [1,2,3]"| ObsA2["a sees [1,2,3]"] L2b -.->|"now [1,2,3,4]"| ObsB2["b sees [1,2,3,4]"] end
What this diagram shows. On the left, aliasing: two names a and b both bind to the same heap object. Any mutation through either name is visible through both — the underlying list is shared. On the right, an independent copy: b = a.copy() creates a new heap object containing the same elements (shallow), and binds b to it. Now mutations through b no longer affect a. The visual takeaway: = in Python binds a name; it does not copy. To get independent copies, you must explicitly use copy(), [:], dict(), copy.deepcopy(), or rebuild from scratch. The same diagram applies to dicts, sets, and custom objects. Aliasing is invisible at the source-code level — b = a and b = a.copy() look almost identical — but the runtime semantics are radically different. Bugs from this distinction are a leading cause of “the test passed in isolation but failed when integrated.”
12. Common Interview Problems Where Aliasing Bites
| Problem | The aliasing trap |
|---|---|
| Backtracking — collect results | result.append(path) adds an alias; mutating path later corrupts every result. Fix: result.append(path[:]) or result.append(list(path)) |
| Permutations / Subsets / Combinations | Same trap as above — see Permutations, Subsets, Combinations |
| DP table initialization | [[0] * n] * n shares rows. Fix: list comprehension |
| Dict / set mutation in iteration | RuntimeError: dictionary changed size during iteration. Fix: iterate list(d) |
| Default mutable argument | def f(arr=[]) shares list across calls. Fix: arr=None |
| Class instance attributes | class P: inv = [] shares across instances. Fix: init in __init__ |
| Closure in loop | lambda: i captures i by reference. Fix: lambda i=i: i |
| Returning a class’s internal list | Caller can mutate it. Fix: return self.list[:] (defensive copy) |
| Memo dict shared between calls | Sometimes intentional (caching), sometimes a bug |
| Building graph adjacency list | adj = [[]] * n aliases. Fix: adj = [[] for _ in range(n)] |
| Mutating arguments | Functions that mutate their arguments (list.sort(), arr.reverse()) surprise callers; document or return a new list |
| Slicing assignment | arr[i:j] = new mutates arr in place vs arr = arr[:i] + new + arr[j:] returns new |
dict.copy() of nested dict | Shallow; nested dicts shared. Fix: copy.deepcopy |
| Sorting tuples vs lists | Tuples can be dict keys; lists cannot (mutable, unhashable). Hashing depends on immutability |
| Caching with mutable keys | lru_cache requires hashable args; lists/dicts will raise TypeError: unhashable type |
13. Pitfalls — Aliasing in Aliasing Bugs
13.1 Confusing Identity (is) and Equality (==)
a = 1000
b = 1000
print(a is b) # False (or True for small ints due to caching — implementation-dependent)
print(a == b) # Trueis is for identity (same object); == is for value. Use is only for None, True, False, and sentinel values. Use == for everything else. CPython caches small integers (-5 to 256) so a is b for a = b = 100 happens to be True, but this is implementation-defined and not reliable.
13.2 not target vs target is None
def f(target=None):
if not target: # WRONG: also matches [] and 0 and ''
target = []
def f(target=None):
if target is None: # RIGHT: matches only the sentinel
target = []Using not target swallows legitimate falsy inputs. Always use is None.
13.3 Forgetting Strings Are Immutable
s = "hello"
t = s
t = t + " world"
print(s) # "hello" ← unchanged
print(t) # "hello world"String concatenation creates a new string. The variable t is rebound to the new string; s is unaffected. This is not aliasing — strings are immutable. The trap is the opposite direction: novices sometimes assume strings are mutable like lists and write code that doesn’t work.
13.4 Forgetting Tuples Containing Mutables
t = ([1, 2], [3, 4]) # tuple of lists
# t[0] = [9, 9] # TypeError: tuple is immutable
t[0].append(99) # OK — mutates the inner list
print(t) # ([1, 2, 99], [3, 4])Tuples are immutable containers, but their elements may be mutable and aliasable. t[0].append(99) mutates the inner list (which the tuple still references — only the binding in the tuple is fixed).
13.5 += on Lists vs Strings
a = [1, 2]
b = a
b += [3] # equivalent to b.extend([3]); mutates a
print(a) # [1, 2, 3] ← surprise
a = "hello"
b = a
b += " world" # creates new string; rebinds b
print(a) # "hello" ← unchanged+= on a mutable type is in-place; on an immutable type it’s rebind. This asymmetry surprises people. The reason: Python’s __iadd__ falls back to __add__ for immutable types.
13.6 Setting a Default Inside __init__ to a Class-Level Default
class Buggy:
DEFAULT = []
def __init__(self, items=DEFAULT): # SAME default-mutable trap, with a class const
self.items = itemsLifting the [] to a class attribute named DEFAULT doesn’t fix the trap; it just gives the shared list a name. Every Buggy() instance shares Buggy.DEFAULT.
13.7 Serialization Round-Trips as a Deep-Copy Substitute
copy.deepcopy(obj) is the standard way to produce a fully independent copy. Some scientific code uses serialization round-trips (encoding to a binary format and decoding back) as a deep-copy substitute. Caveats: not all objects serialize cleanly (file handles, lambdas, runtime-only constructs); the round-trip is slower than deepcopy; and some serialization formats have security implications when applied to untrusted data. Prefer copy.deepcopy for an in-process deep copy unless there’s a specific reason to use serialization.
13.8 NumPy Views vs Copies
import numpy as np
a = np.array([1, 2, 3, 4])
b = a[1:3] # b is a VIEW into a — aliased!
b[0] = 99
print(a) # [1, 99, 3, 4] ← mutated through viewNumPy slicing returns views that share memory with the original — efficient but trap-ful. Use a[1:3].copy() for an independent buffer.
13.9 Mutable Default in Recursion / Backtracking
def perms(arr, current=[], result=[]): # double trap
if len(current) == len(arr):
result.append(current[:])
return result
for x in arr:
if x not in current:
current.append(x)
perms(arr, current, result)
current.pop()
return result
print(perms([1, 2, 3]))
print(perms([4, 5, 6])) # results bleed across calls!The result of perms([4, 5, 6]) includes the leftover from perms([1, 2, 3]). Fix both defaults with None.
13.10 Shared Mutable Returned from a Cached Property
from functools import cached_property
class Container:
@cached_property
def items(self):
return [1, 2, 3]
c = Container()
c.items.append(99)
print(c.items) # [1, 2, 3, 99] ← caller mutated the cached valuecached_property caches the first return value. If callers mutate it, the cache holds the mutated state. Fix: return a copy from the property, or freeze the value (tuple, frozenset).
14. Open Questions
- Why didn’t Python adopt explicit
mutable/immutabletype annotations for arguments? Some PEPs have proposed; none have landed. The Python community generally prefers convention (sentinels, defensive copies) over types for this. Verify against current PEP status. - Does Python 3.13’s experimental no-GIL mode change aliasing-bug semantics? Aliasing is about object identity; the GIL is about thread-safety. Removing the GIL exposes new race conditions on aliased mutations but doesn’t change which mutations are aliased.
- Do other dynamic languages (Ruby, JavaScript) have the default-mutable-argument trap? Ruby evaluates default values per call (no trap). JavaScript’s
function f(x=[])evaluates per call (no trap). Python is the outlier. - Why does
a is bsometimes returnTruefora, bindependently constructed? CPython’s implementation caches small integers, interns short strings, and so on. The behavior is implementation-defined. Never rely onisfor value comparison.
15. See Also
- Off-By-One Errors — Survival Guide — sibling pitfall note
- Integer Overflow in Interviews — sibling pitfall note
- Recursion Depth Limits — sibling pitfall note
- Backtracking Framework — appending
pathwithout copying is the most common backtracking bug - Permutations, Subsets, Combinations — concrete sites of the path-aliasing bug
- Memoization vs Tabulation —
lru_cacherequires hashable (immutable) arguments - Hash Function Design — why mutable objects can’t be hashed
- Singly Linked List — node-based aliasing in linked structures
- DP State Identification — defensive coding around DP-table aliasing
- Big-O Notation —
deepcopyisO(total size) - Space Complexity — defensive copies cost memory
- SWE Interview Preparation MOC