The Dark Side of Python: 7 Hidden Pitfalls That Can Crash Your Code

The Dark Side of Python: 7 Hidden Pitfalls That Can Crash Your Code

The Dark Side of Python: 7 Hidden Pitfalls That Can Crash Your Code

Python looks friendly, but it hides subtle traps that can break production. Here are seven that have burned me—and how to avoid them—with copy-pasteable fixes.

By ·

Python is famous for readability—but that doesn’t make it foolproof. Under the surface are behaviors that surprise even experienced developers. In this guide, you’ll see each pitfall with a wrong example and a right fix you can apply immediately.

1) Mutable Default Arguments

Default argument values are evaluated once—at function definition time. If that default is mutable, it’s shared across calls.

# ❌ Wrong: shared list across calls
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['apple', 'banana']  ← surprise
# ✅ Right: use None sentinel and create per-call
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
Rule: Avoid mutable defaults (lists, dicts, sets). Use None and initialize inside.

2) Floating-Point Surprises

Binary floating-point can’t represent some decimals exactly.

# ❌ Expectation mismatch
print(0.1 + 0.2 == 0.3)  # False
# ✅ Compare with tolerance
import math
print(math.isclose(0.1 + 0.2, 0.3))

# Or use Decimal if you need exact decimal arithmetic
from decimal import Decimal, getcontext
getcontext().prec = 28
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))  # True
When to use what: Use float for scientific/approx values; use Decimal for money or exact decimals.

3) Late Binding in Loops

Lambdas/closures capture variables by name, not value—so they see the final loop value.

# ❌ All capture the same (final) i
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2]
# ✅ Bind at definition with default arg
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]
Alternative: Use functools.partial to freeze arguments when building callbacks.

4) is vs == (Identity vs Equality)

is checks object identity; == checks value equality. Small integers and some strings may be interned, confusing identity checks.

# ❌ Identity is not value
a = 256; b = 256
print(a is b)  # True (implementation detail)
a = 257; b = 257
print(a is b)  # False
# ✅ Always use == for value comparison
print(a == b)
Use is only with singletons like None: if x is None:

5) Catching Every Exception

except: swallows keyboard interrupts, system exits, and real errors—making bugs harder to find.

# ❌ Overbroad except hides the real issue
try:
    1 / 0
except:
    print("Error!")  # Which error?
# ✅ Catch specific exceptions
try:
    1 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
Better debugging: Log exception details with logging.exception inside specific handlers.

6) Implicit None Returns

Branches that don’t return explicitly produce None, which can break callers later.

# ❌ Missing else returns None for falsy values
def double_if_truthy(x):
    if x:
        return x * 2

print(double_if_truthy(0))  # None (surprise)
# ✅ Handle all paths explicitly
def double_if_truthy(x):
    return x * 2 if x else 0
Tip: Add type hints and run a linter (mypy/ruff) to catch inconsistent return types.

7) Shadowing Built-ins

Naming variables list, dict, str hides the built-ins and leads to confusing errors.

# ❌ Overwrites built-in
list = [1, 2, 3]
print(list("123"))  # TypeError
# ✅ Use descriptive names; keep built-ins intact
numbers = [1, 2, 3]
print(list("123"))  # ok
Safety net: Linters warn on shadowing. Enable rules in ruff or flake8.

Quick Checklist

  1. Avoid mutable default args; use None.
  2. Compare floats with tolerance or use Decimal.
  3. Freeze loop variables in lambdas (i=i or partial).
  4. Use == for values; reserve is for None.
  5. Catch specific exceptions; log details.
  6. Return explicitly on all code paths.
  7. Don’t shadow built-ins; let linters help.

FAQ

How do I catch these pitfalls automatically?

Use linters (ruff, flake8) and type checkers (mypy). Add tests that cover edge cases like 0, None, and empty collections.

When should I use Decimal over float?

Use Decimal for money and exact decimal math. Stick to float for scientific/approximate calculations.

What if I already shadowed a built-in?

Rename your variable and restart the interpreter/session. In notebooks, reassigning the built-in name back to the original can be error-prone—restart is safest.

Final Thoughts

Python’s elegance comes with sharp edges. Knowing these seven pitfalls—and their fixes—will save you from late-night debugging and production surprises.

Your turn: Which pitfall got you recently? Share in the comments. If this helped, pass it to a teammate who writes Python.

Follow me on Medium

Comments

Popular posts from this blog

Docker for Beginners: Build, Ship, and Run Apps with Ease

Top 7 Real-World Projects to Learn React, Python, and AWS (Beginner to Advanced)