Python
8 Common Python Mistakes That Even Junior Developers Make
Avoid the most frequent Python pitfalls — from mutable default arguments to modifying lists while iterating — with clear fixes and explanations for each bug.
June 2026 · 6 min read · 1 views · 0 hearts
Advertisement
Python is one of the most forgiving languages to learn, but that same flexibility can quietly lead you into traps that only reveal themselves when your code breaks at 2 AM. I’ve debugged enough junior dev projects to know the classic pitfalls by heart—here are the most common ones and how to sidestep them.
Mutable Default Arguments
You’ve probably written a function like this:
def add_item(item, items=[]):
items.append(item)
return items
Looks innocent. But call it twice:
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — wait, what?
The default [] is created once when the function is defined, not each time you call it. So every call shares the same list.
Fix: Use None and create a new list inside:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Using == Instead of is for None Checks
Python has two equality operators: == checks value, is checks identity. For None, you almost always want is because None is a singleton—there’s only one of it.
x = None
if x == None: # works, but wrong style
if x is None: # correct and faster
Why does it matter? Custom classes can override __eq__ and make x == None return True for non-None objects. Using is avoids that surprise.
Forgetting That Variables Can Be Mutable
Lists, dictionaries, and sets are mutable. If you assign one to another variable, they point to the same object:
original = [1, 2, 3]
copy = original
copy.append(4)
print(original) # [1, 2, 3, 4] — oops
Fix: Use .copy(), list(), or copy.deepcopy() for nested structures:
copy = original.copy()
Confusing == and is for String Comparison
is checks memory location, not value. Python sometimes caches small strings, so "hello" is "hello" might return True—until your program grows and it doesn’t. That kind of bug is a nightmare to trace.
a = "hello"
b = "hello"
print(a is b) # True in CPython, but not guaranteed
print(a == b) # True always
Rule of thumb: use == for strings. Use is only for None, True, False.
Not Using with for File Handling
Opening a file and forgetting to close it is a classic resource leak. In Python, files are automatically garbage-collected eventually, but you don’t control when.
f = open("data.txt", "r")
data = f.read()
# Program crashes here — file stays open
Fix: Use the context manager:
with open("data.txt", "r") as f:
data = f.read()
# File closes automatically, even if an exception occurs
Catching Too Broad an Exception
Bare except: or except Exception: hides bugs. You might accidentally catch KeyboardInterrupt or SystemExit and swallow them.
try:
result = risky_operation()
except: # catches everything including Ctrl+C
pass
Fix: Catch specific exceptions:
try:
result = risky_operation()
except ValueError:
handle_value_error()
Or at least re-raise unknown ones.
Modifying a List While Iterating
This is subtle. Removing elements while looping skips items:
nums = [1, 2, 3, 4, 5]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # [1, 3, 5] — correct by accident, usually not
But more often you’ll get IndexError or skip elements. Better to build a new list:
nums = [n for n in nums if n % 2 != 0]
Or iterate over a copy:
for n in nums[:]:
if n % 2 == 0:
nums.remove(n)
Accidental Global Variable Modification Inside Functions
Inside a function, assigning to a variable creates a local variable unless you declare global. This is usually fine—but if you forget, you might think you’re modifying a global when you’re actually creating a new local.
count = 0
def increment():
count += 1 # UnboundLocalError
def increment_global():
global count
count += 1 # works
The fix: use global explicitly, or better, avoid mutable global state. Pass and return values instead.
Misunderstanding for Loop Scoping in Python 2
If you’re still maintaining Python 2 code (and you shouldn’t be, but some of us have to), note that loop variables leak into the enclosing scope. Python 3 fixed this by making the loop variable local to the comprehension or loop body.
# Python 2
for x in range(5):
pass
print(x) # 4
In Python 3, x still exists after the loop but only holds the last iterated value. Not ideal, but at least it’s consistent.
Overusing lambda When Named Functions Are Clearer
lambda is great for short, one-off functions in map() or filter(). But when the logic gets more than one line, or you use it repeatedly, a named function improves readability.
# Confusing
process = lambda x: x**2 + 3*x - 5 if x > 0 else 0
# Clearer
def process(x):
return x**2 + 3*x - 5 if x > 0 else 0
Also, lambda can’t contain statements (only expressions), so you can’t use print() or return inside it.
The best way to catch these mistakes is experience—and a good linter. Run pylint or flake8 on your code regularly, and watch for warnings about mutable defaults, unused variables, or broad exceptions. Python’s readability is your superpower, but only if you avoid the hidden landmines.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.