Common Pitfalls
These are mistakes that even experienced Python developers make. Know them to avoid them.
1. Mutable Default Arguments
# BUG — the list is shared between all calls!
def add_item(item: str, items: list = []) -> list:
items.append(item)
return items
print(add_item("a")) # ["a"]
print(add_item("b")) # ["a", "b"] — unexpected!
Fix
def add_item(item: str, items: list | None = None) -> list:
if items is None:
items = []
items.append(item)
return items
Never use mutable defaults
Lists, dicts, and sets as default arguments are shared between calls. Use None instead.
2. Late Binding in Closures
# BUG — all functions return 4!
funcs = []
for i in range(5):
funcs.append(lambda: i)
print([f() for f in funcs]) # [4, 4, 4, 4, 4]
Fix
funcs = []
for i in range(5):
funcs.append(lambda i=i: i)
print([f() for f in funcs]) # [0, 1, 2, 3, 4]
3. Bare except
# BAD — catches everything, including KeyboardInterrupt
try:
do_something()
except:
pass
# GOOD — catch specific exceptions
try:
do_something()
except ValueError as e:
logger.error("Invalid value: %s", e)
except ConnectionError as e:
logger.error("Connection failed: %s", e)
4. Silently Ignoring Exceptions
# BAD — you will never know about errors
try:
result = risky_operation()
except Exception:
pass
# GOOD — log the error
try:
result = risky_operation()
except Exception:
logger.exception("risky_operation failed")
raise
5. Comparing with is Instead of ==
# BAD — compares identity, not value
if x is 1000: ...
# GOOD — compares value
if x == 1000: ...
Use is only for None, True, False:
if x is None: ...
if result is True: ...
6. Modifying a List While Iterating
# BUG — skips items!
numbers = [1, 2, 3, 4, 5]
for n in numbers:
if n % 2 == 0:
numbers.remove(n)
Fix
numbers = [1, 2, 3, 4, 5]
numbers = [n for n in numbers if n % 2 != 0]
7. Not Closing Resources
# BAD — file may not be closed on error
f = open("data.txt")
data = f.read()
f.close()
# GOOD — always closed, even on error
with open("data.txt", encoding="utf-8") as f:
data = f.read()
8. Flaky Tests
Tests that sometimes pass and sometimes fail:
| Cause | Fix |
|---|---|
| Shared state between tests | Use fixtures with proper cleanup |
| Time-dependent logic | Mock the clock |
| Network calls in tests | Use mocks or test doubles |
| Order-dependent tests | Make each test independent |
| Race conditions | Add proper waits/synchronization |
# BAD — depends on external service
def test_api():
response = requests.get("https://real-api.com/users")
assert response.status_code == 200
# GOOD — use mock
def test_api(mocker):
mock_get = mocker.patch("requests.get")
mock_get.return_value.status_code = 200
response = requests.get("https://real-api.com/users")
assert response.status_code == 200
9. Overusing Inheritance
# BAD — deep hierarchy
class Animal: ...
class Mammal(Animal): ...
class Domestic(Mammal): ...
class Dog(Domestic): ...
class Labrador(Dog): ...
# GOOD — flat composition
class Dog:
def __init__(self, breed: str, is_domestic: bool) -> None:
self.breed = breed
self.is_domestic = is_domestic
10. Not Using Type Hints
# BAD — unclear types
def process(data, flag):
...
# GOOD — clear types
def process(data: list[dict[str, str]], flag: bool) -> list[str]:
...
Pitfall Summary
| Pitfall | Quick Rule |
|---|---|
| Mutable defaults | Use None, then create inside |
| Late binding | Use i=i in lambdas |
| Bare except | Always catch specific exceptions |
| Silent exceptions | Always log or re-raise |
is vs == |
Use is only for None |
| Modify while iterating | Use list comprehension |
| Unclosed resources | Use with statement |
| Flaky tests | Mock external dependencies |
| Deep inheritance | Prefer composition |
| No type hints | Always add type hints |