| name | python-gotchas |
| description | Complete Python gotchas reference. PROACTIVELY activate for: (1) Mutable default arguments, (2) Mutating lists while iterating, (3) is vs == comparison, (4) Late binding in closures, (5) Variable scope (LEGB), (6) Floating point precision, (7) Exception handling pitfalls, (8) Dict mutation during iteration, (9) Circular imports, (10) Class vs instance attributes. Provides: Problem explanations, code examples, fixes for each gotcha. Ensures bug-free Python code. |
Quick Reference
| Gotcha | Problem | Fix |
|---|
| Mutable default | def f(x=[]) | Use None, create in function |
| Iterate + mutate | Skips items | Iterate over copy items[:] |
is vs == | Identity vs value | Use is only for None |
| Late binding | lambda: i captures var | lambda i=i: i |
| Float precision | 0.1 + 0.2 != 0.3 | math.isclose() |
| Dict mutation | RuntimeError | list(d.keys()) |
| Class attribute | Shared mutable | Init in __init__ |
| Falsy Values | Examples |
|---|
| Boolean | False |
| None | None |
| Numbers | 0, 0.0, 0j |
| Empty collections | "", [], {}, set() |
| Scope Rule | Order |
|---|
| LEGB | Local → Enclosing → Global → Built-in |
global | Access module-level variable |
nonlocal | Access enclosing function variable |
When to Use This Skill
Use for debugging and prevention:
- Understanding why code behaves unexpectedly
- Avoiding common Python pitfalls
- Reviewing code for subtle bugs
- Learning Python's evaluation rules
- Fixing mutable default arguments
Related skills:
- For fundamentals: see
python-fundamentals-313
- For testing: see
python-testing
- For type hints: see
python-type-hints
Python Common Gotchas and Pitfalls
Overview
Python has several well-known pitfalls that trip up developers of all experience levels. Understanding these gotchas prevents subtle bugs and unexpected behavior.
1. Mutable Default Arguments
The Problem
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a"))
print(add_item("b"))
print(add_item("c"))
Why It Happens
Default arguments are evaluated once when the function is defined, not each time it's called. The same list object is reused across all calls.
The Fix
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("a"))
print(add_item("b"))
print(add_item("c"))
Other Mutable Defaults
def bad_dict(data={}): ...
def bad_set(data=set()): ...
def bad_class(config=SomeClass()): ...
def good_dict(data=None):
if data is None:
data = {}
return data
def good_set(data=None):
if data is None:
data = set()
return data
2. Mutating Lists While Iterating
The Problem
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
if num % 2 == 0:
numbers.remove(num)
print(numbers)
Why It Happens
The iterator uses indices internally. When you remove an item, all subsequent indices shift, causing items to be skipped.
The Fixes
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers[:]:
if num % 2 == 0:
numbers.remove(num)
print(numbers)
numbers = [1, 2, 3, 4, 5, 6]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers)
numbers = [1, 2, 3, 4, 5, 6]
numbers = list(filter(lambda x: x % 2 != 0, numbers))
print(numbers)
numbers = [1, 2, 3, 4, 5, 6]
for i in range(len(numbers) - , -, -):
numbers[i] % == :
numbers[i]
(numbers)
3. is vs ==
The Problem
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)
print(a is b)
x = 256
y = 256
print(x is y)
x = 257
y = 257
print(x is y)
The Rule
- Use
== to compare values
- Use
is only for identity (singletons like None, True, False)
if value is None:
...
if value == other_value:
...
if value is 0:
...
4. Variable Scope (LEGB)
The Problem
functions = []
for i in range(3):
functions.append(lambda: i)
print([f() for f in functions])
Why It Happens
The lambda captures the variable i, not its value. By the time lambdas are called, i is 2.
The Fixes
functions = []
for i in range(3):
functions.append(lambda i=i: i)
print([f() for f in functions])
from functools import partial
def return_value(x):
return x
functions = [partial(return_value, i) for i in range(3)]
print([f() for f in functions])
UnboundLocalError
x = 10
def increment():
x = x + 1
return x
def increment():
global x
x = x + 1
return x
def increment(x):
return x + 1
5. String Concatenation
Implicit Concatenation Gotcha
items = [
"apple"
"banana"
"cherry"
]
print(items)
items = [
"apple",
"banana",
"cherry",
]
Type Mixing
name = "User"
count = 42
message = f"Hello {name}, you have {count} messages"
message = "Hello " + name + ", you have " + str(count) + " messages"
6. Late Binding in Closures
The Problem
class MyClass:
def __init__(self, callbacks=[]):
self.callbacks = callbacks
def add_callback(self, func):
self.callbacks.append(func)
obj1 = MyClass()
obj2 = MyClass()
obj1.add_callback(lambda: print("Hello"))
print(len(obj2.callbacks))
The Fix
class MyClass:
def __init__(self, callbacks=None):
self.callbacks = callbacks if callbacks is not None else []
def add_callback(self, func):
self.callbacks.append(func)
7. Boolean Evaluation
Falsy Values
falsy_values = [
False,
None,
0,
0.0,
0j,
"",
[],
{},
set(),
range(0),
]
data = []
if data:
print("Has data")
else:
print("No data")
if data is None:
print("Is None")
elif data == []:
print("Is empty list")
Explicit Checks
def process(items):
if not items:
return
def process(items):
if items is None:
raise ValueError("items cannot be None")
if len(items) == 0:
return
8. Floating Point Precision
The Problem
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
The Fixes
import math
from decimal import Decimal
print(math.isclose(0.1 + 0.2, 0.3))
price = Decimal("19.99")
tax = Decimal("0.0875")
total = price * (1 + tax)
print(total)
print(round(total, 2))
9. Exception Handling
Bare Except
try:
risky_operation()
except:
pass
try:
risky_operation()
except Exception:
pass
try:
risky_operation()
except ValueError as e:
logger.error(f"Invalid value: {e}")
except ConnectionError as e:
logger.error(f"Connection failed: {e}")
raise
Exception Variable Scope
try:
1 / 0
except ZeroDivisionError as e:
error = e
print(e)
print(error)
10. Dictionary Key Ordering
Modern Python (3.7+)
d = {"b": 2, "a": 1, "c": 3}
print(list(d.keys()))
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2)
Gotcha: Changing Dict During Iteration
d = {"a": 1, "b": 2, "c": 3}
for key in d:
if d[key] == 2:
del d[key]
d = {"a": 1, "b": 2, "c": 3}
for key in list(d.keys()):
if d[key] == 2:
del d[key]
print(d)
d = {"a": 1, "b": 2, "c": 3}
d = {k: v for k, v in d.items() if v != 2}
11. Import Gotchas
Circular Imports
from module_b import func_b
def func_a():
return func_b()
def func_a():
from module_b import func_b
return func_b()
import module_b
def func_a():
return module_b.func_b()
Module Name Shadowing
import random
random.randint(1, 10)
12. Class Attribute vs Instance Attribute
class MyClass:
shared_list = []
def add_item(self, item):
self.shared_list.append(item)
a = MyClass()
b = MyClass()
a.add_item("hello")
print(b.shared_list)
class MyClass:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
Quick Reference
| Gotcha | Problem | Solution |
|---|
| Mutable defaults | def f(x=[]) | Use None, create in function |
| Mutating while iterating | Skips items | Iterate over copy or use comprehension |
is vs == | Identity vs equality | Use is only for None, True, False |
| Late binding | Captures variable, not value | Use default argument to capture |
| Falsy values | Empty != None | Be explicit in checks |
| Float precision | 0.1 + 0.2 != 0.3 | Use math.isclose() or Decimal |
| Bare except | Catches too much | Catch specific exceptions |
| Dict iteration | Can't modify during | Iterate over list(d.keys()) |
| Circular imports | Import errors | Import inside function or import module |
| Class attributes | Shared unexpectedly | Initialize in __init__ |