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.
