PY

Common Mistakes

Python · 8 entries

Mutable Default Arguments

syntax
# BAD
def f(items=[]):
# GOOD
def f(items=None):
example
# WRONG: Shared mutable default
def add_tag_bad(tag, tags=[]):
    tags.append(tag)
    return tags

print(add_tag_bad("a"))
print(add_tag_bad("b"))  # Surprise!

# CORRECT: Use None sentinel
def add_tag(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

print(add_tag("a"))
print(add_tag("b"))
output
['a']
['a', 'b']
['a']
['b']

Note Default values are evaluated once at function definition time, not on each call. Mutable defaults (list, dict, set) are shared across calls. Always use None as default.

Late Binding in Closures

syntax
# BAD: variable captured by reference
# GOOD: use default argument to capture value
example
# WRONG: All lambdas see final value of i
funcs_bad = [lambda: i for i in range(4)]
print([f() for f in funcs_bad])

# CORRECT: Capture current value via default arg
funcs_good = [lambda i=i: i for i in range(4)]
print([f() for f in funcs_good])
output
[3, 3, 3, 3]
[0, 1, 2, 3]

Note Python closures capture variables by reference, not by value. The loop variable's final value is what all closures see. The i=i default-argument trick forces a copy at each iteration.

Modifying a List During Iteration

syntax
# BAD: for x in items: items.remove(x)
# GOOD: iterate over a copy or build a new list
example
# WRONG: Modifying during iteration skips elements
nums = [1, 2, 3, 4, 5, 6]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
print(f"Wrong: {nums}")  # 4 was skipped!

# CORRECT: List comprehension to filter
nums = [1, 2, 3, 4, 5, 6]
result = [n for n in nums if n % 2 != 0]
print(f"Right: {result}")
output
Wrong: [1, 3, 5]
Right: [1, 3, 5]

Note Removing items while iterating shifts indices and causes skipped elements. Use a comprehension, filter(), or iterate over a copy (list[:]) instead.

is vs == (Identity vs Equality)

syntax
a == b  # equal values
a is b  # same object
example
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)
print(a is b)

# CPython caches small integers
x = 256
y = 256
print(x is y)

x = 257
y = 257
print(x is y)  # May be False!
output
True
False
True
False

Note Use == for value comparison. Use 'is' only for None, True, False, or when you specifically need identity checks. CPython caches integers -5 to 256, making 'is' unreliable for numbers.

Global vs Local Variable Confusion

syntax
global var_name  # to modify a global
nonlocal var_name  # to modify enclosing scope
example
counter = 0

def increment_wrong():
    # UnboundLocalError: counter referenced before assignment
    # counter += 1  # This fails!
    pass

def increment_correct():
    global counter
    counter += 1

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

tick = make_counter()
print(tick(), tick(), tick())
output
1 2 3

Note If a function assigns to a variable, Python treats it as local in the entire function. Reading a global is fine, but modifying requires 'global'. For enclosing scopes, use 'nonlocal'.

Shallow vs Deep Copy

syntax
import copy
shallow = copy.copy(obj)
deep = copy.deepcopy(obj)
example
import copy

original = [[1, 2], [3, 4]]
shallow = original.copy()
deep = copy.deepcopy(original)

original[0].append(99)
print(f"Original: {original}")
print(f"Shallow:  {shallow}")
print(f"Deep:     {deep}")
output
Original: [[1, 2, 99], [3, 4]]
Shallow:  [[1, 2, 99], [3, 4]]
Deep:     [[1, 2], [3, 4]]

Note list.copy(), dict.copy(), and slicing (lst[:]) all produce shallow copies. Nested mutable objects are still shared. Use copy.deepcopy() when you need fully independent nested structures.

Forgetting Strings Are Immutable

syntax
# BAD: s.replace(...) without reassigning
# GOOD: s = s.replace(...)
example
name = "  Alice  "

# WRONG: result is discarded
name.strip()
print(f"'{name}'")  # unchanged!

# CORRECT: reassign the result
name = name.strip()
print(f"'{name}'")
output
'  Alice  '
'Alice'

Note All string methods return new strings. Forgetting to capture the return value is a very common bug. The same applies to str.replace(), str.lower(), etc.

Truthiness Gotchas

syntax
# Falsy: None, False, 0, 0.0, '', [], {}, set()
example
def process(data=None):
    # WRONG: if not data
    # This also catches empty list [], 0, and ""
    if not data:
        print("No data")  # Triggered by empty list too!

    # CORRECT: if data is None
    if data is None:
        print("Data is None")

process([])   # Might not intend to reject []
process(None)
process(0)
output
No data
No data
Data is None
No data

Note Empty collections, zero, empty string, and None are all falsy. When you specifically mean 'no value provided', check 'is None' rather than relying on truthiness.