Python’s range() builds a sequence of integers from a start value up to, but not including, a stop value, using an optional step.
range() is one of those Python tools that looks tiny on the page and then pops up everywhere once you start writing loops. You’ll see it in beginner scripts, data cleanup code, test setups, and list processing. Get this one right and a lot of Python starts to feel less slippery.
The whole thing comes down to three moving parts: where the numbers begin, where they stop, and how far they jump each time. The stop value stays out. That one rule does most of the heavy lifting. When people get stuck with range(), it’s usually because they expected the stop value to be included.
What range() Gives You
range() does not hand back a full list. It returns a range object, which is an iterable sequence type. You can loop over it, test whether a number is inside it, slice it, and turn it into a list when you want to inspect the values on screen.
Python lets you call it in three forms:
range(stop)— starts at0, moves by1range(start, stop)— starts where you say, moves by1range(start, stop, step)— starts where you say, moves by your chosen step
That means range(5) gives 0, 1, 2, 3, 4. It does not give 1 through 5. If you want that, you need range(1, 6).
Reading Start, Stop, And Step In Real Code
Start is the first number Python tries to use. Stop is the boundary. Step is the jump size between values. A positive step moves upward. A negative step moves downward.
Say you write range(2, 10, 2). Python starts at 2, checks that it is still short of 10, then keeps adding 2. You get 2, 4, 6, 8. The 10 never appears because stop marks the edge, not the last included value.
The same rule works in reverse. range(10, 0, -2) gives 10, 8, 6, 4, 2. Zero stays out for the same reason. If your step points the wrong way, the range is empty. range(2, 10, -1) has nowhere valid to go, so it produces no values.
list(range(5)) # [0, 1, 2, 3, 4]
list(range(1, 6)) # [1, 2, 3, 4, 5]
list(range(0, 10, 3)) # [0, 3, 6, 9]
list(range(10, 0, -3)) # [10, 7, 4, 1]
Range In Python Loops And Indexing
Most people first meet range() inside a for loop. That makes sense. It gives the loop a neat stream of numbers without building a big list first. The official Python tutorial section on range() uses it in this exact way.
When you just need to repeat something a set number of times, range() keeps the loop short and clear:
for _ in range(3):repeats a block three timesfor i in range(5):gives counters from0to4for i in range(1, 6):lines up with human-style counting
It also shows up when positions matter. Say you need to compare each item with the one before it, swap values by index, or walk a grid row by row. In those cases, range() gives you the exact positions you need.
Why Python Leaves The Stop Value Out
At first, the excluded stop can feel odd. After a bit, it starts to feel tidy. Python uses zero-based indexing, so a sequence of length 10 has legal indices from 0 through 9. That lines up perfectly with range(10).
This also makes slicing and counting patterns fit together without extra mental math. The start is included. The stop is excluded. You see that rule in slices, and you see it again in range(). Once that clicks, a lot of off-by-one bugs fade out.
If you need an inclusive upper end, add one for upward counts or subtract one from the stop boundary when counting downward. That small adjustment is normal Python style.
Common range() Calls At A Glance
| Call | Values Produced | What To Notice |
|---|---|---|
range(4) |
0, 1, 2, 3 | Starts at 0 when only one argument is given |
range(1, 5) |
1, 2, 3, 4 | Stop stays out |
range(0, 10, 2) |
0, 2, 4, 6, 8 | Positive step skips by 2 |
range(5, 0, -1) |
5, 4, 3, 2, 1 | Negative step walks downward |
range(3, 3) |
empty | Start already meets the stop boundary |
range(3, 10, -1) |
empty | Step points the wrong way |
range(-3, 2) |
-3, -2, -1, 0, 1 | Negative starting values work fine |
range(10, -1, -5) |
10, 5, 0 | The pattern keeps stepping until the stop boundary would be crossed |
Why range() Is Not A List
This is where range() gets nicer than it first appears. A list stores every value. A range object stores the rule: start, stop, and step. Python can then compute each value when you ask for it.
That keeps memory use small even when the numbers stretch far. The official range type docs state that a range object keeps the same small memory footprint no matter how many values it represents.
You’ll notice that difference with something like range(1_000_000_000). Python can represent that pattern compactly. Turn it into a list, and you ask Python to build every one of those values at once.
That’s why range() fits loops so well. It acts like a sequence where you need one, but it does not drag list-sized storage into every numeric loop.
What Else A Range Object Can Do
A lot of beginners treat range() like a loop-only helper. It does more than that. Since it is a sequence type, you can use several sequence-style operations on it:
5 in range(10)checks membershiprange(0, 20, 2)[3]pulls out a value by indexrange(0, 20, 2)[:4]gives a smaller range objectrange(0, 20, 2)[-1]gets the last value in the pattern
That means you can inspect, test, and slice a range without turning it into a list first. That behavior surprises a lot of new Python users the first time they see it.
Mistakes That Trip People Up
Most range() bugs fall into a short list:
- Forgetting that stop stays out. If you want 1 through 5, use
range(1, 6), notrange(1, 5). - Using the wrong step direction. Counting down needs a negative step.
- Expecting a list on print.
print(range(5))showsrange(0, 5). Uselist(range(5))when you want to see all values. - Using a zero step.
range(0, 10, 0)raisesValueErrorbecause Python cannot move through the sequence.
There’s another classic slip: the off-by-one error. Your loop runs one time too many or one time too few. The clean fix is to read the call out loud: start here, stop before there, move by this much.
Picking The Right Loop Pattern
| Task | Pattern | Why It Fits |
|---|---|---|
| Repeat a block five times | range(5) |
Short and direct |
| Count from 1 to 10 | range(1, 11) |
Human-style numbers with stop kept out |
| Walk backward through indices | range(len(items)-1, -1, -1) |
Starts at the last valid index |
| Get index and value together | enumerate(items) |
Cleaner than manual indexing |
| Take every third value | range(start, stop, 3) |
Built-in stepping keeps the loop tidy |
When enumerate() Beats range()
New Python users often reach for range(len(items)) for every list loop. It works, but it can feel stiff when you only need the values. In that case, loop over the list itself.
If you need both position and value, enumerate() reads better than indexing by hand. Your code says what it means, which makes later edits less annoying.
Use range() when numbers are the point. Use direct iteration when values are the point. Use enumerate() when you need both. That split clears up a lot of beginner hesitation.
A Clean Mental Model
Think of range() as a number rule, not a bag of numbers. It starts at one place, stops before another, and moves in fixed jumps. Read it that way and the odd parts stop feeling odd.
When a loop acts strange, test the range by itself with list(). Seeing the exact values usually reveals the bug right away. Do that a few times and range() turns from a sticking point into one of the handiest parts of Python.
References & Sources
- Python Software Foundation.“The range() Function.”Shows loop examples, the excluded stop value, and positive or negative stepping.
- Python Software Foundation.“Ranges.”Explains the range type, sequence behavior, slicing, and its small fixed memory use.
- Python Software Foundation.“enumerate().”Shows the built-in tool for looping with both positions and values.
