Which Python operators can chain, and does `x != y != z` mean all three differ?
answer
- Chaining is not just for `<`
- Ten operators share one precedence
- Only adjacent pairs are compared
- Membership and identity chain as well
- `1 != 2 != 1` evaluates to True
basics
~20 sAll ten comparison operators chain, including in, not in, is and is not. A chain compares only adjacent pairs, so x != y != z allows x == z - indeed 1 != 2 != 1 is True.
solid answer
~50 sChaining is a property of the comparison operator *class*, not of `<` specifically: `<`, `<=`, `>`, `>=`, `==`, `!=`, `is`, `is not`, `in` and `not in` all share one precedence level and all chain. Two consequences catch people out. First, chaining `!=` does not mean pairwise distinctness - `x != y != z` expands to `x != y and y != z`, so `1 != 2 != 1` is `True` even though the outer operands are equal. Second, you can chain *different* operators, including membership: `1 in [1, 2] == True` silently means `1 in [1, 2] and [1, 2] == True`, which is `False` - not the `True` most readers expect. Because comparison binds tighter than `not`, `not a is b` parses as `not (a is b)`; it gives the same answer as `a is not b`, but style guidance prefers the single `is not` operator.
code
python · 3 linesprint(1 in [1, 2] == True)
print(1 != 2 != 1)
print(0 < 1 in [1, 2])go deeper
Know that chaining is not limited to < and >, and that a chain only compares neighbouring pairs. If you take one thing away, take 1 != 2 != 1 being True.
Name the ten comparison operators that share a precedence level and chain, and work through 1 in [1, 2] == True step by step. Explain why is not is one token while not x is y is a negated comparison.
Spot these in review: an accidental chain mixing in or == with an ordering operator, and a != chain used as a distinctness assertion. Both survive tests because they return a plausible bool rather than raising.
Decide how much of this belongs in tooling rather than in reviewers' heads - a linter rule against mixed-family chains and against not x is y costs nothing and removes a class of quiet bug from the review agenda entirely.
## The operator set Python has ten comparison operators, and the grammar treats them as one interchangeable class: `<` `<=` `>` `>=` `==` `!=` `is` `is not` `in` `not in` They all share a single precedence level, they all bind tighter than the boolean operators `not`, `and` and `or`, and - the point of this question - **any of them may appear in a chain**. Chaining is not a special rule bolted onto `<`; it is how the grammar defines a comparison expression. A run of `operand op operand op operand` means the conjunction of the adjacent pairs, with each operand evaluated only once. ## Surprise one: `!=` does not mean "all different" Because a chain is just a conjunction of *adjacent* pairs, `x != y != z` expands to `x != y and y != z`. The pair `(x, z)` is never compared: ```python 1 != 2 != 1 # True - and 1 == 1 ``` Writing that chain to assert three distinct values is a real and quiet bug; the expression is `True` for exactly the input it was meant to reject. For genuine distinctness, compare the count of a set against the number of values, or spell out all three pairs. The same caveat technically applies to `==`. `a == b == c` implies `a == c` only if `__eq__` behaves transitively, which the language does not enforce - a type is free to define equality however it likes, and float NaN is the standard counterexample in the other direction, being unequal to itself. For ordinary types the chain reads as intended, but the guarantee comes from the types, not from the chaining. ## Surprise two: membership and identity chain too This is where chaining stops being a convenience and starts being a trap, because the operators involved do not look like they belong in a chain: ```python 1 in [1, 2] == True # False ``` A reader parses this as "is `1 in [1, 2]` equal to `True`?" and expects `True`. Python parses it as a two-link chain: `1 in [1, 2] and [1, 2] == True`. The first link is true, the second compares a list with a bool and is false, so the whole expression is `False`. Mixed chains such as `0 < 1 in [1, 2]` (meaning `0 < 1 and 1 in [1, 2]`) are legal in the same way. The practical rule: when a comparison operand is itself the *result* of a comparison, parenthesise it. `(1 in [1, 2]) == True` says what the author meant - though comparing to `True` at all is usually redundant, since `in` already yields a bool. ## `is not` and `not in` are single operators `is not` and `not in` are each one operator token, not a `not` applied to a separate expression. That matters for two reasons. There is no separate method behind the negated forms - a type that supports `in` gets `not in` automatically - and the chaining rules apply to them just as they do to `<`. Because comparison binds tighter than the boolean `not`, the expression `not a is b` parses as `not (a is b)` and produces the same value as `a is not b`. They are interchangeable in result but not in readability, and Python's style guidance is explicit that `is not` is preferred; the same holds for `not in` over `not x in y`. A reviewer seeing `not x is None` should ask for `x is not None`. ## Style guidance worth stating in review * Chains of homogeneous ordering operators are idiomatic and readable: `0 <= i < len(items)`, `start <= ts < end`. * Chains that **mix** operator families - especially one that pulls `in`, `is` or `==` into a chain with an ordering operator - are almost always accidental. Parenthesise or split them. * A chain of `!=` is a smell; state what you actually mean about distinctness. * Prefer the single-token `is not` and `not in` forms over negating the whole comparison. None of this is exotic knowledge that gates an offer, but it explains a class of quiet bug that survives review precisely because the code looks like it says something else.
- How would you correctly express that three values are all different from one another?Compare a set's size with the number of values - `len({x, y, z}) == 3` - when the values are hashable, or spell out the three pairs explicitly with `and`. Both say what a chain of `!=` only appears to say. The set form additionally documents the intent, though it relies on `__hash__` and `__eq__` agreeing.
- Why does `1 in [1, 2] == True` evaluate to False?It is a chain, not a comparison of a membership result. Python reads it as `1 in [1, 2] and [1, 2] == True`; the first link is true, but the second compares a list with a bool and is false, so the expression is `False`. Parenthesising the membership test - or dropping the redundant `== True` - fixes it.
- Is `not x is None` different from `x is not None`?They always produce the same value. Comparison binds tighter than the boolean `not`, so `not x is None` parses as `not (x is None)`, while `is not` is a single operator token. The difference is readability, and Python's style guidance explicitly prefers `x is not None`, just as it prefers `x not in y` over `not x in y`.
saying these in an interview costs you the question
- Thinks only `<` and `>` can be chained
- Reads `x != y != z` as all three being different
- Expects `1 in [1, 2] == True` to evaluate to True
- Believes `not in` needs its own dunder method
- Says mixing `in` with `==` in a chain is a SyntaxError
- Claims `a == b == c` is guaranteed to imply `a == c`