To check whether a Python variable sits between two numbers, chain the comparison the way you’d write it on paper:
if 18 <= age <= 65:
print("working age")
That’s the whole answer. Python allows low <= x <= high directly, which most languages don’t, so you rarely need and.
Below I check the corners that actually bite: range(), floats, and arrays. Everything ran on Python 3.12.5.
Inclusive or exclusive between two numbers
Swap the operators to decide whether the edges count:
age = 34
# Python lets you chain the comparison, exactly like the maths
if 18 <= age <= 65:
print("working age")
temperature = 21.5
print("comfortable:", 18 <= temperature <= 24)
# the boundaries are yours to choose
score = 100
print("0 to 100 inclusive :", 0 <= score <= 100)
print("0 to 100 exclusive :", 0 < score < 100)
Output:
working age
comfortable: True
0 to 100 inclusive : True
0 to 100 exclusive : False
<= includes the boundary, < excludes it.Mixing them is fine too. 0 <= score < 100 reads as “zero or more, but under a hundred”, which is how most ranges in code actually behave.
Why chaining beats and for a between check
1 <= x <= 10 and 1 <= x and x <= 10 give the same answer, but they are not the same code.
The chained form evaluates the middle expression once. The spelled-out form evaluates it twice:
calls = []
def reading():
calls.append(1) # count how many times this runs
return 5
calls.clear()
chained = 1 <= reading() <= 10
print("chained :", chained, "| reading() ran", len(calls), "time(s)")
calls.clear()
spelled_out = 1 <= reading() and reading() <= 10
print("with and :", spelled_out, "| reading() ran", len(calls), "times")
Output:
chained : True | reading() ran 1 time(s)
with and : True | reading() ran 2 times
With a plain variable that costs nothing. With a function call, a database lookup or anything with a side effect, running it twice is a real bug waiting to happen.
That alone is a good reason to make chaining your habit.
Should you use range() for a between check?
x in range(low, high) looks readable and gets recommended a lot. It has three problems:
# range() looks tempting, and it has three problems
print("10 in range(1, 10) :", 10 in range(1, 10), " <- stop is excluded")
print("1 <= 10 <= 10 :", 1 <= 10 <= 10, " <- what you probably meant")
print("2.5 in range(1, 5) :", 2.5 in range(1, 5), " <- no error, just wrong")
print("1 <= 2.5 <= 5 :", 1 <= 2.5 <= 5)
import timeit
membership = timeit.timeit("500 in range(0, 1_000_000)", number=100_000) / 100_000 * 1e6
comparison = timeit.timeit("0 <= 500 < 1_000_000", number=100_000) / 100_000 * 1e6
print(f"in range(): {membership:.3f} us | chained compare: {comparison:.3f} us")
Output:
10 in range(1, 10) : False <- stop is excluded
1 <= 10 <= 10 : True <- what you probably meant
2.5 in range(1, 5) : False <- no error, just wrong
1 <= 2.5 <= 5 : True
in range(): 0.087 us | chained compare: 0.032 us
- The
stopvalue is excluded, so10 in range(1, 10)isFalse. - A float never matches.
2.5 in range(1, 5)returnsFalsewith no error at all. - It’s slower. Membership is a constant-time check in Python 3, not a scan, but a comparison is still quicker.
The float case is the dangerous one, because nothing tells you anything went wrong. Use range() for looping and comparisons for testing.
Float precision in a between check
This isn’t specific to range checks, but it shows up here constantly:
total = 0.1 + 0.2
print("0.1 + 0.2 =", total)
print("0.3 <= total <= 0.3 :", 0.3 <= total <= 0.3) # False, and correctly so
import math
print("math.isclose :", math.isclose(total, 0.3))
# for a range with float edges, give yourself a tolerance
tolerance = 1e-9
print("within tolerance :", 0.3 - tolerance <= total <= 0.3 + tolerance)
Output:
0.1 + 0.2 = 0.30000000000000004
0.3 <= total <= 0.3 : False
math.isclose : True
within tolerance : True
0.1 + 0.2 lands just above 0.3, so a strict test fails.When the boundaries are computed rather than typed, add a small tolerance or reach for math.isclose. The same care applies when you round to two decimal places for display.
A reusable check, and other types
Wrap it in a function when the same range is tested in several places:
def between(value, low, high, inclusive=True):
"""True when value falls inside the range."""
return low <= value <= high if inclusive else low < value < high
print(between(5, 1, 10))
print(between(10, 1, 10))
print(between(10, 1, 10, inclusive=False))
# chaining is not only for numbers
print("dates :", "2026-01-01" <= "2026-06-15" <= "2026-12-31")
import datetime as dt
today = dt.date(2026, 6, 15)
print("real dates:", dt.date(2026, 1, 1) <= today <= dt.date(2026, 12, 31))
# clamp instead of test, when you want the value pulled into range
print("clamped:", max(1, min(99, 150)))
Output:
True
True
False
dates : True
real dates: True
clamped: 99
Chained comparison isn’t limited to numbers. Dates, strings and anything else with an ordering works the same way, which makes date-range filters pleasantly short.
The last line is worth stealing: max(low, min(high, value)) clamps a value into range instead of testing it.
Checking a whole array or column
On a NumPy array or a pandas column you want one answer per element, and the syntax changes:
import numpy as np
import pandas as pd
readings = np.array([12.5, 19.0, 24.8, 31.2, 8.4])
# element-wise: `and` will not work here, use & or the NumPy helper
inside = (18 <= readings) & (readings <= 25)
print("mask :", inside)
print("kept :", readings[inside])
print("count:", inside.sum(), "of", readings.size)
series = pd.Series(readings)
print("pandas .between():", series.between(18, 25).tolist())
Output:
mask : [False True True False False]
kept : [19. 24.8]
count: 2 of 5
pandas .between(): [False, True, True, False, False]
& for NumPy, or .between() if you’re in pandas.Note the brackets around each comparison. & binds more tightly than <=, so leaving them out gives a confusing error.
And and simply doesn’t work on arrays, because it needs one true-or-false answer and an array has many:
import numpy as np
readings = np.array([12.5, 19.0, 24.8])
try:
print(18 <= readings and readings <= 25)
except ValueError as error:
print("ValueError:", error)
What NumPy says:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
That message is one of the most-searched NumPy errors there is. It means “use &“, and it shows up whenever array logic meets Python’s keywords.
More Python basics worth a read:
- List comprehension with if else
- Check if a variable is None
- Round numbers to 2 decimal places
- Find the maximum value in an array
- Create a 2D array with NumPy
Frequently asked questions
How do I check if a number is between two values in Python?
Chain the comparison: low <= x <= high. Python evaluates it as one expression, which is documented under comparisons in the language reference.
Can I write a < x < b in Python?
Yes. Chained comparison is built into the language, and it evaluates the middle expression only once.
Should I use range() to check a range?
No. range() excludes the stop value, silently returns False for floats, and is slower than a comparison.
How do I include the boundaries?
Use <= on both sides. Use < where you want the edge excluded, and you can mix the two.
Why does my float range check fail?
Because 0.1 + 0.2 is not exactly 0.3. Add a small tolerance to the bounds, or compare with math.isclose.
How do I check a range on a NumPy array or pandas column?
Use (low <= arr) & (arr <= high) with brackets, or series.between(low, high) in pandas.
Why do I get “truth value of an array is ambiguous”?
You used and on an array. Replace it with &, which combines element by element.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn