Skip to content

🐜 Common Bugs

Some bugs appear so frequently that every developer should recognize them instantly. Learning these patterns helps you avoid them and fix them quickly when they occur.


✅ Off-By-One Errors

# Bug: Missing last element
for i in range(len(items) - 1):  # Should be len(items)
    process(items[i])

# Bug: Index out of range
items = [1, 2, 3]
last = items[len(items)]  # Should be len(items) - 1

# Bug: Wrong loop bounds
for i in range(1, 10):  # Processes 1-9, not 1-10
    print(i)

Fix: Always verify loop bounds include/exclude correct values.


✅ Mutable Default Arguments

# Bug: Shared mutable default
def append_item(item, items=[]):  # BAD!
    items.append(item)
    return items

append_item(1)  # Returns [1]
append_item(2)  # Returns [1, 2] - same list!

# Fix: Use None as default
def append_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Rule: Never use mutable objects (lists, dicts) as default arguments.


✅ Modifying While Iterating

# Bug: Modifying list during iteration
items = [1, 2, 3, 4, 5]
for item in items:
    if item % 2 == 0:
        items.remove(item)  # Skips elements!

# Fix: Iterate over a copy or use list comprehension
items = [1, 2, 3, 4, 5]
items = [x for x in items if x % 2 != 0]

# Or iterate backwards
for i in range(len(items) - 1, -1, -1):
    if items[i] % 2 == 0:
        del items[i]

✅ Variable Scope Issues

# Bug: UnboundLocalError
counter = 0

def increment():
    counter += 1  # Error! Looks for local 'counter'
    return counter

# Fix: Use global (or better, avoid global state)
def increment():
    global counter
    counter += 1
    return counter

# Better fix: Pass and return values
def increment(counter):
    return counter + 1

✅ String/Integer Confusion

# Bug: Comparing string to int
user_input = input("Enter number: ")  # Returns string!
if user_input == 5:  # Always False
    print("Got 5")

# Fix: Convert types
if int(user_input) == 5:
    print("Got 5")

✅ None Comparisons

# Bug: Using == for None
if value == None:  # Works but not idiomatic
    pass

# Fix: Use 'is' for None
if value is None:
    pass

# Bug: Not handling None
def process(data):
    return data.upper()  # Crashes if data is None

# Fix: Check for None
def process(data):
    if data is None:
        return ""
    return data.upper()

✅ Shallow vs Deep Copy

# Bug: Shallow copy shares nested objects
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0][0] = 99
print(original)  # [[99, 2], [3, 4]] - also changed!

# Fix: Use deep copy for nested structures
import copy
deep = copy.deepcopy(original)

✅ Boolean Traps

# Bug: Empty collections are falsy
items = []
if not items:
    print("No items")  # This runs

# Bug: Zero is falsy
count = 0
if count:  # False!
    print("Has count")

# Be explicit when checking for None vs empty vs zero
if items is None:
    print("Not initialized")
elif len(items) == 0:
    print("Empty list")

✅ Late Binding in Closures

# Bug: All functions use final value of i
funcs = []
for i in range(3):
    funcs.append(lambda: i)

[f() for f in funcs]  # [2, 2, 2] - all use i=2!

# Fix: Capture current value
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # Default argument captures

[f() for f in funcs]  # [0, 1, 2]

🔍 Key Takeaways

  • Check loop bounds for off-by-one errors.
  • Never use mutable default arguments.
  • Don't modify collections while iterating them.
  • Be aware of variable scope in functions.
  • Use is for None comparisons.
  • Remember shallow copy vs deep copy.
  • Capture loop variables in closures explicitly.

Back: Reading Tracebacks | Back to Module 14