Why this matters
A range check asks whether a number sits between two limits. A menu accepts a choice from 1 to 5. A website accepts a port number from 1 to 65535. A form accepts an age from 13 to 120. A clock program accepts an hour from 0 to 23.
In mathematics, people write this as 1 <= x <= 10, which reads as "x is at least 1 and at most 10". Python accepts that line exactly as written. Some other languages, such as Java, reject it, so a programmer who moves between languages can get an error from the same line.
This lesson shows both forms in Python, the edge values that cause off-by-one bugs, and the mistakes that are specific to Python. An off-by-one bug is a mistake where a limit is included when it should not be, or left out when it should be included.
Three terms are used below. A comparison tests two values against each other, such as x >= 1. The comparison operators are <, <=, >, >=, == and !=. A boolean is a value that is either True or False. The keyword and means "both must be true": the result is True only when both sides are True.
Rewriting the "outside the range" test with not is covered in the De Morgan rewrites lesson. This lesson stays with the inside test.
Start with one comparison
Begin with the smallest check: one comparison in an if.
Output:
old enough
doneThe part age >= 13 is a comparison. Its result is a boolean. The if runs the indented block below it only when that result is True. Python uses indentation, the spaces at the start of a line, to mark which lines belong to the block. Java uses braces for the same job.
Now suppose the program needs a teenager, which means an age from 13 to 19. The age must pass two tests at the same time: it must be at least 13, and it must be at most 19. Python can express this in two ways, and both are shown below.
The chained form
Python lets you write both tests in one line:
Output:
in rangeThis is a chained comparison: two comparison operators in a row, with a value in the middle that is shared by both. 1 <= x <= 10 means the same as 1 <= x and x <= 10. The next program shows one difference between the two. It uses a function that prints when it is called, so you can see how many times the middle value is calculated.
Output:
evaluated 5
True
TrueThe function show printed once. Python calculated the middle value one time and used it for both comparisons. The second line has no function call, so it prints nothing extra. The difference matters when the middle value is expensive to calculate, or when it does something visible such as printing or reading a file.
Use the chained form when the middle value is a plain variable or a short expression. Use the two-comparison form when the code needs to read the same way in any language, or when the two tests have different reasons and should be explained separately.
Two comparisons joined with and
The form with two comparisons is x >= 1 and x <= 10. Read it as "x is at least 1, and x is at most 10". Each side is a complete comparison that gives a boolean. The and gives True only when both are True.
The test is correct at both ends of the range. The value 1 passes the first test (1 is at least 1) and the second test (1 is at most 10). The value 10 passes both too. The value 0 fails the first test. The value 11 fails the second.
Example 1: a function for the check
A range check is used in many places, so it is useful to write it once in a function. The function below takes the number and the two limits.
Output:
0 in 1..10? False
1 in 1..10? True
5 in 1..10? True
10 in 1..10? True
11 in 1..10? False
-3 in 1..10? FalseThe keyword def starts a function, and return gives back its answer. The names low and high say which limit comes first, which matters because the order of the two limits changes the answer (see the mistakes section below). The for loop visits each item of the list [0, 1, 5, 10, 11, -3]. A list is an ordered collection written in square brackets. The text f"..." is an f-string: the parts inside { and } are replaced by the value of the expression inside them.
Python checks the left side of and first. If that side is False, Python does not evaluate the right side, because the answer is already known. This is called short-circuit evaluation. It matters when the right side could fail. The next program shows it.
Example 2: the bounds check goes first
Output:
skipped, no crashThe index -1 is not a valid position for this list. Python never reads items[i], because i >= 0 is False and that settles the and. If the two tests were in the other order, the program would read the list at a position that is not valid for the check. Put the bounds check first.
Python has one more rule that makes this check necessary. A negative index counts from the end of the list, so -1 does not cause an error. It gives the last item.
Output:
items[-1] is 30Python returns 30 without any warning. A range check that allows -1 therefore reads a value silently. The check 0 <= i < len(items) in the next example rules out negative indexes.
Example 3: an index range stops before the length
A list of three items has the indexes 0, 1 and 2. The first valid index is 0, and the last valid index is 2, which is one less than the number of items. So the valid range is "0 or more, and less than the length". This is called a half-open range, because it includes the low end and leaves out the high end.
0 <= i < len(items)The function len(items) gives the number of items. Here it is 3. The < (not <=) leaves out the 3.
The next program uses the wrong form, i <= len(items), in a loop. The loop runs for i from 0 to 3, because range(len(items) + 1) gives 0, 1, 2 and 3, and the check lets 3 through.
items = [10, 20, 30]
for i in range(len(items) + 1):
if 0 <= i <= len(items):
print(f"items[{i}] = {items[i]}")Output, with the file path shortened to the file name:
items[0] = 10
items[1] = 20
items[2] = 30
Traceback (most recent call last):
File "main.py", line 4, in <module>
print(f"items[{i}] = {items[i]}")
~~~~~^^^
IndexError: list index out of rangeThe first three lines are correct. When i is 3, the check i <= len(items) is True, so Python tries to read items[3]. That position does not exist, and Python stops the program with IndexError. The message does not say which index was used or how long the list is, so read the line number in the traceback and look at the code on that line.
The correct form uses <:
Output:
items[0] = 10
items[1] = 20
items[2] = 30The loop still runs to 3, but the check skips it. Python programs usually avoid this check altogether. The function range(stop) gives the numbers from 0 up to, but not including, stop. So range(len(items)) gives exactly the valid indexes:
Output:
items[0] = 10
items[1] = 20
items[2] = 30
[0, 1, 2]The second line shows the numbers that range gives. The range function uses the same half-open rule as the < in the check.
Example 4: characters and decimal numbers
Python has no separate type for a single character. A character is a string, which is text in quotes, of length 1. Strings compare one character at a time, using each character's code number. The code of "a" is 97, the code of "z" is 122, and the code of "M" is 77. A chained comparison such as "a" <= c <= "z" therefore checks whether c is a lowercase letter from a to z.
Output:
a is lower? True
M is lower? False
z is lower? True
5 is lower? FalseThe check works only when each value has one character. A longer string is compared as a whole, so "apple" passes the test because it starts with a:
Output:
TruePython also has the method islower(), which answers the question directly for a single character:
Output:
True False True FalseDecimal numbers need one more idea. The float type can hold a special value called nan, which stands for "not a number". The math module gives it as math.nan. Python produces it from some calculations, such as float("inf") - float("inf"). Every comparison with nan gives False, including nan == nan. This is useful for range checks, because a nan is never reported as inside a range.
Output:
NaN in 0.0..1.0? False
nan < 0 or nan > 1: False
NaN outside, as not inside: TrueThe first line is the inside test, and it is False, which is the right answer. The second and third lines show a trap. Writing "outside" as nan < 0.0 or nan > 1.0 gives False, so nan is not reported as outside either. Writing it as the negation of the inside test gives True. The two forms of "outside" disagree on nan. When a nan can reach the check, choose the form deliberately and say why in a comment. The function math.isnan(value) tests for nan directly.
Common mistakes
Mistake 1: a second comparison without its left side
The chained form 1 <= x <= 10 is valid. A common error is to write the second test with the variable missing, as x >= 1 and <= 10. Python stops before it runs anything, so the error appears when the program starts.
x = 5
if x >= 1 and <= 10:
print("in range")Output, with the file path shortened to the file name:
File "main.py", line 2
if x >= 1 and <= 10:
^^
SyntaxError: invalid syntaxThe message SyntaxError means the text is not valid Python. The caret (the ^ under the code) points at the <= that has nothing on its left. The fix is x >= 1 and x <= 10, or the chained form 1 <= x <= 10.
Mistake 2: or where and is needed
A range needs both tests to pass, so the keyword is and. Using or (the "either one" operator) gives a test that is always True. The next program uses x >= 1 or x <= 10.
Output:
0: True
1: True
5: True
10: True
11: True
50: TrueEvery number passes. Any whole number is either at least 1 or at most 10, because if it is not at least 1, it is less than 1 and therefore at most 10. The test cannot fail.
Mistake 3: an exclusive limit where the range is inclusive
If the problem says "1 to 10 inclusive", the upper limit must be <=. Writing < leaves the limit out. The next program uses x >= 1 and x < 10.
Output:
0: False
1: True
5: True
10: False
11: False
50: FalseThe value 10 is wrongly rejected. Use < for the upper limit only when the range is half-open, such as the valid indexes of a list.
Mistake 4: the limits in the wrong order
The function in_range(x, low, high) needs low to be the smaller limit. Passing them the other way round gives False for every value.
Output:
False
TrueThe call in_range(5, 10, 1) asks for a number that is at least 10 and at most 1 at the same time. No number can do that. Python does not warn about this, because the code is valid. Check the order of the limits when you write the call.
Mistake 5: a value that is text, not a number
Input from a user is text until it is converted. A chained comparison between a number and text raises an error:
x = 5
print(1 <= x <= "10")Output, with the file path shortened to the file name:
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(1 <= x <= "10")
^^^^^^^^^^^^^^
TypeError: '<=' not supported between instances of 'int' and 'str'The type int is a whole number, and str is text. The value "10" is in quotes, so it is text. Python refuses to compare the two. Convert the text first with int(...). For example, int(input("Enter a number: ")) reads a line typed by the user and gives back a whole number.
Mistake 6: "outside" written with the wrong pieces
People often write "outside the range" by flipping the operators inside the same expression. The next program compares four forms. The first two are correct, the third is always True, and the fourth is always False.
Output:
0: a=True b=True c=True d=False
1: a=False b=False c=True d=False
5: a=False b=False c=True d=False
10: a=False b=False c=True d=False
11: a=True b=True c=True d=False
50: a=True b=True c=True d=FalseThe forms a and b agree on every value and are both correct. The form c is the or mistake from above. The form d is always False, because it would need the number to be below 1 and above 10 at the same time.
Practice
Write each answer in a file called main.py and run it with python3 main.py before you check the answer.
- Write a condition that is
Truewhenhouris a valid clock hour from 0 to 23, andFalseotherwise.
Show the answer
0 <= hour <= 23The chained form and hour >= 0 and hour <= 23 give the same answer for every value.
- Given
n, write a condition that isTruewhennis from 1 to 100 inclusive, except for 50.
Show the answer
1 <= n <= 100 and n != 50With n set to 50, the first two parts are True but n != 50 is False, so the whole condition is False. With n set to 49, all three parts are True.
- Which of these tests are correct for "x is outside the range 1 to 10"?
- a)
not (x >= 1 and x <= 10) - b)
x < 1 or x > 10 - c)
x >= 1 or x <= 10 - d)
not (x >= 1) and not (x <= 10)
Show the answer
a and b. Test c is always True, and test d is always False. The output of Mistake 6 shows this for six values.
- Write a condition that is
Truewhen the stringcis a single letter, either fromAtoZor fromatoz.
Show the answer
("A" <= c <= "Z") or ("a" <= c <= "z")Python does not need the parentheses here, because and and or are read after comparisons. The parentheses make the two groups easy to see. A test for a single character also needs len(c) == 1 if the value could be longer.
- A program calls
in_range(x, 0, n)wherenis the length of a list. Explain why the result forx = 3and a list of length 3 isTruewhen this is used as an index check, and why that is wrong for a list index. Then write the correct index check.
Show the answer
The call returns True, because 3 is at least 0 and at most 3. But 3 is not a valid index for a list of length 3, since the valid indexes are 0, 1 and 2. The <= includes the length, which is one past the last index. The correct check is 0 <= i < len(items), which is the half-open form from Example 3.
Summary
- Python reads
1 <= x <= 10as one chained comparison, which means the same as1 <= x and x <= 10, and it calculates the middle value once. - Write a range check as
low <= x <= highor asx >= low and x <= high. - Use
<for the upper limit only when the range is half-open, such as the valid indexes of a list. - A negative index such as
items[-1]does not cause an error in Python, so the lower bound check must be written. - A string is compared as a whole, so a single-character check needs a length check or
islower(). - A
nanvalue fails every comparison, so the inside test isFalsefor it, and the two forms of "outside" give different answers.