Python Basics — Common Mistakes
Common Mistakes
Every Python beginner makes these mistakes. Learning to spot and avoid them will save you hours of debugging.
1. Mutable Default Arguments
# ❌ BUG: Default list is shared between all calls
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] ← Expected ['b']!
# ✅ FIX: Use None as default
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['b'] ✓
Why: Default arguments are evaluated once when the function is defined, not each time it’s called. Mutable defaults (lists, dicts, sets) persist between calls.
2. Modifying a List While Iterating
# ❌ BUG: Skips elements
numbers = [1, 2, 3, 4, 5, 6]
for n in numbers:
if n % 2 == 0:
numbers.remove(n)
print(numbers) # [1, 3, 5, 6] ← 6 was skipped!
# ✅ FIX: Create a new list
numbers = [1, 2, 3, 4, 5, 6]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers) # [1, 3, 5] ✓
# ✅ FIX: Iterate over a copy
numbers = [1, 2, 3, 4, 5, 6]
for n in numbers[:]: # [:] creates a copy
if n % 2 == 0:
numbers.remove(n)
print(numbers) # [1, 3, 5] ✓
3. == vs is Confusion
# == checks VALUE equality
# is checks IDENTITY (same object in memory)
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — same values
print(a is b) # False — different objects
# ✅ Use == for value comparison
if user_input == "yes":
proceed()
# ✅ Use is only for None, True, False
if result is None:
handle_none()
# ❌ Don't use is for strings or numbers (unreliable)
a = 1000
b = 1000
print(a is b) # May be True or False — implementation detail!
4. Off-by-One Errors
# ❌ Missing the last element
items = ["a", "b", "c", "d", "e"]
for i in range(len(items) - 1): # Stops at index 3, misses "e"
print(items[i])
# ✅ FIX
for i in range(len(items)): # Goes through all indices
print(items[i])
# ✅ Even better — iterate directly
for item in items:
print(item)
# ❌ Common range mistake
for i in range(1, 5):
print(i) # Prints 1, 2, 3, 4 — NOT 5!
5. Scope / UnboundLocalError
# ❌ BUG: Assignment creates a new local variable
count = 0
def increment():
count = count + 1 # UnboundLocalError!
return count
# ✅ FIX: Use global (sparingly)
count = 0
def increment():
global count
count += 1
return count
# ✅ Better: Pass and return
def increment(count):
return count + 1
count = 0
count = increment(count)
6. Forgetting to Return
# ❌ BUG: Function returns None
def add(a, b):
result = a + b
# Forgot return!
total = add(3, 4)
print(total) # None
# ✅ FIX
def add(a, b):
return a + b
total = add(3, 4)
print(total) # 7
7. String Immutability
# ❌ Strings can't be modified in place
name = "hello"
name[0] = "H" # TypeError!
# ✅ Create a new string
name = "hello"
name = "H" + name[1:]
print(name) # Hello
# ✅ Or use replace()
name = "hello"
name = name.replace("h", "H")
print(name) # Hello
8. Integer Division Surprise
# In Python 3, / always returns float
print(10 / 3) # 3.3333...
print(10 / 2) # 5.0 (not 5!)
# Use // for integer division
print(10 // 3) # 3
print(10 // 2) # 5
# ❌ Gotcha with negative numbers
print(-7 // 2) # -4 (not -3! — rounds toward negative infinity)
9. Using = Instead of ==
# ❌ Assignment, not comparison (SyntaxError in if statement)
# if x = 5: # SyntaxError in Python (thankfully!)
# But this silent bug can happen:
x = 5 # Assignment
x == 5 # Comparison (True), but result is unused
# ✅ Walrus operator (Python 3.8+) for assignment in expressions
if (n := len(items)) > 10:
print(f"Too many items: {n}")
10. Forgetting self in Methods
# ❌ BUG: Missing self
class Dog:
def __init__(name): # Missing self!
name = name
def bark(): # Missing self!
print("Woof!")
# ✅ FIX
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} says Woof!")
11. Dictionary Key Errors
# ❌ Crashes on missing key
user = {"name": "Alice", "age": 30}
print(user["email"]) # KeyError!
# ✅ Use .get() with default
email = user.get("email", "not provided")
print(email) # not provided
# ✅ Or check first
if "email" in user:
print(user["email"])
12. Not Closing Files
# ❌ File might stay open
f = open("data.txt", "r")
data = f.read()
# Forgot f.close()!
# ✅ Always use with
with open("data.txt", "r") as f:
data = f.read()
# Automatically closed
13. Confusing append() and extend()
a = [1, 2, 3]
# append() adds ONE item (even if it's a list)
a.append([4, 5])
print(a) # [1, 2, 3, [4, 5]]
# extend() adds EACH item from an iterable
a = [1, 2, 3]
a.extend([4, 5])
print(a) # [1, 2, 3, 4, 5]
# += is the same as extend
a = [1, 2, 3]
a += [4, 5]
print(a) # [1, 2, 3, 4, 5]
14. Chained Comparisons Surprise
# Python supports chained comparisons (this is a feature!)
x = 5
print(1 < x < 10) # True — same as (1 < x) and (x < 10)
print(1 < x > 3) # True — same as (1 < x) and (x > 3)
# ❌ But this can be confusing:
print(1 == True) # True (True equals 1)
print(0 == False) # True (False equals 0)
print("" == False) # False (different types)
15. Import Circular Dependencies
# ❌ module_a.py imports module_b, and module_b imports module_a
# This causes ImportError or unexpected behavior
# module_a.py
# from module_b import func_b # ❌ Circular!
# ✅ FIX: Restructure code, or import inside the function
def func_a():
from module_b import func_b # Import when needed
return func_b()
Quick Reference Card
| Mistake | Fix |
|---|---|
| Mutable default args | Use None, create new object in function |
| Modify list while iterating | Use comprehension or iterate over copy |
== vs is |
Use == for values, is only for None |
| Off-by-one | Use range(len(x)) or iterate directly |
| UnboundLocalError | Pass values as arguments, return results |
Forgot return |
Always check functions return a value |
| String mutation | Strings are immutable — create new ones |
Missing self |
First parameter of methods is always self |
| KeyError | Use .get() with default value |
| File not closed | Always use with statement |
Next up: Python Best Practices →