In this post we’ll see how to define and use functions. We’ll start with an example of a simple program and then see how to improve it using functions in Python. We’ll conclude with an analysis of the advantages we get by using functions in general.

## Motivation

Let’s say we’re working on a program that prints all of the divisors for each number from 1 to 5. We could do it with the following code:

for n in range(1, 5 + 1): divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div) print('Number {} divisors are: {}'.format(n, divisors))

The output will be:

Number 1 divisors are: [1] Number 2 divisors are: [1, 2] Number 3 divisors are: [1, 3] Number 4 divisors are: [1, 2, 4] Number 5 divisors are: [1, 5]

Now, let’s say we want to do the same but for numbers in the range 11 – 15 as well. Since the task is very similar, we could just copy the code we had before, changing the input range:

for n in range(11, 15 + 1): divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div) print('Number {} divisors are: {}'.format(n, divisors))

But what if we need to do the same for dozens of different ranges of numbers? While we could just copy the initial code and change the ranges at each time, this task becomes increasingly tedious and error prone. What if we modify the code by mistake while copying it? Or what if we notice we had a bug on the original code?

Now imagine that we want to do the same but only for pair numbers of the input range. While it’s a minor modification, we will need to address the changes at each part of the copied code. Not to mention the possibility of creating new bugs and errors as we modify it.

We’ll see how **functions** will allow us to address all of these problems.

## Creating functions: divide and conquer

One of the most important skills for programming is the ability to decompose one big problem into several smaller ones. It also has to do with being able to recognize patterns in the task at hand.

One sub-task of our initial problem is getting the divisors of a number. Given a value `n`

, the portion of code that took care of it was:

divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div)

Now that we have identified this small sub-task, we can **encapsulate** it with a **function definition**:

def get_divisors(n): divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div) return divisors

See that we have added a line with the keyword `def`

. This is done to define a function, which is given the name `get_divisors`

. The original code that we’ve encapsulated has been indented (following Python’s syntax). It forms the **function’s body** . Next to the function name, between parentheses, we have the **function parameters**. The parameters are variables (or data in general) that’s passed to the function in order to perform its operations. In this case the only parameter is `n`

.

The final line uses the `return`

keyword. A function is like a sealed box that works inside its own **environment** that is not accessible from outside the function. If we want to get some sort of output from it, we specify it with the *return* statement. It’s like specifying what result would we like to get out of the function. In this case, we’re returning the `divisors`

list that was defined inside the function’s body.

We could define a function in simple terms as a bunch of code to which we give a name.

## Using functions

Once we have defined our function as we did above, we can **call** it as follows:

get_divisors(5) >>> [1, 5]

We’re calling our function `get_divisors`

with `5`

as its **argument**. The function will take this argument, `5`

, and substitute it into its body in place of its parameter `n`

. Basically, we’re asking Python to evaluate the `get_divisors`

body with `n = 5`

.

When the function reaches the `return`

statement, it will “give back” the stated variable. `get_divisors`

was made to return its variable `divisors`

, which corresponds to the list `[1, 5]`

when it’s called with argument `5`

.

At this point we could include our function into our initial program:

for n in range(1, 5 + 1): divisors = get_divisors(n) print('Number {} divisors are: {}'.format(n, divisors))

This is already an improvement. As an extra step, let’s create another function that automates the whole process:

def print_range_divisors(start, end): for n in range(start, end + 1): divisors = get_divisors(n) print('Number {} divisors are: {}'.format(n, divisors))

See that `print_range_divisors`

doesn’t have any `return`

statement. The function will just reach the end of its body and quit. The only intended effect was to `print`

our results. This is known as a **side-effects** function. Another interesting feature is that we called the `get_divisors`

function inside the `print_range_divisors`

body.

We call the resulting function with:

print_range_divisors(1, 5)

Getting the same output as we had initially. Only this time using a single line!

## Advantages

If the last example didn’t convince you of the advantages of using functions, we’ll review some of them now.

### Re-usability

With functions, we create a bunch of code that can be easily reutilized. On the example above, we used it to print the divisors for numbers in the range 1 to 5, but we can use it for any other range of numbers.

### Maintainability

If there’s a bug or if any modification is necessary, there are only a few lines of code to change.

### Abstraction

The idea of abstraction is to “hide” unnecessary details. Take the `get_divisors`

function. Using the notion of abstraction, we could say that we don’t care about how it’s implemented. It gets the job done and that’s all. Just like we did when we called it from the `print_range_divisors`

function. Knowing the implementation details does not change it.

In this sense, you can consider a function as a “black box”. It’s a machine that takes an input and gives back some result. The details of how it does it, what’s inside this machine, are abstracted.

### Modularity

The idea of modularity is to divide your program into pieces. And, as much as possible, to keep those pieces as separate independent parts. This means that a change in one piece will not need to change the other parts as well. This is **modular design.**

### Readability

By separating the code into functions that perform specific and separate operations, our whole program is much more understandable. Without having to look at any of the function body’s, we can tell what they do by their names: `get_divisors`

, `print_range_divisors`

.

Always name your functions and variables with meaningful names!

In a future post, we will cover **higher-order functions**. Functions that include functions as parameters or that return functions.