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 Udbhav ·
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
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
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]
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)
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")
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
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
ruff or flake8.Quick Checklist
- Avoid mutable default args; use
None. - Compare floats with tolerance or use
Decimal. - Freeze loop variables in lambdas (
i=iorpartial). - Use
==for values; reserveisforNone. - Catch specific exceptions; log details.
- Return explicitly on all code paths.
- 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
Post a Comment