Skip to main content

15 Python Pitfalls That Trip Up Even Experienced Developers

Intermediate25 min7 exercises105 XP
0/7 exercises

Python is famous for being readable and beginner-friendly. But lurking beneath that clean syntax are several traps that catch even experienced developers. These aren't bugs in Python — they're features that behave differently than you'd expect.

Each pitfall in this tutorial follows the same pattern: see the problem, understand why it happens, and learn the fix. By the end, you'll have a mental checklist that saves you hours of debugging.

Pitfall #1: Mutable Default Arguments

This is the single most common Python gotcha. When you use a mutable object (like a list or dict) as a default argument, Python creates it once when the function is defined — not each time the function is called. Every call shares the same object.

The mutable default argument trap
Loading editor...

The list [] is created once when Python reads the def statement. Every subsequent call mutates that same list. This is one of the most FAQ'd Python behaviors.

Buggy: mutable default
def add_item(item, lst=[]):
    lst.append(item)
    return lst
Fixed: use None sentinel
def add_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

Pitfall #2: Late Binding Closures

When you create functions inside a loop, they don't capture the value of the loop variable — they capture a reference to it. By the time you call the function, the loop is done and the variable holds its final value.

Late binding closure trap
Loading editor...

All four lambdas reference the same variable i. When they're called after the loop, i equals 3. The fix is to capture the current value using a default argument.

Buggy: late binding
fns = []
for i in range(4):
    fns.append(lambda: i)
# All return 3
Fixed: capture with default arg
fns = []
for i in range(4):
    fns.append(lambda i=i: i)
# Returns 0, 1, 2, 3

Pitfall #3: `is` vs `==` — Identity vs Equality

== checks if two objects have the same value. is checks if two variables point to the same object in memory. These are completely different questions, but Python's integer caching makes them seem interchangeable for small numbers.

Identity vs equality
Loading editor...

Pitfall #4: Modifying a List While Iterating

Removing items from a list while looping over it causes Python to skip elements. The loop uses an internal index counter, and when you remove an item, everything shifts — but the counter keeps going.

Never modify a list while iterating over it
Loading editor...

Pitfall #5: Shallow Copy vs Deep Copy

A shallow copy duplicates the outer container but keeps references to the same inner objects. If those inner objects are mutable, changes to them affect both the original and the copy.

Shallow vs deep copy
Loading editor...

Pitfall #6: The UnboundLocalError Trap

If you assign to a variable inside a function, Python treats it as a local variable for the entire function — even lines before the assignment. This causes a confusing UnboundLocalError.

UnboundLocalError explained
Loading editor...

Pitfall #7: String Concatenation in Loops

Strings in Python are immutable. Every time you do s += "more", Python creates a brand new string, copies the old content, and appends the new part. In a loop, this is O(n^2) — painfully slow for large data.

Slow: repeated concatenation
result = ""
for word in words:
    result += word + " "
# O(n^2) — copies everything each time
Fast: join()
result = " ".join(words)
# O(n) — one allocation
String join vs concatenation
Loading editor...

Pitfall #8: The Bare `except:` Clause

A bare except: catches everything — including KeyboardInterrupt (Ctrl+C) and SystemExit. This can make your program impossible to stop and hide real bugs.

Dangerous: bare except
try:
    result = risky_operation()
except:
    print("Something went wrong")
# Catches KeyboardInterrupt, SystemExit,
# MemoryError... everything!
Safe: specific exception
try:
    result = risky_operation()
except ValueError as e:
    print(f"Invalid value: {e}")
except Exception as e:
    print(f"Error: {e}")
# Lets KeyboardInterrupt through
Catching specific exceptions
Loading editor...

Pitfall #9: Python's Integer Cache

Python pre-creates integer objects for -5 through 256 and reuses them everywhere. This means is comparisons "work" for small numbers but fail for larger ones — a subtle trap if you use is instead of == for comparison.

Integer caching behavior
Loading editor...

Practice Exercises

Fix the Mutable Default
Fix the Bug

The create_group function is supposed to create independent groups, but all groups share the same member list. Fix the bug so each call creates a fresh list.

After the fix, the code should print two independent groups.

Loading editor...
Predict the Output: Late Binding
Predict Output

What does this code print? Think carefully about when the lambda reads the value of i.

functions = []
for i in range(3):
    functions.append(lambda i=i: i * 10)

results = [f() for f in functions]
print(results)
Loading editor...
Safe List Filtering
Write Code

Write a function remove_negatives(numbers) that returns a new list with all negative numbers removed. Do NOT modify the original list.

Test with nums = [3, -1, 4, -5, 2, -3, 6]. Print the result of remove_negatives(nums) and then print nums to prove it's unchanged.

Loading editor...
Fix the Bug: Shallow Copy Surprise
Fix the Bug

The code below tries to create an independent copy of a matrix, but modifying the copy also changes the original. Fix it so the original stays unchanged.

The output should show the original matrix unchanged after modifying the copy.

Loading editor...
Predict the Output: Variable Scope
Predict Output

What does this code print? Pay attention to how Python resolves variable names.

x = "global"

def outer():
    x = "outer"
    def inner():
        print(x)
    inner()

outer()
print(x)
Loading editor...
Safe Exception Handling
Refactor

Refactor this function to use specific exception handling instead of a bare except:. The function should:

  • Catch ZeroDivisionError and print "Error: division by zero"
  • Catch TypeError and print "Error: invalid types"
  • Print the result for valid inputs
  • Test with safe_calc(10, 2), safe_calc(10, 0), and safe_calc(10, "a").

    Loading editor...
    Build a Safe Defaults Function
    Write Code

    Write a function build_profile(name, hobbies=None, scores=None) that:

  • Creates a new list for hobbies if None, otherwise uses the given list
  • Creates a new dict for scores if None, otherwise uses the given dict
  • Returns a dictionary: {'name': name, 'hobbies': hobbies, 'scores': scores}
  • Create two profiles independently:

    1. p1 = build_profile('Alice') then add 'reading' to p1['hobbies']

    2. p2 = build_profile('Bob') then add 'gaming' to p2['hobbies']

    Print p1['hobbies'] and p2['hobbies'] to show they're independent.

    Loading editor...