Introduction
Python is famous for being readable, expressive, and beginner-friendly. That reputation is deserved, but it hides an important truth: Python also contains a handful of deceptively small language behaviors that cause large, expensive bugs in production systems.
Most Python pitfalls are not syntax problems. They are semantic traps rooted in object identity, scope, mutability, iteration, or numeric representation. If you understand those mechanics clearly, you can avoid the kind of bugs that look obvious in hindsight but are painful to diagnose at 2:00 a.m.
This guide covers the most common Python gotchas and shows the safe patterns to use instead.
1. Integer Division and Numeric Semantics
Python 2 and Python 3 handle division differently. That difference still appears in legacy codebases and explains many migration bugs.
Python 2 behavior
In Python 2, dividing two integers with / performs integer division.
1 / 2 # 1
Python 3 behavior
In Python 3, / always produces a floating-point result.
1 / 2 # 0.5
Safe rule
Use // when you want floor division, and use / only when you explicitly want a floating-point result.
1 // 2 # 0
5 // 2 # 2
2. Mutable Default Arguments
This is one of the most famous Python gotchas. Default argument values are evaluated once, at function definition time, not each time the function is called.
def append_to_list(value, my_list=[]):
my_list.append(value)
return my_list
print(append_to_list(1)) # [1]
print(append_to_list(2)) # [1, 2]
The list is shared across calls, which means later invocations inherit the earlier call state.
Safe pattern
Use None as the default sentinel and create a fresh container inside the function.
def append_to_list(value, my_list=None):
if my_list is None:
my_list = []
my_list.append(value)
return my_list
When it matters
- Accumulators.
- Caches.
- Request-scoped data.
- Dataclass or class constructor defaults.
3. Late Binding in Closures and Loops
Python closures capture variables by reference, not by snapshot. That means lambdas created in a loop often all see the same final value.
funcs = [lambda: x for x in range(3)]
print([f() for f in funcs]) # [2, 2, 2]
The closure looks up x when the function is executed, not when it is created.
Safe pattern
Bind the current value using a default parameter.
funcs = [lambda x=x: x for x in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
This technique is extremely common in callback-heavy code, especially when using GUI frameworks, event handlers, or async task dispatch.
4. Chained Comparisons and Boolean Mistakes
Python supports chained comparisons, which is useful and expressive.
x = 5
print(1 < x < 10) # True
But parenthesizing the wrong way can completely change the meaning.
print((1 < x) < 10) # True, but not the same expression
The first line means “x is between 1 and 10.” The second line evaluates 1 < x first, producing True, and then compares that boolean value to 10.
Safe rule
Prefer explicit, readable comparison expressions when the logic is nontrivial.
5. Nested List Aliasing
Multiplying a list creates repeated references, not deep copies.
matrix = [[0] * 3] * 3
matrix[0][0] = 1
print(matrix)
# [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
All rows point to the same inner list, so changing one row changes them all.
Safe pattern
Use a list comprehension to build independent rows.
matrix = [[0] * 3 for _ in range(3)]
Variants of the same bug
rows = [[]] * ndefaults = [obj] * ncache = [dict()] * n
6. Floating-Point Precision
Binary floating point cannot represent many decimal fractions exactly.
print(0.1 + 0.2 == 0.3) # False
This is not a Python bug. It is a property of binary floating-point representation.
Safe pattern
Use math.isclose() for approximate comparisons.
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
When precision matters
Use decimal.Decimal for currency, accounting, or other exact decimal use cases.
from decimal import Decimal
price = Decimal("0.10")
tax = Decimal("0.20")
print(price + tax)
7. Default Arguments With None and Empty Containers
The None sentinel pattern is useful, but it must be used consistently. Never write defensive code that mutates the shared default and then “cleans it up” later.
def build_index(items=None):
if items is None:
items = []
return {item: i for i, item in enumerate(items)}
This pattern makes object creation explicit and avoids accidental state leakage.
8. Truthiness Pitfalls
Python has flexible truthiness rules, which are helpful but can hide bugs.
if []:
print("never happens")
Empty containers are falsey, while non-empty containers are truthy. That behavior is useful, but it can also mask differences between None, [], 0, and "".
Safe rule
Use explicit checks when None has a different meaning from “empty.”
if value is None:
...
9. File and Resource Handling
Leaving files, sockets, or database connections open is a practical runtime problem, not just a style issue.
f = open("data.txt")
data = f.read()
If an exception happens before the file closes, the resource may leak until garbage collection. That is especially dangerous in long-running services.
Safe pattern
Use context managers.
with open("data.txt") as f:
data = f.read()
This is the Pythonic way to guarantee cleanup.
10. Import Shadowing and Module Confusion
Naming a local file json.py, random.py, or datetime.py can shadow the standard library module of the same name.
Why it hurts
- Imports resolve to the wrong file.
- Runtime behavior becomes inconsistent.
- Errors look unrelated to the real cause.
Safe rule
Avoid names that collide with standard library modules.
11. Slice and Copy Assumptions
Not every object copy is deep. Lists slice shallowly, and copied containers can still point to shared nested objects.
original = [[1], [2]]
copied = original[:]
copied[0].append(3)
print(original)
If you need full isolation, use copy.deepcopy() deliberately.
12. Practical Debugging Checklist
When Python code behaves strangely, ask these questions first:
- Is this object mutable, and is it shared across calls?
- Am I comparing exact values that should be approximate?
- Did I rely on loop variables inside a closure?
- Is a container shared because of list multiplication or default values?
- Is a module name shadowing the standard library?
Conclusion
Python is approachable, but the language rewards precision. The most dangerous pitfalls are not advanced metaprogramming tricks; they are small misunderstandings about evaluation time, mutability, scope, and numeric representation.
If you use explicit defaults, context managers, safe numeric comparisons, and careful closure patterns, you avoid most of the bugs that trip up experienced Python developers.
Comments