Python Comparison Chaining: Syntax, Behavior, and Pitfalls
python comparison chaining: Learn how Python comparison chaining works, how it evaluates expressions like a < b < c, and when to use it for readable, correct code.
Python comparison chaining lets you combine multiple comparison operators into a single expression, such as 0 < x < 10. The expression is evaluated as 0 < x and x < 10, but with one important difference: the middle operand (x in this case) is evaluated only once. This behavior makes chained comparisons both more readable and more efficient than the equivalent and expression when the middle operand is a function call or a property access with side effects.
What Python Comparison Chaining Is
In Python, you can chain comparison operators without needing to repeat the operand. For example:
def is_between(value, lower, upper): return lower <= value <= upper
This reads naturally: lower <= value <= upper. The expression is equivalent to lower <= value and value <= upper, but Python's grammar treats the chained form as a single comparison expression. This syntax works with any of the comparison operators: <, <=, >, >=, ==, !=, is, is not, in, and not in.
How Python Evaluates Chained Comparisons
When Python encounters a < b < c, it evaluates the expression as a < b and b < c. The evaluation is left-to-right, and short-circuiting applies: if a < b is false, Python does not evaluate b < c at all. This is the same short-circuit behavior you get with the explicit and operator.
The critical difference is that the middle operand b is evaluated exactly once. In the explicit and form, b appears twice and is evaluated twice. For simple variables this makes no difference, but for function calls or property lookups it can affect both performance and correctness.
def get_value(): print("get_value called") return 5 if 0 < get_value() < 10: print("Value is between 0 and 10")
Here, get_value() is called once. If you wrote 0 < get_value() and get_value() < 10, the function would be called twice, which could be a problem if the function has side effects or is expensive.
Practical Use Cases for Chained Comparisons
Chained comparisons are most useful for range checks and boundary validation. A common pattern is checking whether a value falls within a specific interval:
def validate_age(age): if 0 < age <= 150: print("Valid age") else: print("Invalid age")
You can also chain different operators, though the expression must remain readable. For example, checking that a string contains a substring and is not empty:
if "python" in doc and doc != "": # ...
But this is better written with explicit and because the two conditions are independent. Chaining is most natural when the operators share a common middle operand.
The Single-Evaluation Guarantee
The single-evaluation behavior is a deliberate design decision in Python. It means that in a < b < c, the expression b is evaluated only once, even though it appears twice in the logical expansion. This is particularly valuable when b is a complex expression:
def get_balance(): # Simulate a database call return fetch_balance() if 100 <= get_balance() <= 500: print("Balance within range")
If you used and, get_balance() would be called twice, potentially returning different values if the underlying data changes between calls. Chaining avoids that inconsistency.
Chaining with Other Comparison Operators
Chaining works with all comparison operators, but you need to be careful with is and in because the semantics can be surprising. For example:
if a is b is c: # Equivalent to a is b and b is c
This is valid, but it can be confusing because is checks identity, not equality. Similarly, x in y in z is equivalent to x in y and y in z, but the meaning of in depends on the types of y and z. Overusing chained in or is can hurt readability. Keep chains short and stick to the numeric comparison operators for the clearest code.
Readability and Maintainability Tradeoffs
While chaining reduces repetition, it can also reduce readability when the chain becomes long. A chain like a < b < c < d < e is hard to parse at a glance. In such cases, an explicit and expression with line breaks may be clearer:
if a < b and b < c and c < d and d < e: # ...
Chaining is also less flexible if you need to reuse the middle operand later. For example, if you need to check that b is within a range and also use b in a subsequent condition, you should assign it to a variable first.
Common Mistakes and Misconceptions
A common mistake is assuming that a < b < c is parsed as (a < b) < c. In Python, that is not the case. The chained form is a special syntax that expands to the and form. If you actually want to compare the boolean result of a < b with c, you need parentheses:
if (a < b) < c: # This is not the same as a < b < c
This is rarely what you want, but it's a valid expression because booleans are integers in Python. Another misconception is that chaining always evaluates the middle operand once, which is true, but if the middle operand is a complex expression that itself has side effects, those side effects occur only once. This is usually desirable, but you should be aware of it when debugging.
When to Prefer Explicit and Over Chaining
Chaining is not always the best choice. Prefer explicit and when:
- You need to reuse the middle value in a later condition.
- The chain has more than three comparisons, making it hard to read.
- The middle operand is a function call that you want to call only once but also need to store its result.
- You are working with custom classes where the comparison operators have unusual behavior.
In those cases, assigning the middle value to a variable and using and is clearer:
value = get_value() if 0 < value and value < 10: # ...
This also avoids the single-evaluation guarantee becoming a hidden dependency.