Steven's Knowledge

Pitfalls & Correctness

The bugs that survive code review -- mutable defaults, late-binding closures, is vs ==, plus datetime, Decimal money, and copy semantics

Pitfalls & Correctness

These are the bugs that look correct on the page and fail in production weeks later. Mutable default arguments, late-binding closures, and is vs == are the classic traps; datetime/timezone handling, Decimal for money, and shallow-vs-deep copies are the correctness issues that quietly corrupt data.

Common Pitfalls That Bite Experts

These are the bugs that survive code review because the code looks correct.

Mutable default arguments are evaluated once, at function definition:

def add_item(item, basket=[]):   # BUG: one shared list across all calls
    basket.append(item)
    return basket

add_item("a")            # ["a"]
add_item("b")            # ["a", "b"]  -- surprise!

def add_item(item, basket=None):  # the fix
    basket = [] if basket is None else basket
    basket.append(item)
    return basket

Late-binding closures capture the variable, not its value -- classic in loops:

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

funcs = [lambda i=i: i for i in range(3)]  # bind now via default arg
[f() for f in funcs]              # [0, 1, 2]

is vs ==: is tests identity, == tests equality. It only "works" for small ints and short strings because of interning -- an implementation detail you must never depend on:

a = 1000; b = 1000
a == b      # True
a is b      # False (CPython only interns -5..256)
# Use 'is' only for singletons: None, True, False.

Mutating a collection while iterating it corrupts iteration:

for x in items:
    if pred(x):
        items.remove(x)          # skips elements / RuntimeError on dicts

items = [x for x in items if not pred(x)]   # build a new one instead

Catching too broadly hides real failures, including the ones you must never swallow:

try:
    risky()
except Exception:    # silently eats bugs; never use bare 'except:'
    pass
# bare 'except:' even catches KeyboardInterrupt and SystemExit.
# Catch the narrowest exception you can actually handle, and log the rest.

Async traps: a blocking call inside a coroutine freezes the entire event loop, and fire-and-forget tasks can be garbage-collected mid-flight:

async def handler():
    time.sleep(5)                # BUG: blocks the whole loop, not just this task
    await asyncio.sleep(5)       # correct -- yields control

# A task with no surviving reference may be collected before it finishes:
asyncio.create_task(background_job())        # risky -- may vanish
task = asyncio.create_task(background_job())  # keep a reference, or use a TaskGroup

Correctness: Time, Money, and Copies

A separate class of bugs is not about syntax but about semantics -- the code runs, ships, and is wrong in production weeks later.

Datetimes: a naive datetime (no timezone) is a footgun. Use timezone-aware UTC everywhere internally; convert to local only at display:

from datetime import datetime, timezone, timedelta

datetime.utcnow()                    # BUG: naive -- no tzinfo, silently wrong in math
datetime.now(timezone.utc)           # correct: aware, unambiguous

# Comparing naive and aware datetimes raises TypeError -- the loud failure
# is a gift; the silent ones (DST gaps, naive arithmetic) are the dangerous ones.
from zoneinfo import ZoneInfo        # stdlib (3.9+), no pytz needed
local = datetime.now(ZoneInfo("Pacific/Auckland"))

Money: never use float. 0.1 + 0.2 != 0.3 in binary floating point. Store integer cents, or use Decimal with an explicit context:

from decimal import Decimal, ROUND_HALF_UP

0.1 + 0.2                            # 0.30000000000000004
Decimal("0.1") + Decimal("0.2")      # Decimal('0.3')  -- note: str, not float, input

price = Decimal("19.99")
tax = (price * Decimal("0.15")).quantize(Decimal("0.01"), ROUND_HALF_UP)
# Decimal(0.1) (from a float) carries the float's error -- always pass strings.

Float equality should be a tolerance check, not ==:

import math
math.isclose(computed, expected, rel_tol=1e-9)   # not  computed == expected

Copies: assignment never copies; = binds a new name to the same object. copy.copy is shallow (nested objects are shared); copy.deepcopy is recursive:

import copy
a = [[1, 2], [3, 4]]
b = a[:]                  # shallow copy of outer list
b[0].append(99)           # mutates a[0] too -- inner lists are shared!
c = copy.deepcopy(a)      # fully independent

On this page