1.2.19 · D2Introduction to Programming (Python)

Visual walkthrough — `range()` — start, stop, step

1,998 words9 min readBack to topic

We will use exactly three plain-English words the whole way:

  • start — the first number Python lands on.
  • step — how far each jump moves (can be negative = jump left).
  • stop — a wall. We must never land on it or past it.

Step 1 — Lay the numbers on a line and take the first hop

WHAT. Draw a number line. Put a dot at start. That first dot is our zeroth number, which we name . Now jump right by step and drop another dot — that is .

WHY. Everything range() does is "keep hopping by the same amount." Before we can count hops, we must agree on where the hops land. A number line is the honest picture of "an integer, and the one step away from it."

PICTURE. Below, start = 2, step = 2. The blue dots are the landing spots; each yellow arrow is one jump of size step.

Figure — `range()` — start, stop, step

Sanity check with the picture: , , . ✓


Step 2 — Plant the wall and decide the landing rule

WHAT. Mark stop on the same line as a red wall. Our one law: a dot is allowed only if it lands strictly before the wall.

WHY. This is the single design choice that makes range() predictable. "Before the wall" (not "on or before") is what "half-open" means: stop itself is excluded. Choosing strictly before is what gives the clean promise len(range(N)) == N later.

PICTURE. Wall at stop = 9 (red). Green dots landed before the wall — they are kept. The next hop would land on , which is past the wall — that dot is rejected (grey, hollow).

Figure — `range()` — start, stop, step

Step 3 — Turn "how many dots?" into "how much distance?"

WHAT. We want , the number of dots we keep. Instead of counting them one by one, measure the gap from start to stop, and ask how many hops fit inside that gap.

WHY. Counting by hand doesn't scale — imagine a million dots. But a ratio answers it instantly. Distance to cover is . Each hop eats of that distance. So the raw number of hops that fit is a division: We use division here for the same reason you divide to ask "how many $2 hops cover $7?" — division is the tool that answers "how many of THIS fit inside THAT?"

PICTURE. The gap stop - start = 7 is shown as one long yellow bracket. Each step-sized tile below it () shows how many jumps pack into that gap: three whole tiles and a leftover sliver.

Figure — `range()` — start, stop, step

Three-and-a-half hops "fit." But you can't take half a hop and land on a real integer — so what do we do with the ? That is Step 4.


Step 4 — Round up (the ceiling), and see exactly why

WHAT. Apply , the ceiling — the "round up to the next whole number" operation. .

WHY ceiling and not floor? Look again at that leftover sliver of in Step 3. The dot at does land before the wall at , so it is a kept, valid dot — it exists because of that fractional leftover. Rounding down would throw it away and undercount. Rounding up keeps it. In words: any leftover distance, however tiny, means one more dot fit before the wall.

PICTURE. The number sits on a small axis between the integers and . A green arrow snaps it up to (the count we want); a faded red arrow shows the wrong downward snap to and the dot it would wrongly delete.

Figure — `range()` — start, stop, step

Check against Step 2's picture: the kept green dots were — that is 4 dots. ✓


Step 5 — Flip the arrows: counting DOWN with a negative step

WHAT. Let step be negative — hops point left. Take range(10, 0, -2). Now the wall sits to the left of start, and "before the wall" means landing to its right.

WHY. A negative step doesn't need a new formula — it needs a flipped inequality. When you hop left, "not yet past the wall" means you're still greater than stop, so the keep-rule becomes . The beautiful part: the count formula is unchanged, because both and go negative, and negative-over-negative is positive.

PICTURE. start = 10 (right), wall stop = 0 (left), yellow arrows now point left. Green dots are kept; the hollow dot at is on the wall, so rejected.

Figure — `range()` — start, stop, step

Step 6 — The degenerate cases: wrong direction & zero distance

WHAT. Two ways to get nothing:

  1. Point the wrong way: range(10, 0) — default step = +1 tries to hop right, but the wall is to the left.
  2. Wall on start: range(5, 5) — zero distance to cover.

WHY. In case 1 the raw division is , a negative number of hops — meaningless. In case 2 it's . Neither can be a real count, so we wrap the whole thing in : it clamps any negative or zero "fit" to an honest empty range.

PICTURE. Top strip: start at , arrow points right (away from wall at ) → immediately past "invalid direction," zero dots. Bottom strip: start and wall on the same spot at → the very first dot is already on the wall, rejected → zero dots.

Figure — `range()` — start, stop, step

The one-picture summary

Every idea on one number line: start dot, wall at stop, hops of size step, the yellow gap , the division into hop-tiles, the ceiling that keeps the last dot, and the guard for the empty cases.

Figure — `range()` — start, stop, step
\boxed{\,n = \max\!\left(0,\ \left\lceil \tfrac{\text{stop}-\text{start}}{\text{step}}\right\rceil\right)\,}$$ > [!recall]- Feynman: tell the whole story in plain words > I draw a number line and drop my first stone on **start**. I keep tossing stones the same > distance apart — that distance is **step**. Off to the side there's a wall at **stop** that I'm > forbidden to hit or overshoot, so I keep every stone that lands *before* it and reject the first > one that reaches it. To count my stones without counting one-by-one, I measure the whole gap from > start to wall, and I ask "how many step-sized jumps fit in that gap?" — that's a division. If the > answer has a leftover bit, that leftover still let one more stone squeeze in, so I round **up** > (ceiling). And if I aimed the wrong way, or the wall was right on my starting stone, the division > comes out zero or negative — nonsense for a stone-count — so I clamp it to zero with $\max$ and > get an empty handful. That's the entire machine. > [!mnemonic] The picture in five words > **Gap ÷ Step, round UP, clamp.** Distance over jump, ceiling, then $\max(0,\cdot)$. --- ## Active recall Why do we **divide** $(\text{stop}-\text{start})$ by $\text{step}$? ::: Division answers "how many step-sized hops fit inside the gap" — the count of valid landing spots. Why **ceiling** and not floor or nearest-rounding? ::: Any leftover distance means one more dot still landed before the wall, so we round up to keep it. Why is the keep-rule $a_k < \text{stop}$ and not $\le$? ::: Strictly-less **excludes** `stop`, giving the half-open interval and the clean `len(range(N)) == N`. For a negative step, why does the *count* formula stay the same? ::: Both $(\text{stop}-\text{start})$ and $\text{step}$ turn negative, and negative ÷ negative is positive. What does $\max(0,\cdot)$ protect against? ::: Wrong-direction (negative division) and zero-distance ranges — it clamps them to an empty range. Count of `range(2, 9, 2)` from the formula? ::: $\lceil(9-2)/2\rceil = \lceil 3.5\rceil = 4$. Count of `range(10, 0, -2)`? ::: $\lceil(0-10)/(-2)\rceil = \lceil 5\rceil = 5$. --- ## Connections - [[1.2.19 `range()` — start, stop, step (Hinglish)]] — the parent topic, all forms and mistakes. - [[Arithmetic sequences]] — $a_k = \text{start} + k\cdot\text{step}$ is exactly an arithmetic progression. - [[for loops]] — this count is how many times a `for i in range(...)` loop body runs. - [[Lists and list()]] — `list(range(...))` materialises the dots into an actual list. - [[Indexing and slicing]] — slices obey the same half-open `[start, stop)` counting picture. - [[numpy.arange]] / [[linspace]] — same number-line idea, but for float hops.