Learn the ins and outs of comparison operator chaining, and especially the cases you should avoid.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
In this Pydon't we will go over the chaining of comparison operators:
One of the things I enjoy about Python is that some of its features make so much sense that you don't even notice that you are using a feature until someone points out that such code wouldn't work in other languages. One such example is comparison chaining! Look at this snippet of code and tell me if it doesn't look natural:
>>> a = 1 >>> b = 2 >>> c = 3 >>> if a < b < c: ... print("Increasing seq.") ... Increasing seq.
When Python sees two comparison operators in a row, like in
a < b < c,
it behaves as if you had written something like
a < b and b < c,
b only gets evaluated once (which is relevant if
b is an
expression like a function call).
In my opinion, this features makes a lot of sense and does not look surprising. Instead, now I feel kind of sad that most languages do not have support for this behaviour.
Another example usage is for when you want to make sure that three values are all the same:
>>> a = b = 1 >>> c = 2 >>> if a == b == c: ... print("all same") ... else: ... print("some are diff") ... some are diff >>> c = 1 >>> if a == b == c: ... print("all same") ... else: ... print("some are diff") ... all same
Did you know that you can actually chain an arbitrary number of comparison operators?
a == b == c == d == e checks if all five variables are the same, while
a < b < c < d < e checks if you have a strictly increasing sequence.
Even though this feature looks very sensible, there are a couple of pitfalls you have to look out for.
We saw above that we can use
a == b == c to check if
c are all the same.
How would you check if they are all different?
If you thought about
a != b != c, then you just fell into the first pitfall!
Look at this code:
>>> a = c = 1 >>> b = 2 >>> if a != b != c: ... print("a, b, and c all different:", a, b, c) a, b, and c all different: 1 2 1
The problem here is that
a != b != c is
a != b and b != c,
which checks that
b is different from
a and from
c, but says nothing about
From the mathematical point of view,
!= isn't transitive, i.e.,
a relates to
b and knowing how
b relates to
tell you how
a relates to
As for a transitive example, you can take the
== equality operator.
a == b and
b == c then
it is also true that
a == c.
Recall that in a chaining of comparisons, like
a < b < c, the expression
b in the middle is only
evaluated once, whereas if you were to write the expanded expression,
a < b and b < c,
b would get evaluated twice.
b contains an expression with side-effects or if it is something that isn't constant,
then the two expressions are not equivalent and you should think about what you are doing.
This snippet shows the difference in number of evaluations of the expression in the middle:
>>> def f(): ... print("hey") ... return 3 ... >>> if 1 < f() < 5: ... print("done") ... hey done >>> if 1 < f() and f() < 5: ... print("done") ... hey hey done
This snippet shows that an expression like
1 < f() < 0 can actually evaluate to
when it is unfolded:
>>> l = [-2, 2] >>> def f(): ... global l ... l = l[::-1] ... return l >>> if 1 < f() and f() < 0: ... print("ehh") ... ehh
l[::-1] is a “slice” that reverses a list.
I'll be writing about list slicing soon, so stay tuned for that!
Of course that
1 < f() < 0 should never be
True, so this just shows that
the chained comparison and the unfolded one aren't always equivalent.
This feature looks really natural, but some particular cases aren't so great. This is a fairly subjective matter, but I personally don't love chains where the operators aren't "aligned", so chains like
a == b == c
a < b <= c
a <= b < c
look really good, but in my opinion chains like
a < b > c
a <= b > c
a < b >= c
don't look that good.
One can argue, for example, that
a < b > c reads nicely as
b is larger than both
c”, but you could also write
max(a, c) < b or
b > max(a, c).
Now there's some other chains that are just confusing:
a < b is True
a == b in l
a in l is True
not in are comparison operators, so you can
also chain them with the other operators.
This creates weird situations like
>>> a = 3 >>> l = [3, 5] >>> if a in l == True: ... print("Yeah :D") ... else: ... print("Hun!?") ... Hun!?
Here is a breakdown of what is happening in the previous example:
a in l == Trueis equivalent to
a in l and l == True;
a in lis
l == Trueis
a in l == Trueunfolds to
True and Falsewhich is
The one who wrote
a in l == True
(a in l) == True, but that is also the same as
a in l.
Having a simple utility function that ensures that a given value is between two bounds becomes really simple, e.g.
def ensure_within(value, bounds): return bounds <= value <= bounds
or if you want to be a little bit more explicit, while
bounds is a vector with exactly two items
(take a look at the Pydon't about deep unpacking),
you can also write
def ensure_within(value, bounds): m, M = bounds return m <= value <= M
Straight from Python's
module, we can find a helper function
(that is not exposed to the user), that reads as follows:
def _is_dunder(name): """Returns True if a __dunder__ name, False otherwise.""" return (len(name) > 4 and name[:2] == name[-2:] == '__' and name != '_' and name[-3] != '_')
This function checks if a string is from a “dunder” method or not.
“Dunder” comes from “double underscore” and just refers to some Python
methods that some classes have, and that allow them to interact
nicely with many of Python's built-in features.
These methods are called “dunder” because their names start and end with
You have seen the
__repr__ dunder methods
in the “str and repr” Pydon't and the
dunder method in the “Truthy, falsy, and bool” Pydon't.
I will be writing about dunder methods in general in a later Pydon't,
so feel free to subscribe to stay tuned.
The first thing the code does is check if the beginning and the ending
of the string are the same and equal to
>>> _is_dunder("__str__") True >>> _is_dunder("__bool__") True >>> _is_dunder("_dnd__") False >>> _is_dunder("_______underscores__") False
Here's the main takeaway of this article, for you, on a silver platter:
“Chaining comparison operators feels so natural, you don't even notice it is a feature. However, some chains might throw you off if you overlook them.”
This Pydon't showed you that:
incan look really misleading.
If you liked this Pydon't be sure to leave a reaction below and share this with your friends and fellow Pythonistas.
Also, don't forget to subscribe to the newsletter so you don't miss a single Pydon't!
Online references last consulted on the 1st of March of 2021.
I hope you learned something new! If you did, consider following the footsteps of the readers who bought me a slice of pizza 🍕. Your small contribution helps me produce this content for free and without spamming you with annoying ads.