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, notis - Leverage type hints for better code understanding
Comments