Common Gotchas & Anti-patterns Interview Questions & Answers

7 questions Updated 2026-06-18

Python interview questions on common gotchas: mutable default arguments, late-binding closures, modifying a list while iterating, is vs ==, bare except, mutable class attributes, and shadowing builtins.

A default argument is evaluated once when the function is defined, not on each call. So a mutable default like [] or {} is created a single time and shared across every call that doesn't override it — state leaks between calls.

def append_to(item, target=[]):   # the SAME list every call
    target.append(item)
    return target

append_to(1)   # [1]
append_to(2)   # [1, 2]  <- surprise!

def append_to(item, target=None): # the fix: None sentinel
    if target is None:
        target = []               # fresh list per call
    target.append(item)
    return target

Use None as the default and create the real object inside the body. This is the single most famous Python footgun.

Closures in Python capture variables by reference, not by value. When you create functions in a loop, they all close over the same loop variable, which holds its final value by the time they're called.

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]          # [2, 2, 2]  — not [0, 1, 2]!

# fix: bind the current value via a default argument
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]          # [0, 1, 2]

The default-argument trick captures i's value at definition time. (functools. partial works too.) Remember: closures see the variable's latest value, not a snapshot.

Changing a list's size during iteration shifts the internal index, causing elements to be skipped or repeated. With dicts and sets it's worse — Python raises RuntimeError: dictionary changed size during iteration.

nums = [1, 2, 3, 4]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)        # skips elements — buggy
print(nums)                   # [1, 3] for some inputs, wrong for others

nums = [n for n in nums if n % 2]   # build a new list instead

Fix it by iterating over a copy (for n in nums[:]) or, better, building a new collection with a comprehension or filter. Never mutate a container's size while looping over it.

is tests identity (same object), while == tests value. CPython caches small integers (−5 to 256) and interns some strings, so is coincidentally returns True for those — but fails for values outside the cache, making it look unreliable.

a = 256; b = 256
a is b          # True  — cached

c = 1000; d = 1000
c is d          # often False — not cached
c == d          # True   — always correct

x = "hi"; y = "hi"
x is y          # True (interned) — don't rely on it

Caching is an implementation detail, not a guarantee. Rule: use == for value comparison; reserve is for singletons (None, True, False).

A bare except: (or except Exception: used carelessly) catches everything — including KeyboardInterrupt and SystemExit — and swallows the error silently, hiding bugs and making programs impossible to interrupt or debug.

try:
    risky()
except:                  # catches EVERYTHING, even Ctrl-C
    pass                 # error vanishes — undebuggable

try:
    risky()
except ValueError as e:  # catch only what you expect
    log.error("bad value: %s", e)
    raise                # or handle it deliberately

Catch the specific exceptions you can actually handle, and avoid pass in an except (at minimum log it). If you must catch broadly, use except Exception (not bare) so system-exiting signals still propagate.

A mutable value assigned at class level (outside __init__) is one object shared by every instance. Mutating it through any instance affects all of them — usually not what you intend.

class Cart:
    items = []                  # SHARED across all instances
    def add(self, x):
        self.items.append(x)

a, b = Cart(), Cart()
a.add("apple")
b.items                         # ['apple'] — leaked into b!

class Cart:
    def __init__(self):
        self.items = []         # per-instance — correct

Initialize mutable attributes inside __init__ so each instance gets its own. Class-level attributes are fine for immutable constants/defaults, but never for mutable per-instance state.

Naming a variable after a builtin — list, dict, id, str, type, sumshadows it in that scope, so the original becomes unusable and you get confusing errors later when you try to call it.

list = [1, 2, 3]          # shadows the built-in list type
other = list((4, 5))      # TypeError: 'list' object is not callable

id = 42                   # now id() is gone
id(other)                 # TypeError: 'int' object is not callable

Pick non-conflicting names: items/values instead of list, mapping instead of dict, user_id instead of id. Linters flag builtin shadowing — heed the warning to avoid these baffling bugs.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.