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.

Question Bundle
Python
4 questions
py-decorators
py-gil
py-asyncio
interview-prep

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.
from typing import Optional

def append_to_broken(item, target=[]):
    target.append(item)
    return target

def append_to_safe(item, target: Optional[list] = None):
    if target is None:
        target = []
    target.append(item)
    return target

def cache_result_broken(key, cache={}):
    if key not in cache:
        cache[key] = expensive_compute(key)
    return cache[key]

def expensive_compute(key):
    return key * 2