What does Python's range(10, 0, -1) produce, and when is a range empty?
answer
- One endpoint is in, one is out
- The step has a direction, not just a size
- Wrong direction means nothing, not an error
- Only one argument value raises
- stop is excluded; zero step is ValueError
basics
~20 srange(10, 0, -1) yields 10 down to 1: start is included and stop excluded, so 0 never appears. A range is empty whenever the step cannot carry start toward stop, as in range(0, 5, -1).
solid answer
~40 s`range(start, stop, step)` is half-open: it begins at `start` and stops **before** `stop`, so `range(10, 0, -1)` gives `10, 9, ... 1` and never reaches `0`. With a negative step the interval runs downward, so `start` must be greater than `stop` for anything to be produced. A range is empty — not an error — whenever the step points the wrong way or the endpoints coincide: `range(5, 5)`, `range(0, 5, -1)` and `range(5, 0)` all yield nothing, and the loop body simply never runs. The one argument value that *is* an error is a zero step: `range(0, 5, 0)` raises `ValueError`, because no step size could ever terminate the sequence. The length is ceiling division of the span by the step's magnitude, clamped at zero.
code
pycon · 10 lines>>> list(range(10, 0, -1))
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> list(range(5, 5)), list(range(0, 5, -1)), list(range(5, 0))
([], [], [])
>>> list(range(10, -1, -1))[-3:]
[2, 1, 0]
>>> range(0, 5, 0)
Traceback (most recent call last):
...
ValueError: range() arg 3 must not be zerogo deeper
Be ready to read off range(10, 0, -1) and range(5, 5) on the spot and to say why the stop value never appears. Knowing that the third argument is a step with a direction, not a count, is the bar here.
State the emptiness rule as one condition — the step must point from start toward stop — rather than reciting cases, and derive the length with ceiling division. Mention that only a zero step is an error.
Show where the half-open convention earns its keep in production code: adjacent spans join with no gap or overlap, and an empty input falls out as zero passes with no special-case branch to forget.
Own the argument that off-by-one bugs are a convention problem rather than a discipline problem: choose half-open bounds consistently across a codebase so ranges, slices and batch offsets compose without adjustment.
### range describes a sequence, it does not contain one `range(start, stop, step)` stores three integers and derives every value on demand. `start` defaults to `0` and `step` defaults to `1`, so `range(5)` means `range(0, 5, 1)`. The three arguments are readable back off the object as `.start`, `.stop` and `.step`, which is often the quickest way to see what a range someone passed you actually means. ### The half-open convention Python's ranges — like its slices — are **half-open**: the start is included, the stop is excluded. `range(0, 5)` is `0, 1, 2, 3, 4`. Two consequences follow, and both are the reason the convention was chosen: * `len(range(0, n))` is exactly `n`, with no `+ 1` anywhere. * Adjacent spans join without gaps or overlaps: `range(0, 5)` followed by `range(5, 10)` covers `0..9` once each. The cost is the one thing candidates get wrong: the stop value itself is never produced. `range(10, 0, -1)` counts `10, 9, 8, 7, 6, 5, 4, 3, 2, 1` and omits `0`. To include zero you must move the stop past it: `range(10, -1, -1)`. ### Negative steps A negative `step` reverses the direction of travel, and the termination test flips with it. With a positive step the sequence continues while the current value is **less than** `stop`; with a negative step it continues while the value is **greater than** `stop`. So a descending range needs `start > stop`: ```python list(range(10, 0, -1)) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] list(range(0, 10, -1)) # [] -- start is already past stop list(range(10, -1, -1)) # [10, 9, ..., 1, 0] ``` For simply walking a sequence backwards, the built-in `reversed()` reads better than hand-rolling the arithmetic: `reversed(items)` for the values, `reversed(range(len(items)))` when you need the positions. ### Empty spans are legal, not exceptional There is exactly one emptiness rule: **if the step does not point from start toward stop, the range is empty.** That single condition covers `range(5, 5)` (endpoints coincide), `range(5, 0)` (positive step, wrong direction) and `range(0, 5, -1)` (negative step, wrong direction). No exception is raised and no warning is issued — a `for` loop over an empty range simply executes zero passes. This matters more than it sounds. It is why code that batches, pages or windows over data needs no special case for an empty input: the natural range comes out empty and the loop body never runs. ### The one real error: a zero step ```pycon >>> range(0, 5, 0) Traceback (most recent call last): ... ValueError: range() arg 3 must not be zero ``` The check happens in the constructor, not on iteration, so the failure surfaces where the range is built. The reason is structural rather than stylistic: with a zero step the value never approaches `stop`, so the sequence has no defined length and no termination. Two related type rules come from the same place. All three arguments must be integers — `range(0, 5.0)` raises `TypeError`, because a float span has no exact integer length. And any object implementing `__index__` is accepted in an integer's place. ### Computing the length The number of values is the span divided by the step's magnitude, rounded **up**, and clamped at zero: ```python def range_length(start, stop, step): if step > 0 and start < stop: return (stop - start + step - 1) // step if step < 0 and start > stop: return (start - stop - step - 1) // -step return 0 ``` So `range(0, 10, 3)` has four values (`0, 3, 6, 9`) — the step overshoots the stop on the last hop, and that is fine, because the stop was never going to be produced anyway. Because the length is arithmetic rather than stored, a range can describe a span far larger than memory: `range(10**100)` constructs instantly. Only `len()` on it fails, with `OverflowError`, because `__len__` must return a value that fits a C-sized integer. ### Equality compares sequences, not arguments Two ranges are equal when they represent the same sequence of values, whatever arguments built them. `range(0) == range(2, 1, 3)` is `True` — both are empty. They are not the same object, and their `.start` / `.stop` / `.step` still differ, so compare the attributes when you care about how the range was written rather than what it produces.
- Why does range(0, 5, 0) raise ValueError instead of looping forever?With a zero step the current value never moves toward stop, so the sequence has no length and no termination condition — it is not a very long range, it is an undefined one. CPython rejects it in the constructor rather than at iteration time, so the error points at the line that built the range.
- What is the idiomatic alternative to writing range(len(items) - 1, -1, -1)?Use `reversed()`. `reversed(items)` walks the values back to front, and `reversed(range(len(items)))` gives the positions in descending order without you having to get the `-1` stop right. Both read better than the hand-written descending range, and both are exactly as lazy.
- Can two ranges built from different arguments compare equal?Yes — ranges compare by the sequence of values they represent, so `range(0) == range(2, 1, 3)` is True since both are empty. Equality says nothing about how the range was written: the objects are distinct and their `.start`, `.stop` and `.step` attributes still differ.
A range is a rule, not a list — like the marks on a ruler described by 'start here, stop before there, move this far each time'. Nothing is drawn until you read a tick off it.
saying these in an interview costs you the question
- Thinks range(10, 0, -1) includes the 0
- Says range(5, 5) raises an error rather than yielding nothing
- Treats a zero step as a loop that simply never advances
- Reverses a range by negating stop instead of the step
- Expects range to accept float start, stop or step
- Claims the stop value is included when the step is negative