Why this matters
Many jobs skip values. A program might print only even numbers, check every third item in a list, or count down a timer from 60 to 0. Each of these needs a loop that does not add 1 on each pass.
In Python, the built-in range function makes these loops. range(start, stop, step) produces the numbers from start up to, but not including, stop, moving by step each time. The step is the amount the loop variable changes after each pass. In range(0, 10, 2), the step is 2. In range(10, 0, -1), the step is -1. The step can also come from a variable, such as range(0, 24, step).
This lesson covers three things. You will write loops with a fixed step up, a fixed step down and a step stored in a variable. You will learn to predict where the loop variable stops, which is a common source of off-by-one mistakes. You will also see how to count the number of passes without running the loop.
The loop invariant idea from the previous lesson applies here too.
Words used in this lesson
A step is the amount the loop variable changes after each pass.
The stop value is the second argument of range. range never produces it. In range(0, 10, 3), the stop is 10, and 10 is never produced.
The end value is the value the loop variable holds when the loop stops. For a while loop, it is the first value that fails the test, and it can be past the last value the body used. For a for loop over range, the variable keeps the last value the body used, so the two are the same. The difference matters, and the rest of this lesson returns to it.
A range is the set of values a loop variable takes, from its start to its stop.
An off-by-one mistake is a loop that runs one time too many or one time too few.
Counting up by 2
Here is a loop that prints the even numbers from 0 to 8.
The end=" " argument tells print to put a space after each number instead of starting a new line. The output is:
step 2 up: 0 2 4 6 8
i after for loop = 8Read the header one part at a time. The start is 0, so the first value is 0. The stop is 10, which is not produced. The step is 2. So range(0, 10, 2) produces 0, 2, 4, 6 and 8. Each pass the loop variable moves to the next number in the range. There is no test and no update to write, because range does both.
Notice the value 10. It is never produced. After the loop, i is 8, not 10. A for loop over range never assigns the stop value to the variable. The stop value only marks where the range ends.
Counting down by -1
A countdown runs from a high number to a low number. The step is -1. The stop must be one below the lowest value you want, because range never produces the stop.
The output is:
step -1 down: 10 9 8 7 6 5 4 3 2 1 0
range(10, 0, -1): [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]The start is 10. The stop is -1. Each pass goes one lower, and the value 0 is produced, because 0 is above the stop -1. The list range(10, 0, -1) leaves out 0, because the stop is 0 and range does not produce it. This is easy to do by mistake in Python, and it gives no error. The list function turns a range into a list, so you can see the values.
If the step has the wrong sign, range gives an empty sequence and the body never runs. The common mistakes section shows that case.
A step that does not land on the end
The step does not have to divide the range evenly. Here the loop starts at 1 and adds 3 each time, with a stop of 21. The step never lands on 20, so 19 is the largest value produced.
The output is:
step 3 up: 1 4 7 10 13 16 19
[1, 4, 7, 10, 13, 16, 19]The values are 1, 4, 7, 10, 13, 16 and 19. The next value would be 22, which is past the stop 21, so range ends. The loop never prints 20, because the step of 3 never lands on it.
The next program makes the difference between the last value used and the value after the loop easy to see. It uses a step of 3 with a stop of 10. It uses a while loop, because a while loop shows the value the variable holds after the last test fails.
The output is:
0 3 6 9
last value used = 9, i after loop = 12The body used 9 as its last value. After that pass, i became 12. The test 12 < 10 is false, so the loop stopped with i equal to 12. The number 12 is not a value the body used. It is the first value that failed the test.
A for loop over range works differently. The next program uses the same start, stop and step.
The output is:
for loop variable after range(0, 10, 3): 9The variable keeps 9, the last value the body used. The value 12 is never given to j. So in Python, a for loop leaves the variable at the last value used, and a while loop leaves it at the first value that fails the test. The rule for a while loop is this. After the loop stops, the value of the variable is the first value in the sequence that fails the test. The last value the body used is the value just before it.
Use this rule to predict the end value of a while loop before you run the code. Start with the start value, add the step until the test fails, and stop at the first value that fails.
A variable step
Sometimes the step is not known when you write the program. It might come from a setting, a user, or a function argument. You write it as a variable, and the range uses it.
The output is:
variable step 5 up: 0 5 10 15 20
variable step 5 down: 20 15 10 5 The up loop adds step each pass. Its stop is 24, but the step never lands on 23 or 24, so the largest value it produces is 20. The down loop uses -step as its step, so the numbers go down. Its stop is 0, so 0 is not produced. A negative step needs a start above the stop. With the same numbers and a positive step, the down range would be empty.
A variable step makes the loop reusable. The next example puts the loop in a function, so the caller chooses the start, end and step. It also counts the passes and reports the last value used, so you can compare the loop with a calculation. In this function, end is included in the range, so the stop passed to range is end + 1.
The output is:
start=1 end=20 step=3 count=7 formula=7 last=19
start=0 end=10 step=3 count=4 formula=4 last=9
start=0 end=24 step=6 count=5 formula=5 last=24
start=5 end=5 step=2 count=1 formula=1 last=5
7The formula is (end - start) // step + 1. The // operator is floor division. It keeps the whole part of the division and drops the fraction, so 19 // 3 is 6. The formula gives 6 + 1 = 7 for the first row, which matches the loop. The last line of the output shows another way to get the count. The built-in len function gives the number of values in a range, so len(range(1, 21, 3)) is 7.
The formula works when end is at least start and the step is positive. The third row shows the case where the last value lands exactly on the end: 24 is reached because (24 - 0) // 6 is 4 with no remainder. The second row shows the case where the step misses the end: (10 - 0) // 3 is 3, and the last value is 9, not 10.
The last value can be written as start + ((end - start) // step) * step. For start=0 end=10 step=3, that is 0 + (10 // 3) * 3, which is 0 + 3 * 3, so 9. Both the loop and the formula agree.
Counting down and counting passes
A countdown uses a negative step. The count and last value follow a similar rule, but the signs change. The loop below counts down from 20 by 4. The stop is 0, so the value 0 is not produced.
The output is:
20 16 12 8 4
printed 5 values, last = 4The values are 20, 16, 12, 8 and 4. The next value, 0, is the stop, so range does not produce it. The loop variable's last value is 4.
Stepping over a list in pairs
A step of 2 is often used to read a list two items at a time. The loop below steps through the list by 2. At each position it takes a slice of two items. A slice such as values[i:i + 2] gives the items from position i up to, but not including, position i + 2.
The output is:
10 + 20 = 30
30 + 40 = 70
left over: 50The list has five items, so the pairs start at positions 0, 2 and 4. The pair at position 4 has only one item, 50, because position 5 does not exist. A slice does not fail at the end of a list. It returns fewer items. That is why the check len(pair) == 2 is there, and it handles the leftover item.
The same job can also be done with a stop of len(values) - 1, which is 4 for this list. range(0, 4, 2) produces 0 and 2, so the loop covers the pairs and leaves the last item for code after the loop. A version that reads values[i + 1] directly, with range(0, len(values), 2), fails at the end. The common mistakes section shows that error.
Common mistakes
A common mistake is to think a while loop's variable ends at the last value the body used. In the while example above, the body's last value was 9, but i was 12 after the loop. If your next line of code uses i, use the end value, not the last value.
A second mistake is to choose the wrong direction for the step. Here is a loop that should count up from 0 to 10, but the step is -1.
The output is:
body ran 0 timesThe range from 0 down to 10 is empty, so the body never runs. No error appears, which makes this mistake hard to see. Check the first pass by hand. If the range is empty, the loop does nothing.
A third mistake is to use the count formula for a reversed range. The formula (end - start) // step + 1 assumes end is at least start. If it is not, the formula can give a wrong count. Here is a check for the reversed range with start 7 and end 3:
The output is:
formula gives -3
real count 0The formula gives -3, which is not a count of anything. The loop body runs zero times. If you use the formula, check that end is at least start first, or use len(range(...)), which always gives the real count.
A fourth mistake is a step of zero. range checks its step when it is made, so a zero step stops the program at once:
range(0, 10, 0)The output ends with:
ValueError: range() arg 3 must not be zeroThe lines above that name the file and line. A while loop does not check its step. With i += step and a step of 0, i never moves, the test never changes its answer, and the loop runs forever. The next program adds a counter and a break so that it stops. The break keyword leaves the loop at once.
The output is:
four passes, i = 0, step = 0
doneCheck the step before you use it in a loop. A step read from a user or a file should be tested for zero, and a step of the wrong sign with a < test is another case that never ends.
A fifth mistake is a fractional step with range. range only takes whole numbers:
for i in range(0, 3, 0.5):
print(i)The output ends with:
TypeError: 'float' object cannot be interpreted as an integerFor a fractional step, use a while loop with a decimal start. Then the loop variable is a decimal number, and the fraction is kept:
The output is:
0.0
0.5
1.0Decimal steps are not always exact. Python stores decimal numbers in binary, and 0.1 cannot be stored exactly. Adding 0.1 ten times gives a value slightly below 1. The name _ marks a loop variable that the body does not use:
The output is:
0.9999999999999999A sixth mistake is a step of the wrong type. Python refuses to add a string to a number. It stops when the line runs:
i = 0
i += "2"The output is:
TypeError: unsupported operand type(s) for +=: 'int' and 'str'Fix the step by writing a number, i += 2.
A seventh mistake is to read values[i + 1] when that position does not exist. This version reads the second item of each pair without a check:
values = [10, 20, 30, 40, 50]
for i in range(0, len(values), 2):
print(values[i] + values[i + 1])The output is:
30
70
IndexError: list index out of rangeAt position 4, values[i + 1] means values[5], and a list of five items has no position 5. Use the slice version from the pairs section, or check i + 1 < len(values) before reading the second item.
Practice
Exercise 1. Write a loop that prints the odd numbers from 1 to 15, inclusive. Use a step, not an if inside the body.
Show the answer
The loop is:
for i in range(1, 16, 2):
print(i, end=" ")The output is 1 3 5 7 9 11 13 15 . The start is 1, the step is 2, and the stop is 16, so 15 is the last value. After a for loop, the variable keeps the last value used, so i is 15.
Exercise 2. Write a loop that prints 100, 75, 50, 25 and 0. Use a single loop with a step of -25.
Show the answer
The loop is:
for i in range(100, -1, -25):
print(i, end=" ")The output is 100 75 50 25 0 . The stop is -1, so the value 0 is produced. A stop of 0 would leave 0 out, because range(100, 0, -25) gives 100, 75, 50 and 25.
Exercise 3. For the loop for i in range(2, 20, 5), write down every value the body uses, the last value used, and the value of i after the loop.
Show the answer
The values are 2, 7, 12 and 17. The last value used is 17. After a for loop over range, i keeps 17. If the same loop were written as a while loop, i would become 22 after the pass with 17, the test 22 < 20 would be false, and the loop would stop with i equal to 22.
Exercise 4. The loop below is meant to print 30, 26, 22, and so on down to 2. It prints nothing. Find the bug and fix it.
for i in range(30, 0, 4):
print(i, end=" ")Show the answer
The step is 4, which is positive. A range from 30 up to 0 is empty, so the body never runs. A countdown needs a negative step. The fixed loop is:
for i in range(30, 0, -4):
print(i, end=" ")Its output is 30 26 22 18 14 10 6 2 . The value 2 is the last one used, because the next value, -2, is below the stop 0, so range ends.
Exercise 5. Write a function that prints every multiple of step from 0 up to limit, where step and limit are both parameters. Check it with step 7 and limit 30.
Show the answer
The function is:
def print_multiples(step, limit):
for value in range(0, limit + 1, step):
print(value, end=" ")
print()With step 7 and limit 30, the output is 0 7 14 21 28 . The stop is 31, so 35 is not produced. If the caller passes a step of 0, range raises a ValueError right away, so the program stops with an error instead of running forever. A real program should still check the step and report a clear message.
Summary
range(start, stop, step)produces values fromstartup to, but not including,stop, moving bystep.- A negative step counts down, and the stop must be one below the lowest value you want.
- A
forloop overrangeleaves the variable at the last value used, while awhileloop leaves it at the first value that fails the test. - The count of passes is
len(range(start, stop, step)), and a step of zero is refused byrangewith aValueError. - A fractional step needs a
whileloop with decimal numbers, and decimal steps may not land on the value you expect.