Randomness is all around us. When you flip a coin or roll a die, you can never be sure of the final outcome. This unpredictability has a lot of applications like determining the winners of a lucky draw or generating test cases for an experiment with random values produced based on an algorithm.

## Mathematical Modules in Python: Random

Keeping this usefulness in mind, Python has provided us with the random module. You can use it in games to spawn enemies randomly or to shuffle the elements in a list.

## How Does Random Work?

Nearly all of the functions in this module depend on the basic `random()`

function, which will generate a random float greater than or equal to zero and less than one. Python uses the Mersenne Twister to generate the floats. It produces 53-bit precision floats with a period of 2**19937-1. It is actually the most widely used general-purpose pseudo-random number generator.

Sometimes, you want the random number generator to reproduce the sequence of numbers it created the first time. This can be achieved by providing the same seed value both times to the generator using the `seed(s, version)`

function. If the parameter *s* is omitted, the generator will use the current system time to generate the numbers. Here is an example:

```
import random
random.seed(100)
random.random()
# returns 0.1456692551041303
random.random()
# returns 0.45492700451402135
```

Keep in mind that unlike a coin flip, the module generates pseudo-random numbers which are completely deterministic, so it is not suitable for cryptographic purposes.

## Generating Random Integers

The module has two different functions for generating random integers. You can use `randrange(a)`

to generate a random whole number smaller than `a`

.

Similarly, you can use `randrange(a, b[,step])`

to generate a random number from `range(a, b, step)`

. For example, using `random.randrange(0, 100, 3)`

will only return those numbers between 0 and 100 which are also divisible by 3.

If you know both the lower and upper limit between which you want to generate the numbers, you can use a simpler and more intuitive function called `randint(a, b)`

. It is simply an alias for `randrange(a, b+1)`

.

```
import random
random.randrange(100)
# returns 65
random.randrange(100)
# returns 98
random.randrange(0, 100, 3)
# returns 33
random.randrange(0, 100, 3)
# returns 75
random.randint(1,6)
# returns 4
random.randint(1,6)
# returns 6
```

## Functions for Sequences

To select a random element from a given non-empty sequence, you can use the `choice(seq)`

function. With `randint()`

, you are limited to a selection of numbers from a given range. The `choice(seq)`

function allows you choose a number from any sequence you want.

Another good thing about this function is that it is not limited to just numbers. It can select any type of element randomly from a sequence. For example, the name of the winner of a lucky draw among five different people, provided as a string, can be determined using this function easily.

If you want to shuffle a sequence instead of selecting a random element from it, you can use the `shuffle(seq)`

function. This will result in an *in place* shuffling of the sequence. For a sequence with just 10(n) elements, there can be a total 3628800(n!) different arrangements. With a larger sequence, the number of possible permutations will be even higher—this implies that the function can never generate all the permutations of a large sequence.

Let’s say you have to pick 50 students from a group of 100 students to go on a trip.

At this point, you may be tempted use the `choice(seq)`

function. The problem is that you will have to call it about 50 times in the best case scenario where it does not choose the same student again.

A better solution is to use the `sample(seq, k)`

function. It will return a list of *k* unique elements from the given sequence. The original sequence is left unchanged. The elements in the resulting list will be in selection order. If *k* is greater than the number of elements in the sequence itself, a ValueError will be raised.

```
import random
ids = [1, 8, 10, 12, 15, 17, 25]
random.choice(ids) # returns 8
random.choice(ids) # returns 15
names = ['Tom', 'Harry', 'Andrew', 'Robert']
random.choice(names) # returns Tom
random.choice(names) # returns Robert
random.shuffle(names)
names
# returns ['Robert', 'Andrew', 'Tom', 'Harry']
random.sample(names, 2)
# returns ['Andrew', 'Robert']
random.sample(names, 2)
# returns ['Tom', 'Robert']
names
# returns ['Robert', 'Andrew', 'Tom', 'Harry']
```

As you can see, `shuffle(seq)`

modified the original list, but `sample(seq, k)`

kept it intact.

## Generating Random Floats

In this section, you will learn about functions that can be used to generate random numbers based on specific real-value distributions. The parameters of most of these functions are named after the corresponding variable in that distribution’s actual equation.

When you just want a number between 0 and 1, you can use the `random()`

function. If you want the number to be in a specific range, you can use the `uniform(a, b)`

function with *a* and *b* as the lower and higher limits respectively.

Let’s say you need to generate a random number between *low* and *high* such that it has a higher probability of lying in the vicinity of another number *mode*. You can do this with the `triangular(low, high, mode)`

function. The *low* and *high* values will be 0 and 1 by default. Similarly, the *mode* value defaults to the mid-point of the low and high value, resulting in a symmetrical distribution.

There are a lot of other functions as well to generate random numbers based on different distributions. As an example, you can use `normalvariate(mu, sigma)`

to generate a random number based on a normal distribution, with *mu* as mean and *sigma* as standard deviation.

```
import random
random.random()
# returns 0.8053547502449923
random.random()
# returns 0.05966180559620815
random.uniform(1, 20)
# returns 11.970525425108205
random.uniform(1, 20)
# returns 7.731292430291898
random.triangular(1, 100, 80)
# returns 42.328674062298816
random.triangular(1, 100, 80)
# returns 73.54693076132074
```

## Weighted Probabilities

As we just saw, it is possible to generate random numbers with uniform distribution as well as triangular or normal distribution. Even in a finite range like 0 to 100, there are an infinite number of floats that can be generated. What if there is a finite set of elements and you want to add more weight to some specific values while selecting a random number? This situation is common in lottery systems where numbers with little reward are given a high weighting.

If it is acceptable for your application to have weights that are integer values, you can create a list of elements whose frequency depends on their weight. You can then use the `choice(seq)`

function to select an element from this weighted list randomly. Here is an example showing the selection of a prize amount randomly.

```
import random
w_prizes = [('$1', 300), ('$2', 50), ('$10', 5), ('$100', 1)]
prize_list = [prize for prize, weight in w_prizes for i in range(weight)]
random.choice(prize_list)
# returns '$1'
```

In my case, it took ten trials to get a $2 prize chosen from the list. The chances of getting a $100 prize would be much lower. Similarly, you can also add bias to other such programs.

## Final Thoughts

This module can be useful in a lot of situations like shuffling the questions in an assignment or generating random usernames or passwords for your users by using the `shuffle()`

function. You can also generate random numbers uniformly as well as give weighting to numbers in a specific range. In our next tutorial, we will be using the functions from this module to generate random data for statistical analysis.

Do you have some interesting applications of random number generators in mind that can be useful to fellow readers? Let us know in the comments.