Common Mistakes
Python is forgiving syntax, but that forgiveness hides a handful of traps that catch beginners and experienced developers alike. None of these are exotic — they show up in real codebases, in real code reviews, and in real production incidents. This page rounds up the mistakes that come up again and again, why they happen, and how to avoid them.
Quick reference
Mistake | Why it happens | Fix |
|---|---|---|
Mutable default arguments | The default value is created once, when the function is defined, not on every call. | Use |
Modifying a list while iterating it | Removing or inserting items shifts indices under the iterator, so items get skipped. | Iterate over a copy ( |
Comparing floats with | Floating-point numbers cannot represent most decimals exactly. | Use |
Using |
| Use |
Forgetting | Instance methods are called on an instance, and Python passes it implicitly as the first argument. | Always declare instance methods as |
Late-binding closures in loops | Functions created in a loop share the loop variable — they look it up when called, not when created. | Capture the value with a default argument or |
Circular imports | Two modules import each other at module load time, so one of them sees a half-initialised module. | Restructure the shared code into a third module, or move the import inside the function that needs it. |
Bare | It silently swallows every exception, including ones you never meant to catch. | Catch |
Mutable default arguments
Default argument values are evaluated once, when the def statement runs — not each time the function is called. If the default is a mutable object like a list or dict, every call that relies on the default shares the same object.
broken
def add_item(item, items=[]):
items.append(item)
return items
print(add_item('a')) # ['a']
print(add_item('b')) # ['a', 'b'] <- surprise! same list every callfixed
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item('a')) # ['a']
print(add_item('b')) # ['b']Modifying a list while iterating over it
When you loop over a list with for x in lst, Python tracks the current index internally. Removing an element shifts every element after it down by one, so the next index the loop visits skips an item.
broken — skips elements
nums = [1, 2, 3, 4, 5, 6]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # [1, 3, 5] <- looks right by luck, but try [2, 4, 6, 8] and seefixed — iterate a copy, or build a new list
nums = [1, 2, 3, 4, 5, 6]
# Option 1: iterate over a copy
for n in nums[:]:
if n % 2 == 0:
nums.remove(n)
# Option 2: comprehension (preferred)
nums = [1, 2, 3, 4, 5, 6]
nums = [n for n in nums if n % 2 != 0]Comparing floats with `==`
Floating-point numbers are stored in binary, and most decimal fractions cannot be represented exactly. This means arithmetic that looks correct on paper can fail an equality check.
broken
print(0.1 + 0.2 == 0.3) # False print(0.1 + 0.2) # 0.30000000000000004
fixed
import math print(math.isclose(0.1 + 0.2, 0.3)) # True
Using `is` instead of `==`
is checks whether two names point to the exact same object in memory; == checks whether two objects are equal in value. CPython caches small integers and some string literals, so is can appear to work — until it does not.
broken — works by accident, then breaks
a = 1000 b = 1000 print(a is b) # False on most CPython builds — not cached print(a == b) # True — this is what you actually want x = 5 y = 5 print(x is y) # True — small ints ARE cached, but this is an implementation detail print(x == y) # True — reliable regardless of implementation
Forgetting `self`
When Python calls instance.method(arg), it desugars to ClassName.method(instance, arg) — the instance is always passed as the first parameter. Omit it in the signature and Python will either raise a TypeError about too many arguments, or (if you also forget to declare the method inside the class body correctly) fail in a more confusing way.
broken
class Counter:
def increment(): # missing self
pass
c = Counter()
c.increment()
# TypeError: increment() takes 0 positional arguments but 1 was givenfixed
class Counter:
def increment(self):
self.count = getattr(self, 'count', 0) + 1
c = Counter()
c.increment()Late-binding closures in loops
A function defined inside a loop does not "freeze" the loop variable's current value — it looks the variable up when the function is called, by which point the loop has finished and the variable holds its final value. This is the classic "lambda in a loop" bug.
broken — every lambda prints the same value
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] <- not [0, 1, 2]!fixed — capture the value with a default argument
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # i=i captures the current value now
print([f() for f in funcs]) # [0, 1, 2]alternative fix — functools.partial
from functools import partial
def identity(i):
return i
funcs = [partial(identity, i) for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]Circular imports
A circular import happens when module A imports module B, and module B (directly or indirectly) imports module A. The symptom is usually an ImportError or AttributeError complaining that a name "partially initialized module" has no attribute — because whichever module runs second sees an incomplete version of the other.
the shape of the problem
# models.py from services import save_user # imports services # services.py from models import User # imports models -> circular!
The most durable fix is to restructure: pull the shared pieces (like User) into a third module that both models.py and services.py depend on, so neither imports the other. A quick, pragmatic workaround is to move the import inside the function that needs it, so it only runs after both modules have finished loading:
quick workaround — local import
# services.py
def save_user(user):
from models import User # imported lazily, when the function runs
...Bare `except:` clauses
A bare except: catches everything — not just the exceptions you expected, but typos that raise NameError, KeyboardInterrupt when the user presses Ctrl+C, and even SystemExit. This makes bugs invisible and programs impossible to stop cleanly.
broken
try:
result = compute_soemthing() # typo — should be compute_something
except:
result = None
# The NameError from the typo is silently swallowed. Good luck debugging this.fixed
try:
result = compute_something()
except Exception as exc:
logging.exception('computation failed: %s', exc)
result = None