Python has a built-in function called range that can easily generate a range of whole numbers. There is another built-in function called xrange that provides the same result, but uses a lot less memory.

## range and xrange

In the following 3 examples we could replace range by xrange and receive the same result:

examples/python/range_3_7.py

```#!/usr/bin/env python
from __future__ import print_function

for v in range(3, 7):
print(v)
```

From the first number (3) until one less than the second number (7):

```3
4
5
6
```

examples/python/range_5.py

```#!/usr/bin/env python
from __future__ import print_function

for v in range(5):
print(v)
```

If there is only one number, that will be the end number and the default start number will be 0. Thus range(5) is the same as range(0, 5)

```0
1
2
3
4
```

examples/python/range_step.py

```#!/usr/bin/env python
from __future__ import print_function

for v in range(0, 5, 2):
print(v)
```

If there are three parameters, the third one is the "step" that defaults to 1 if the third parameter is not present. The result will be the following:

```0
2
4
```

## Variable holding a range

What if we would like to create a variable that will hold the range? We can do that and it is quite simple with either range or xrange

```r = range(1000)
```

Then we can go over the elements:

```for v in r:
pass
```

or we can access them by index:

```print(r)
```

## Memory Size

The big difference is in the amount of memory they use:

examples/python/range-memory.py

```#!/usr/bin/env python
from __future__ import print_function
import sys

r = range(10000)
print(sys.getsizeof(r))  # 80072

x = xrange(10000)
print(sys.getsizeof(x))  # 40
```

The variable holding the range created by range uses 80072 bytes while the variable created by xrange only uses 40 bytes.

The reason is that range creates a list holding all the values while xrange creates an object that can iterate over the numbers on demand.

examples/python/range-type.py

```#!/usr/bin/env python
from __future__ import print_function
import sys

r = range(10000)
x = xrange(10000)
print(type(r))    # <type 'list'>
print(type(x))    # <type 'xrange'>
```

## Speed - Benchmarking range and xrange

The "cost" of the memory savings is that looking up indexes in xrange will take slightly longer. This benchmark code uses the timeit module to show that the xrange version is 10% slower:

examples/python/range-benchmark.py

```#!/usr/bin/env python
from __future__ import print_function
import timeit

r = range(10000)
x = xrange(10000)

def range_index():
z = r

def range_xindex():
z = x

if __name__ == '__main__':
import timeit
print(timeit.timeit("range_index()", setup="from __main__ import range_index",   number=10000000))
print(timeit.timeit("range_xindex()", setup="from __main__ import range_xindex", number=10000000))
```

The resulting numbers are:

```2.16770005226
2.35304307938
```