Skip to main content
โšก Calmops

Pitfalls in Python: A Deep Dive Into Common Gotchas

Pitfalls and Gotchas in Python

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 = [[]] * n
  • defaults = [obj] * n
  • cache = [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:

  1. Is this object mutable, and is it shared across calls?
  2. Am I comparing exact values that should be approximate?
  3. Did I rely on loop variables inside a closure?
  4. Is a container shared because of list multiplication or default values?
  5. 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.

References

Comments