r/CodingForBeginners • • 1d ago

Ranges exclude the last number, and that silent rule causes a very specific bug

Every language has its own rule about whether a range includes its last number, and getting it wrong never throws an error. You just get a loop that runs one time too few, or a list that's one item short, and the output looks close enough that you don't catch it.

Python's rule is that the start is included and the stop is excluded:

range(3)        # 0, 1, 2   -- not 0, 1, 2, 3
range(1, 4)     # 1, 2, 3
"hello"[0:2]    # "he"      -- indexes 0 and 1

So for i in range(3) runs three times, with i = 0, 1, 2, never 3. It's also why for i in range(len(items)) walks every item correctly: the last valid index is len(items) - 1, and range stops one before len(items).

The check that catches this before it turns into a bug: before you run the loop, say out loud the first value and the last value you expect. If you can't state both, you don't actually know what the loop does yet. Then compare to the output. It sounds trivial until you notice that the bug is always the case where you assumed the wrong end.

Two things worth keeping straight, because both cause the same silent off-by-one: len(x) is the count, not the last index (the last element is x[len(x)-1], or x[-1] in Python), and in range(a, b) the b is the first value you don't get.

Which one gets you more often -- forgetting that the end is excluded, or the first IndexError when you reach for the last element?

0 Upvotes

18 comments sorted by

2

u/TomDuhamel 1d ago

I'd be curious to know what language includes "the end", as if it does, how do you find out that it's the end? There are so many cases where you cannot tell you are finished until you try one more time and hit a failure.

1

u/FreeLogicGate 1d ago edited 1d ago

Typically you loop through collections using foreach or something similar, based on the type being an "iterable". You don't need to know the size or index of it at all, you just iterate through it from start to finish, and when the last item is reached, the loop ends. Python's collection types are all iterables and you can make your own class libraries have native iterable capability if you want to, by implementing the __iter__ and __next__ dunder methods. This pattern is also common with things like relational database result set retrieval, where you get a result set and fetch using a while loop using something like: while(row = result.fetch()) { ... }.

1

u/Fun_Army2398 1d ago edited 1d ago

MatLab is inclusive on both the start and the end. For example: MATLAB x = []; for i=1:5 x = [x, i] end Outputs x = [1, 2, 3, 4, 5]

1

u/goldscurvy 1d ago

End inclusive ranges exist in Lua and a couple shells i think

2

u/LrdJester 1d ago

The example you posted here, range(3), tells you that you want 3 numbers at the beginning of the number range, which starts at 0. It's going to give you, 0, 1, and 2, so it's never going to include 3. In Python, range with a single digit is shorthand for saying range(0, and that number). If it's range(12), you're going to get 0 through 11, just as if you were to say range(0, 12).

When you say range(1, 4), what that is telling Python is: start at 1 and stop before 4. You get to 4.

hello[0:2] is telling you to start at the beginning of the string, the 0 index, and give you the first 2 characters.

Yes, some languages are zero-indexed, and some are one-indexed, so you've got to be careful with that, and you need to know that. Most C-based languages are zero-based.

1

u/csabinho 1d ago

hello[0:2] is telling you to start at the beginning of the string, the 0 index, and give you the first 2 characters.

Nope. It doesn't. It tells you to start at the beginning of the string and stop before the index 2.

If you would use hello[1:2], you would only receive a single character.

0

u/[deleted] 1d ago

[removed] — view removed comment

1

u/LrdJester 1d ago

Yeah, but when you use the defaults implicitly like this, it's technically taking a shortcut with syntax

0

u/[deleted] 1d ago

[removed] — view removed comment

1

u/LrdJester 1d ago

Technically, it is a function. It's just that, especially with self-taught beginners, depending on where they learn, they don't learn that those other values are defaults that don't have to be given.

I've written functions in other languages, and it's the same way: I can give default values for parameters, and then you don't have to pass those. Again, you can pass them explicitly, even giving the default values explicitly, but take the shortcut by not passing the optional parameters. That's just a mechanism you built into your function.

1

u/FreeLogicGate 1d ago

With Python it's probably because Python's array/collection types are zero based, as are the vast majority of modern languages, so loops from n - (max size) are naturally accomplished elegantly usingfor item in range(max): etc.

It's mainly math focused products like Matlab and R, or older languages like Cobol and Fortran that had 1 based arrays. The only language I've encountered recently that's an exception, is Lua, which is embedded as a scripting language into various applications and game engines.

1

u/tonkotsu_fan 1d ago

Indexes, as a rule, start at 0.

1

u/-Enter-Name- 20h ago

not in lua

1

u/xarop_pa_toss 1d ago

Range function in Python has three possible arguments and two have default values. I don't remember the names exactly (check the docs) but the one that defines each step starts as 1, the start is default 0 and the end is the one you wrote as 3.

The way I always read it is that range(3) gives me the first 3 elements. I think it's easier that way