15 Python Pitfalls That Trip Up Even Experienced Developers
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 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.
def add_item(item, lst=[]):
lst.append(item)
return lstdef add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lstPitfall #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.
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.
fns = []
for i in range(4):
fns.append(lambda: i)
# All return 3fns = []
for i in range(4):
fns.append(lambda i=i: i)
# Returns 0, 1, 2, 3Pitfall #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.
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.
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.
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.
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.
result = ""
for word in words:
result += word + " "
# O(n^2) — copies everything each timeresult = " ".join(words)
# O(n) — one allocationPitfall #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.
try:
result = risky_operation()
except:
print("Something went wrong")
# Catches KeyboardInterrupt, SystemExit,
# MemoryError... everything!try:
result = risky_operation()
except ValueError as e:
print(f"Invalid value: {e}")
except Exception as e:
print(f"Error: {e}")
# Lets KeyboardInterrupt throughPitfall #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.
Practice Exercises
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.
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)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.
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.
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)Refactor this function to use specific exception handling instead of a bare except:. The function should:
ZeroDivisionError and print "Error: division by zero"TypeError and print "Error: invalid types"Test with safe_calc(10, 2), safe_calc(10, 0), and safe_calc(10, "a").
Write a function build_profile(name, hobbies=None, scores=None) that:
{'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.