Operator Precedence & Evaluation

When an expression mixes several operators, precedence and associativity decide which parts bind first — and a few Python-specific rules (chained comparisons, short-circuit values) trip up even experienced programmers.

Python expressions like a + b * c ** 2 are not evaluated left to right — each operator has a precedence (how tightly it binds) and an associativity (which direction ties are broken). Understanding this lets you predict what an expression does without running it, and know exactly when parentheses are required versus merely helpful for readability.

What Precedence and Associativity Mean

Precedence ranks operators: higher-precedence operators group with their operands before lower-precedence ones do. ** (exponentiation) binds tighter than *, which binds tighter than +. So 2 + 3 * 4 is 2 + (3 * 4), not (2 + 3) * 4.

Associativity resolves ties between operators of the *same* precedence appearing in a row. Most binary operators are left-associative (evaluated left to right): 10 - 3 - 2 is (10 - 3) - 2 = 5. ` is the notable exception — it is right-associative: 2 3 2 is 2 (3 2) = 512, not (2 3) ** 2 = 64`.

print(2 + 3 * 4)      # 14  -> 2 + (3 * 4)
print(10 - 3 - 2)     # 5   -> (10 - 3) - 2  (left-associative)
print(2 ** 3 ** 2)    # 512 -> 2 ** (3 ** 2)  (right-associative)

The Precedence Table (High to Low, Practical Subset)

The full table in the language reference has dozens of entries; here is the practical ordering you'll use daily, from tightest-binding to loosest:

Unary minus binds tighter than most binary operators but *looser* than ` on its right operand: -2 2 is -(2 2) = -4, not (-2) 2 = 4`. This surprises many newcomers — use parentheses when it matters.

1. ()                      grouping (parentheses)
2. ** (right-assoc.)       exponentiation
3. unary +x, -x, ~x        unary plus/minus/invert
4. *  /  //  %             multiplication, division, floor div, modulo
5. +  -                    addition, subtraction (binary)
6. <<  >>                  bitwise shifts
7. &                       bitwise AND
8. ^                       bitwise XOR
9. |                       bitwise OR
10. ==  !=  <  <=  >  >=   comparisons
    is  is not  in  not in
11. not x                  boolean NOT
12. and                    boolean AND
13. or                     boolean OR
14. x if cond else y       conditional expression
15. lambda                 lambda expression
16. =  :=  (assignment)    (walrus is an expression form; = is a statement)
print(-2 ** 2)     # -4
print((-2) ** 2)   # 4

Parentheses: Overriding and Clarifying

Parentheses () have the highest precedence of all and force a subexpression to be evaluated as a unit before anything around it. They are the only fully reliable way to override the default grouping.

Using parentheses purely for readability — even when the default precedence already does what you want — is good practice, not redundant. A reader should not have to consult a precedence table to check your logic.

print((2 + 3) * 4)      # 20 -- forces addition first
print(2 + 3 * 4)        # 14 -- default precedence

# Parentheses also clarify intent even when not strictly required
discount = (price * rate) / 100   # clearer than price * rate / 100

Comparison and Membership Operators

All comparison operators (==, !=, <, <=, >, >=), identity tests (is, is not), and membership tests (in, not in) share one precedence level, below arithmetic and bitwise operators but above the boolean operators not/and/or. This means x + 1 > y - 1 is parsed as (x + 1) > (y - 1) without any parentheses needed.

x, y = 5, 3
print(x + 1 > y - 1)     # True  -> (x+1) > (y-1) -> 6 > 2
print(3 in [1, 2, 3] and 4 not in [1, 2, 3])  # True

Chained Comparisons

Python allows chaining comparison operators: a < b < c is legal and means what a mathematician would expect — a < b AND b < c — but each shared operand (b here) is evaluated only once, not twice. This is a Python LANGUAGE feature, distinct from most C-family languages where a < b < c would evaluate left to right as (a < b) < c, comparing a boolean to c.

Chained comparisons short-circuit: in a < f() < b, if a < f() is False, f() is still called exactly once (its value is needed), but the second comparison is never evaluated once the whole chain is already known False. Each operand between two operators is evaluated at most once.

print(1 < 2 < 3)          # True  -> 1 < 2 and 2 < 3
print(1 < 3 < 2)          # False -> 1 < 3 (True) and 3 < 2 (False)
print(0 <= 5 < 10)        # True  -- classic 'in range' idiom

# Any number of comparisons can be chained:
a = b = c = 5
print(a == b == c)        # True

Short-Circuit Evaluation of and / or

and and or are short-circuiting: they evaluate operands left to right and stop as soon as the overall result is determined. and stops at the first falsy operand (no need to check further — the result is already falsy); or stops at the first truthy operand.

Short-circuiting is not just an optimization -- it is relied upon for correctness, most commonly to guard against errors:

def loud(label, value):
    print(f'evaluating {label}')
    return value

result = loud('A', False) and loud('B', True)
# prints only 'evaluating A' -- B is never called

result2 = loud('C', True) or loud('D', True)
# prints only 'evaluating C' -- D is never called
items = []
if items and items[0] == 'first':   # safe: items[0] only checked if items is non-empty
    print('found it')

and / or Return Operand Values, Not Just True/False

Unlike many languages where &&/|| always produce a boolean, Python's and and or return whichever *operand* determined the result — not a coerced True/False. This is a deliberate language design choice, useful for defaulting and short idiomatic expressions.

This idiom breaks when a legitimate value is falsy but meaningful, e.g. count = user_count or 10 silently replaces 0 with 10. Prefer an explicit None check (count = 10 if user_count is None else user_count) when zero, empty string, or empty list are valid inputs.

print(3 and 5)        # 5   -- 3 is truthy, so result is the second operand
print(0 and 5)        # 0   -- 0 is falsy, short-circuits, returns 0
print('' or 'default') # 'default' -- '' is falsy, moves to next operand
print('hi' or 'default') # 'hi' -- first operand truthy, returned as-is

# Common idiom: fallback default
name = user_input or 'Anonymous'

Conditional Expressions in the Precedence Chain

The conditional expression x if condition else y (sometimes called the ternary expression) sits near the bottom of the precedence table, above only lambda and assignment. This means arithmetic, comparisons, and boolean operators inside each of the three parts are evaluated fully before the conditional structure combines them.

A conditional expression is not the same as an if/elif/else statement — it is an expression that produces a value and can appear anywhere an expression can (inside a function call, a list, an f-string).

n = 7
label = 'even' if n % 2 == 0 else 'odd'
print(label)   # 'odd'

# Chaining conditional expressions (use sparingly -- readability suffers fast)
score = 85
grade = 'A' if score >= 90 else 'B' if score >= 80 else 'C'
print(grade)   # 'B'

Mixing Boolean Operators: not, and, or Together

not binds tighter than and, which binds tighter than or. So not a and b or c parses as ((not a) and b) or c. When mixing all three, add parentheses even where not strictly required — the cognitive cost of re-deriving precedence is rarely worth saving two characters.

a, b, c = True, False, True
print(not a and b or c)       # ((not a) and b) or c -> (False and False) or True -> True
print(not (a and b) or c)     # explicit grouping -- easier to audit

Best Practices: When to Rely on Precedence vs. Parenthesize

Rely on precedence for universally memorized rules: *// before +/-, and comparisons before and/or. Parenthesize everything else, especially: mixing ** with unary minus, mixing bitwise operators (&, |, ^) with comparisons (their precedence relative to comparisons surprises almost everyone), and any expression a reviewer would need more than two seconds to parse.

In Python, comparison operators (==) bind *looser* than bitwise &... but the safe habit is the same as in C: always parenthesize bitwise-and-comparison combinations, because the exact ranking is easy to misremember and differs subtly from other languages you may know.

# Classic bitwise/comparison trap: & binds TIGHTER than == in C, but LOOSER in Python
flags = 0b0110
mask = 0b0010
# print(flags & mask == mask)   # (flags & (mask == mask)) -- WRONG, mask==mask is True(1)
print(flags & mask == mask)     # actually: flags & (mask == mask) -> 0b0110 & True -> 0
print((flags & mask) == mask)   # correct intent: True

Key Takeaways

Precedence decides which operator binds first; associativity breaks ties between operators of equal precedence (** is right-associative; most others are left-associative).

Parentheses always win — use them to override default grouping and, just as importantly, to make intent obvious to readers.

Chained comparisons like a < b < c mean a < b and b < c, evaluating each shared operand once — a Python-specific convenience, not a left-to-right boolean chain.

and/or short-circuit and return the deciding operand's actual value, not a coerced boolean — powerful for defaulting, but risky with falsy-but-valid values like 0 or ''.

Conditional expressions (x if c else y) sit low in the precedence table; each branch is fully evaluated as its own expression first.

When in doubt — especially mixing bitwise operators with comparisons, or unary minus with ** — parenthesize rather than trust memory.

print(-2 2) # expecting 4 print((-2) 2) # 4, if that's the intended value

` binds tighter than unary minus on its left, so -2 2 is -(2 ** 2) = -4`. Parenthesize the base explicitly when you mean to square a negative number.

timeout = user_timeout or 30 # silently replaces a valid 0 with 30 timeout = 30 if user_timeout is None else user_timeout

or falls through on ANY falsy value, not just None. If 0 is a legitimate timeout, or-defaulting silently discards it; an explicit is None check is unambiguous.

if flags & READY == READY:... # relying on memorized C-style precedence if (flags & READY) == READY:...

Python's precedence for bitwise operators relative to comparisons does not match every language you may have learned it from. Parenthesizing removes the ambiguity entirely instead of relying on a rule that's easy to misremember.

result = a if b else c if d else e # deeply nested without grouping result = (a if b else (c if d else e)) # or, better, an if/elif chain as a statement

Nested conditional expressions parse right-to-left and become hard to audit quickly. Parenthesize nested ternaries, or switch to a full if/elif/else statement once logic gets non-trivial.

What does 2 + 3 * 4 ** 2 evaluate to, and in what order are the operators applied?

146. Order: 4 ** 2 = 16 first (highest precedence), then 3 * 16 = 48, then 2 + 48 = 50... wait recompute: 4**2=16, 3*16=48, 2+48=50. Answer: 50.

Why does 1 < 2 < 3 return True in Python but would not behave the same way as a naive left-to-right boolean chain in a C-style language?

Python treats it as a genuine chained comparison, evaluating to 1 < 2 and 2 < 3, both True. In C-style languages, 1 < 2 first evaluates to 1 (true), and then 1 < 3 is checked, which happens to also be true here but for a completely different, coincidental reason — the two languages are not doing the same operation.

What is the result of '' or 0 or 'fallback' and why?

'fallback'. or short-circuits on the first truthy value; '' and 0 are both falsy so evaluation continues, and 'fallback' (truthy) is returned as the deciding value.

Why can relying on and/or returning operand values (rather than a plain boolean) be risky in production code?

Because the returned value can be any object, not necessarily True/False, code that assumes a boolean result can behave unexpectedly, and defaulting patterns like x or default silently override legitimate falsy values like 0, '', or [].

Is ** left- or right-associative? Give an example showing why the distinction matters.

Right-associative. 2 3 2 evaluates as 2 (3 2) = 2 9 = 512, not (2 3) ** 2 = 64 — the two groupings give very different results.

When should you add parentheses even if the default precedence already produces the result you want?

Whenever a reader would need to consult a precedence table to verify your intent quickly — e.g. mixing bitwise and comparison operators, unary minus with exponentiation, or deeply nested conditional expressions. Clarity for the reader outweighs saving a couple of characters.

Predict the printed output and explain the evaluation order.

True. Precedence: and binds tighter than or, so it's (x < y and y < 20) or (x > 100). x < y is True, y < 20 is True, so the and is True; or short-circuits immediately without evaluating x > 100.

Write a function in_range(n, lo, hi) that returns True if n is between lo and hi inclusive, using a chained comparison.

def in_range(n: float, lo: float, hi: float) -> bool: return lo <= n <= hi print(in_range(5, 1, 10)) # True print(in_range(15, 1, 10)) # False

lo <= n <= hi is a chained comparison equivalent to lo <= n and n <= hi, evaluating n only once, which is both correct and idiomatic Python.

What does this print, and what is the likely bug here?

Prints 60. The bug: config_value was legitimately set to 0 (perhaps meaning 'no timeout'), but or treats 0 as falsy and silently substitutes 60. Fix with timeout = 60 if config_value is None else config_value.

Write an expression using a conditional expression (ternary) that assigns 'positive', 'negative', or 'zero' to a variable sign based on an integer n, without using an if/elif/else statement.

n = -7 sign = 'positive' if n > 0 else 'negative' if n < 0 else 'zero' print(sign) # 'negative'

Nested conditional expressions evaluate right-to-left in effect: if n > 0 is False, Python evaluates the second ternary 'negative' if n < 0 else 'zero'. This works but should be used sparingly — an if/elif/else statement is clearer once more than two branches are involved.

x = 5
y = 10
print(x < y and y < 20 or x > 100)
config_value = 0
timeout = config_value or 60
print(timeout)