Skip to main content
โšก Calmops

Common Python Pitfalls: Complete Guide for 2026

Introduction

Python’s operator precedence can lead to subtle bugs, especially when mixing matrix multiplication (@) with indexing. Understanding these nuances is crucial for writing correct Python code. This comprehensive guide covers common pitfalls, best practices, and debugging strategies for Python developers in 2026.

The Matrix Multiplication Pitfall

The Problem

The @ operator (Python 3.5+) has different precedence than you might expect:

import numpy as np

theta = np.array([[1, 2], [3, 4]])
current_state_vector = np.array([5, 6])
action_index, object_index = 0, 0

# WRONG: This applies indexing to the vector, not the result
cur_q_value = theta @ current_state_vector[tuple2index(action_index, object_index)]
# Equivalent to: theta @ (current_state_vector[0])

# CORRECT: Use parentheses
cur_q_value = theta @ current_state_vector
cur_q_value = cur_q_value[tuple2index(action_index, object_index)]

# Or:
cur_q_value = (theta @ current_state_vector)[tuple2index(action_index, object_index)]

Why This Happens

Expression Interpretation
theta @ vec[i] Matrix multiply theta with vec[i]
(theta @ vec)[i] Multiply theta with vec, then index result

The indexing operator [] has higher precedence than @.

Modern Type Checking

# Use type hints to catch issues early
import numpy as np
from numpy.typing import NDArray

def compute_q_value(
    theta: NDArray[np.float64],
    state_vector: NDArray[np.float64],
    action_index: int,
    object_index: int
) -> float:
    """Compute Q value with explicit parentheses"""
    # Explicit is better than implicit
    result = theta @ state_vector
    return result[action_index * 10 + object_index]

Other Common Precedence Pitfalls

1. Chained Comparisons

# What you might think:
result = 1 < x < 5  # Equivalent to (1 < x) and (x < 5) - CORRECT

# Common mistake with or:
if x == 1 or 2:  # Evaluates as (x == 1) or 2 - always truthy if x!=1!
    pass

# Correct:
if x == 1 or x == 2:
    pass

# Even better:
if x in (1, 2):
    pass

2. Boolean Operators with Comparisons

# WRONG: Bitwise & has higher precedence than and
if x > 0 & x < 10:  # Evaluates as x > (0 & x) < 10
    pass

# CORRECT:
if x > 0 and x < 10:
    pass

if 0 < x < 10:  # Python supports chained comparisons
    pass

# Or with parentheses:
if (x > 0) & (x < 10):  # Explicit bitwise (but rare use case)
    pass

3. Lambda Functions

# WRONG:
f = lambda x: x + 1, lambda x: x * 2  # Creates a tuple of two lambdas!

# CORRECT:
f = lambda x: x + 1
g = lambda x: x * 2

# Or for multiple in a list:
funcs = [lambda x: x + 1, lambda x: x * 2]

4. Default Arguments in Lambdas

# WRONG: All lambdas capture the same variable
funcs = [lambda x: x + i for i in range(3)]
funcs[0](1)  # Returns 4, not 1!
funcs[1](1)  # Returns 4, not 2!

# CORRECT: Use default argument to capture value
funcs = [lambda x, i=i: x + i for i in range(3)]
funcs[0](1)  # Returns 1
funcs[1](1)  # Returns 2

# Alternative: Use closure with default argument
funcs = [(lambda i: lambda x: x + i)(i) for i in range(3)]

5. Ternary Operator Precedence

# WRONG:
x = 1 if True else 2 if False else 3  # Evaluates: 1 if (True) else (2 if (False) else 3)
# Result: 1

# CORRECT - be explicit with parentheses:
x = 1 if True else (2 if False else 3)
# Or use parentheses to clarify intent

Common Python Mistakes in 2026

1. Mutable Default Arguments

# WRONG: Default argument is shared across calls
def append_to(element, to=[]):
    to.append(element)
    return to

append_to(1)  # Returns [1]
append_to(2)  # Returns [1, 2] - NOT [2]!

# CORRECT: Use None as default
def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

2. Modifying List While Iterating

# WRONG:
numbers = [1, 2, 3, 4, 5]
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)  # Modifies during iteration!

# CORRECT: Iterate over a copy
numbers = [1, 2, 3, 4, 5]
for n in numbers[:]:  # Create a copy
    if n % 2 == 0:
        numbers.remove(n)

# Or use list comprehension
numbers = [n for n in numbers if n % 2 != 0]

3. Variable Scope in Closures

# WRONG: Late binding closure
functions = [lambda: print(i) for i in range(3)]
functions[0]()  # Prints 2, not 0!
functions[1]()  # Prints 2, not 1!

# CORRECT: Capture variable in default argument
functions = [lambda i=i: print(i) for i in range(3)]
functions[0]()  # Prints 0
functions[1]()  # Prints 1

4. Using is for Value Comparison

# WRONG: Using 'is' for values
a = 256
b = 256
a is b  # Might work for small integers (Python caches -5 to 256)

a = 1000
b = 1000
a is b  # False! Different objects

# CORRECT: Use == for value comparison
a = 1000
b = 1000
a == b  # True - compares values

5. Confusing __str__ and __repr__

class Person:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f"Person('{self.name}')"
    
    # def __str__ defaults to __repr__ if not defined

p = Person("John")

print(repr(p))  # Shows: Person('John')
print(str(p))   # Same as repr if __str__ not defined

# For user-friendly output, define __str__:
class Person:
    def __str__(self):
        return self.name  # Just the name
    
    def __repr__(self):
        return f"Person('{self.name}')"

print(str(p))   # Shows: John
print(repr(p))  # Shows: Person('John')

6. Forgetting self in Methods

# WRONG:
class Counter:
    count = 0
    
    def increment():
        count += 1  # Missing self!

# CORRECT:
class Counter:
    def __init__(self):
        self.count = 0
    
    def increment(self):
        self.count += 1

7. Import Confusion

# WRONG:
from module import *  # Pollutes namespace

# CORRECT:
import module  # or
from module import specific_function
from module import function1, function2

8. Dictionary Key Mistakes

# WRONG: Modifying dict during iteration
d = {'a': 1, 'b': 2}
for key in d:
    if key == 'a':
        d['c'] = 3  # RuntimeError: dictionary changed size

# CORRECT: Iterate over copy
d = {'a': 1, 'b': 2}
for key in list(d.keys()):  # or d.copy()
    if key == 'a':
        d['c'] = 3

9. Threading GIL Misunderstanding

# WRONG: Thinking threads provide parallelism for CPU-bound tasks
import threading
import time

def cpu_task():
    total = 0
    for i in range(10**7):
        total += i
    return total

# Threads won't help here due to GIL!

# CORRECT: Use multiprocessing
from multiprocessing import Pool

with Pool(4) as p:
    results = p.map(cpu_task, range(4))

10. Performance: String Concatenation in Loops

# WRONG: Inefficient string concatenation
result = ""
for s in strings:
    result += s  # Creates new string each iteration

# CORRECT: Use join
result = "".join(strings)

# Or for complex cases:
parts = []
for s in strings:
    parts.append(s)
result = "".join(parts)

Matrix Multiplication Best Practices

Always Use Parentheses

# Clear and explicit
result = (theta @ state_vector)[index]

# Or break it into multiple lines
result = theta @ state_vector
final_result = result[index]

Type Hints for Clarity

from typing import Callable

def tuple2index(action: int, obj: int) -> int:
    """Convert action and object indices to flat index"""
    return action * 10 + obj

# With numpy typing (numpy 1.20+)
import numpy as np
from numpy.typing import NDArray

def compute_q_value(
    theta: NDArray[np.float64],
    state_vector: NDArray[np.float64],
    action_index: int,
    object_index: int
) -> float:
    """Compute Q value with explicit parentheses"""
    result = theta @ state_vector
    return result[tuple2index(action_index, object_index)]

Debugging Tips

1. Use Parentheses to Verify

# When in doubt, add parentheses
result = (theta @ current_state_vector)[index]

2. Check with Simple Test

# Create minimal test case
theta = np.eye(2)  # Identity matrix
state_vector = np.array([1, 0])
action_index, object_index = 0, 0

# If result is theta[i,j] instead of expected, precedence is wrong

3. Use Ruff or Linters

# ruff will flag some precedence issues
# Install: pip install ruff
# Run: ruff check your_file.py

# Modern Python linting
pip install ruff
ruff check --select=E myfile.py

4. Use mypy for Type Checking

pip install mypy
mypy your_file.py

Common Precedence Table

Operator Description
() Parentheses
[] [:] Indexing/Slicing
** Exponentiation
+x -x ~x Unary
* / @ % // Multiplicative
+ - Additive
<< >> Bitwise shift
& Bitwise AND
^ Bitwise XOR
| Bitwise OR
== != < > etc. Comparisons
not Boolean NOT
and Boolean AND
or Boolean OR
:= Walrus operator

Modern Python 3.12+ Features

1. Better Error Messages

# Python 3.12+ gives better error messages
def example():
    if x = 5:  # Assignment expressions need parentheses
        pass

# Error: did you mean '=='?

2. Structural Pattern Matching

# Python 3.10+ match statement
def http_status(status):
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case _:
            return "Unknown"

3. Walrus Operator (3.8+)

# Use wisely - can cause precedence issues
if (n := len(data)) > 10:
    print(f"List has {n} elements")

# Precedence trap:
# a = b == c  # a = (b == c)
# a := b == c  # a = (b == c), parses differently than expected

Conclusion

Understanding operator precedence is essential for writing correct Python code. The @ matrix multiplication operator is particularly tricky because it doesn’t behave like mathematical notation. Always use parentheses to make your intent clear, especially when mixing operators.

Key takeaways:

  • Always use parentheses for complex expressions
  • Use linters (ruff, mypy) to catch issues early
  • Be careful with mutable default arguments
  • Never modify collections during iteration
  • Use == for value comparison, not is
  • Leverage type hints for better code understanding

Comments