A range in python is something that includes the lower-bound but does not include the upper-bound. At first, this may seem confusing but it is intentional and it is used throughout python.
list(range(4, 10))
# [4, 5, 6, 7, 8, 9]
# does not include 10!
xs = ['a', 'b', 'c', 'd', 'e']
xs[1:4]
# [xs[1], xs[2], xs[3]]
# does not include xs[4]!
bisect.bisect_left('jack', names, 2, 5)
# perform a binary search on names[2], names[3], names[4]
# does not include names[5]!
random.randrange(4, 8)
# picks a random number from 4, 5, 6, 7
# does not include 8!
In mathematics, this is called a half-open interval. Python chooses to use half-intervals because they avoid off-by-one errors:
[to avoid off-by-one errors] ... ranges in computing are often represented by half-open intervals; the range from m to n (inclusive) is represented by the range from m (inclusive) to n + 1 (exclusive)
And so as a result, most python library functions will use this idea
of half-open ranges when possible.
However randint is one that does not use half-open intervals.
random.randint(4, 8)
# picks a random number from 4, 5, 6, 7, 8
# it does indeed include 8!
The reason is historical:
- randint was added early in v1.5 circa 1998 and this function name was used for generating both random floating-point numbers randomly and integers randomly
- randrange was added in python in v1.5.2. In v2.0, a notice was added saying that randint is deprecated.
- the deprecation notice for randint has since been removed
randint started off as an earlier library function that didn't include half-open interval because this idea was less cemented in python at the time.