Python Comparison Chaining: Syntax, Behavior, and Pitfalls
Learn how Python comparison chaining works, how expressions like a < b < c are evaluated, and when to prefer the chained form over explicit and.
Python comparison chaining lets you combine multiple comparison operators into a single expression, such as 0 < x < 10. The expression is equivalent to 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 chained form. In the explicit and form, b appears twice, so it is evaluated twice whenever the first comparison is true and the second comparison runs. 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")
Different comparison operators can also appear in a chain, though readability matters. A valid chain such as "python" in doc != "" means "python" in doc and doc != "", but many readers will find the explicit and version clearer:
if "python" in doc and doc != "": # ...
Chaining is most natural when the operators share a common middle operand, as in lower <= value <= upper.
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(): # Placeholder for a database-backed balance lookup. return 250 if 100 <= get_balance() <= 500: print("Balance within range")
If you used and, get_balance() would be called twice. In a real database-backed function, those two calls could return different values if the underlying data changes between them. 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 a subclass of int in Python. Also remember that the middle operand is evaluated only once in a chained comparison. If that middle operand is a complex expression with side effects, those side effects occur only once as well. This is usually desirable, but 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.