Skip to content
VLSI Mentor

Python Part I · Module 10 · Loops

Counting With range()

Every collection so far was something you had: a list you wrote, a log you read, a dictionary you built. A seed sweep is not like that. You do not have a hundred seeds — you have a starting value and a count, and the hundred seeds follow from them. range() is the thing that turns a rule into a collection a loop can walk. It takes about a minute to learn and has exactly one feature that trips people up permanently, which is that it stops before the number you gave it. That is the right design, and this chapter shows why by demonstration rather than asking you to remember it.

Foundation15 min readPythonPart IFundamentalsLoopsrange

Part I · Module 10 · Chapter 10.4 · Counting With range()

1. A Collection You Do Not Write Down

Running one test at ten seeds means ten values that you should not type:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for seed in range(10):
    print(seed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0
1
2
3
4
5
6
7
8
9

Ten passes, from 0 to 9. range(10) is a collection in every sense that matters to a loop — it supplies items one at a time, in order, and the loop is the same loop.

The thing to notice immediately: ten values, and the last one is 9.

2. Why the End Is Excluded

range(n) produces n values starting at zero, so the final value is n - 1. Stated as a rule it is forgettable. It is easier to keep hold of through its three consequences, each of which is the reason it was designed this way.

The count is the argument. You never work out how many passes there will be:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(len(range(10)))
print(len(range(3)))
print(len(range(0)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
10
3
0

range(10) means ten of them. If the end were included, range(10) would give eleven values and every repeat count would need a - 1.

Ranges tile. Two adjacent ranges cover a span exactly once, with no overlap and no gap:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(list(range(0, 3)))
print(list(range(3, 6)))
print(len(range(0, 3)), "+", len(range(3, 6)), "=", len(range(0, 6)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[0, 1, 2]
[3, 4, 5]
3 + 3 = 6

The first ends where the second begins, and 3 appears in exactly one of them. Splitting a sweep of 600 seeds across four machines is range(0, 150), range(150, 300), range(300, 450), range(450, 600) — written straight down, with no fencepost arithmetic anywhere.

It matches indexing. range(len(x)) is exactly the valid positions of x:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tests = ["smoke", "reset", "burst"]
print(list(range(len(tests))))
print(tests[0], tests[2])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[0, 1, 2]
smoke burst

Three items, positions 0 to 2, and no - 1 written anywhere. This is the same half-open convention that made slicing work in Chapter 4.5, where s[0:3] took three characters.

3. Starting Somewhere Else

With two arguments, the first is where to start:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
base = 4210

for seed in range(base, base + 3):
    print("seed", seed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seed 4210
seed 4211
seed 4212

The starting value can be a name, so a sweep is parameterised by where it begins and how many it wants. Written out literally it reads the same:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for seed in range(4210, 4213):
    print("running axi_smoke with seed", seed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
running axi_smoke with seed 4210
running axi_smoke with seed 4211
running axi_smoke with seed 4212

Three seeds. The end is 4213 because the end is excluded — and the count is 4213 - 4210 = 3, which is the subtraction working out because of the convention rather than despite it.

4. Counting by Something Other Than One

A third argument is the step:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(list(range(0, 10, 2)))
print(list(range(1, 10, 3)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[0, 2, 4, 6, 8]
[1, 4, 7]

A negative step counts down, and then the start is the high value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(list(range(3, 0, -1)))
print(list(range(10, 0, -2)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[3, 2, 1]
[10, 8, 6, 4, 2]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for n in range(3, 0, -1):
    print("retrying, attempts left:", n)

print("giving up")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
retrying, attempts left: 3
retrying, attempts left: 2
retrying, attempts left: 1
giving up

The end is still excluded, which is why range(3, 0, -1) stops at 1 and not 0.

A range that cannot get from start to end is simply empty — not an error:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(list(range(5, 1)))
print(len(range(5, 1)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[]
0
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for n in range(5, 1):
    print("this never prints")

print("the loop ran zero times")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the loop ran zero times

That is Chapter 10.1's zero-pass case: an empty collection runs the body zero times and raises nothing. It is also a common silent bug — a sweep whose bounds are the wrong way round does nothing at all, quietly.

A step of zero is an error, because it could never terminate:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
print(list(range(0, 5, 0)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ValueError: range() arg 3 must not be zero

5. A range Does Not Build Anything

range() does not produce a list. It produces a range object, which generates values on demand:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(range(3))
print(type(range(3)))
print(list(range(3)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
range(0, 3)
<class 'range'>
[0, 1, 2]

Printing a range shows you its recipe, not its contents — a frequent source of confusion when debugging. list() materialises it when you actually want to look.

The reason this matters is cost. A range of a million integers occupies the same tiny amount of memory as a range of three, because neither stores any integers at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
r = range(1000000)

print(r)
print(len(r))
print(999999 in r)
print(1000000 in r)
print(r[0], r[-1], r[10])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
range(0, 1000000)
1000000
True
False
0 999999 10

Length, membership and indexing all answer immediately — by arithmetic, not by searching. So for seed in range(1000000): is not a program that first builds a million-element list; it is a loop that counts.

6. range Counts, So It Needs Integers

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
print(list(range(2.5)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TypeError: 'float' object cannot be interpreted as an integer

Even a float with nothing after the decimal point is refused:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
print(list(range(3.0)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TypeError: 'float' object cannot be interpreted as an integer

range counts whole steps, and there is no sensible number of steps between 0 and 2.5. If a count arrives as a float — from a division, typically — convert it deliberately with int(), which Chapter 2.11 covered, so that the rounding is something you chose rather than something that happened.

7. When Not to Use range()

This is the most common misuse of range(), and it is worth stating plainly because it is usually taught as the way to loop:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tests = ["smoke", "reset", "burst"]

for i in range(len(tests)):
    print(i, tests[i])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0 smoke
1 reset
2 burst

It works. It is also a longer, slower, more fragile way of writing this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tests = ["smoke", "reset", "burst"]

for test in tests:
    print(test)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke
reset
burst

The first version introduces an index you did not want, does a lookup per pass, and can go out of bounds if the list changes. Chapter 10.1's rule applies: loop over the collection, not over the numbers that happen to address it.

range(len(...)) earns its place only when you genuinely need the position as data — reporting "failure 3 of 12", or pairing two collections by position. Even then there is a better tool: enumerate() gives you the index and the item together, and Chapter 10.9 covers it. Treat for i in range(len(x)): as a sign that one of those two is what you actually meant.

A related off-by-one is worth seeing, since it is the usual way to "skip the header":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rows = ["name seed status", "axi_smoke 4210 PASS", "axi_burst 4211 FAIL"]

for i in range(1, len(rows)):
    print(rows[i])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
axi_smoke 4210 PASS
axi_burst 4211 FAIL

That is correct — range(1, 3) gives 1 and 2. It is still clearer as a slice, because Chapter 5.2 gave you one: for row in rows[1:]: says every row after the first without arithmetic.

8. The Boundary Table

WrittenProducesCountLast value
range(0)nothing0—
range(3)0, 1, 232
range(1, 4)1, 2, 333
range(0, 10, 2)0, 2, 4, 6, 858
range(3, 0, -1)3, 2, 131
range(5, 1)nothing0—
range(0, 5, 0)——ValueError
range(2.5)——TypeError

The count of range(a, b) is b - a; the count of range(a, b, s) is that span divided by the step, rounded up.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# A
print(list(range(4)))

# B
print(list(range(2, 7, 2)))

# C
total = 0
for n in range(1, 5):
    total = total + n
print("total:", total)

# D
count = 0
for n in range(10, 0, -3):
    count = count + 1
print("passes:", count, "last:", n)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[0, 1, 2, 3]
[2, 4, 6]
total: 10
passes: 4 last: 1

A — four values beginning at zero.

B — starts at 2, steps by 2, stops before 7, so 6 is the last that fits.

C — 1 + 2 + 3 + 4, and 5 is excluded. Had the end been inclusive this would be 15.

D — 10, 7, 4, 1 — four passes, and the loop variable survives holding the last value, which is Chapter 10.1's Section 4.

10. Exercises

Exercise 1 — write the range. Give the range(...) call for each: (a) the ten seeds 4210–4219 (b) the numbers 1 to 100 inclusive (c) every second seed from 0 to 98 inclusive (d) 5 down to 1 (e) nothing at all, using two arguments.

Exercise 2 — predict the count. Without running, give the number of passes for range(7), range(2, 9), range(0, 20, 5), range(1, 2), range(4, 4), range(9, 2, -2). Then verify all six with len().

Exercise 3 — the tiling property. Split range(0, 600) across four machines as four adjacent ranges. Print each one's first and last value, and confirm the four lengths sum to 600 with no value appearing twice.

Exercise 4 — the empty sweep. Write a loop over range(4210, 4200) that would print on every pass. Explain in two sentences why nothing printed and why that is more dangerous than an error.

Exercise 5 — remove the index. Rewrite each without range(len(...)), or say why it cannot be: (a) printing every test name (b) printing 1. smoke, 2. reset with a running number (c) printing every row of a table after the header.

Exercise 6 — lazy or not. Predict the output of print(range(5)) and of print(list(range(5))), then verify. In one sentence, say what the difference tells you about what range(5) is.

Exercise 7 — the two errors. Produce both range() errors deliberately and record the exact messages. For each, say in one sentence why Python refuses rather than guessing.

Exercise 8 — countdown. Print a countdown from 5 to 1 and then go. Then change it to count down to 0 inclusive, and say which argument you changed and why.

11. Summary

  • range(n) produces n values starting at zero, so the last is n - 1.
  • The end is excluded for three reasons: the count is the argument, adjacent ranges tile with no overlap or gap, and range(len(x)) is exactly the valid positions of x.
  • Half-open is the same convention as slicing — learn it once, use it in both places.
  • range(a, b) starts at a, and its count is b - a.
  • A third argument is the step; a negative step counts down from the higher start.
  • A range that cannot reach its end is empty, runs the body zero times, and raises nothing — a silent bug when the bounds are reversed.
  • A step of zero is a ValueError, and a float argument is a TypeError.
  • A range builds nothing. It prints as its recipe, answers len() and in by arithmetic, and a million-element range costs no more than a three-element one.
  • Do not write for i in range(len(x)): to visit items — loop the collection directly, or use enumerate() when you truly need the position.

The next chapter covers the other kind of loop: the one for when you do not know how many passes there will be.

Where this fits

Part of the Python curriculum.