Skip to content

Functions

Functions are self contained objects that are used to create code that we often want to reuse. The most arbitrary use of a function is in this code:

example-1.py from 1 to 9
# example-1.py first example
# Introducing functions with functions
def our_first_function(x, y):
    total = x + y
    print(total)

our_first_function(5, 6)
our_first_function(20, 25)
our_first_function(7, 11)
Output
11
45
18

As you can see, we have created our first function. To create a function you must first use the def keyword followed by the name for the function. The naming conventions for functions are the same as they are for variables. However, it is good practice to have descriptive names for your functions so that you can recognise what they do at a glance.

The shape of a user defined function is always of the form:

def name_of_function(passed_variable):
    <contents of the function,>
    <indented even across>
    <multiple lines.>
The contents of the function is always indented to show that it belongs to the function definition above it. Python then knows that this is the local scope of that function and looks there first for any variables, functions or data you might be trying to use in the function. The passed_variable placeholder can be one or more variables that are passed to the function so they can be used. These are known as function arguments, and are how we can move data between one function and another.

Hint

Understanding how data moves between functions is an important part of programming, as side effects can happen when you don't know what's happening behind the scenes. Make sure you feel solid with the next concepts in passing by value and reference, as they can change depending on the programming language and is the starting point for reducing bugs in code and understanding secure programming.

Passing-by-value and Passing-by-reference

These concepts are two different ways we pass variables to functions in programming. When we pass a value by reference we are essentially passing the actual variable to the function and when we pass by value we are copying the value of the variable and passing the copy to the function. Once the function has done what it needs to do, the pass by value variable is then discarded, this is also called a pure function. The best way to program to ensure less bugs is to only programming using pure functions, but sometimes there is need to program using pass by reference and modifying functions. For a more in-depth look at how passing by reference and value work, take a look at this C++ code:

example-pass-by.cpp from the beginning to the end
#include <iostream>
using namespace std;

// C++ prototypes, not technically required here but old habits...
void pass_by_reference(int &var_1);
void pass_by_value(int var_1);

// example-pass-by.cpp
// Illustrating pass by reference and value 
// using C++ 

// Function declarations
void pass_by_reference(int &var_1)
{
    var_1 = var_1 + 5;
    cout << var_1 << "\n";
}

void pass_by_value(int var_1)
{
    var_1 = var_1 + 10;
    cout << var_1 << "\n";
}

// Program entry point
int main ()
{
    int var_1 = 20;
    cout << "The initial value of var_1 is: " << var_1 << "\n";

    // This preserves the memory address and it 
    // will keep the value.
    cout << "This is after it's passed by reference:" << "\n";
    pass_by_reference(var_1);
    cout << var_1 << "\n";

    // This doesn't preserve the memory address 
    // and passes a copy of the value
    cout << "This is after it's passed by Value." << "\n"; 
    pass_by_value(var_1);
    cout << var_1 << "\n";
} 

Example

In this example you can see that, apart from all the C++ shenanigans, that there isn't a huge difference from python. There are still function definitions and there is still a part where the main program is run. In the function definitions we can see that there is two functions declared, one called pass_by_value and the other pass_by_reference. In pass by reference you can see a key symbol & where in the area where passed variables are put. This is a call to the compiler to tell it to pass in the reference to the variable and not a copy, so when we run this code we get interesting results. Try it for yourself:

The code first runs in main and creates an integer variable called var_1. Then prints out the value of var_1 to the console output (cout). We follow the program down main until we get down to where it calls pass_by_reference. Here it takes in var_1 to the value of 20 and pluses 5 to it, then prints the output. The code then prints out the value of var_1 again to make track it's value. Once we get to the pass_by_value function we might expect that var_1 to be 20 still, but because we passed by reference it makes the changes we made to var_1 permanent and now the value of var_1 is 25. This can throw the math off we were expecting to use in our program by enough that it breaks something in a larger code base, and worse it would be really difficult to track down that bug in code that is 10's of thousands of lines long. To prove that only a copy is passed to pass_by_value we do the exact same math and print out the value of var_1 during the function actions and after, at the very end we see the value change back to 25 proving that only a copy was passed to pass_by_value.

One of the biggest reasons we need to understand why this happens and how it happens in other programming languages is that Python is a pass by object language, this means that depending on the variable type and the way we write our code, it can change how the variable is passed into a python function.

Python's pass-by-object

To understand pythons ability to manipulate variables passed to it's functions you should consider this code:

example-2.py from the beginning to the end
#!/usr/bin/env python3
# example-2.py
# showing pass by object in python
var = 20 
print("Initial variable value and object number:")
print(var)
print(id(var))
print(type(var))

def pass_by_reference():
    global var
    print(f"Pass by reference value: {var}")
    print(f"Pass by reference id: {id(var)}")
    print(type(var))
    var = var + 20
    print(f"Pass by reference final value: {var}")
    print(f"Pass by reference final id: {id(var)}")
    print(type(var))

pass_by_reference()
print(f"After pass by reference value: {var}")
print(f"After pass by reference id: {id(var)}")

def pass_by_value(var_1):
    # var_1 is a place holder and whatever is passed to the function 
    # then uses that label. When var is passed to this function it is 
    # still the same object at this point.
    print(f"Pass by value: {var_1}")
    print(f"Pass by value id: {id(var_1)}")
    print(locals())
    var_1 = var_1 + 20
    print(f"pass by value final value: {var_1}")
    print(f"pass by value final id: {id(var_1)}")
    print(locals())

pass_by_value(var)
print(f"Var's final value: {var}")
print(f"Var's final id: {id(var)}")
print(locals()) 

Example

In this example, you can see that we use the inbuilt function id to show the object identifying number of the variable as we go through the code. This gives us an interesting look at what is happening under the hood in python. As the program runs you can see that it first creates the global variable var with an id number.

Passing by object has some interesting side effects, you can use it to program everything as pure functions or use it to manipulate mutable variables, but it depends on how you write the function. In our previous examples we programmed the function to change the value of a variable passed to it. We can program any function as a pure function implicitly by taking advantage of the assignment operator and how it creates objects. Consider and compare this example to the previous one.

example-3.py from the beginning to the end
#!/usr/bin/env python3
# example-2.py
# showing pass by 
var = 20 
print("Initial variable value and object number:")
print(var)
print(id(var))

def pass_by_reference():
    global var
    print(f"Pass by reference value: {var}")
    print(f"Pass by reference id: {id(var)}")
    total = var + 20
    print(f"Pass by reference final value: {var}")
    print(f"Pass by reference final id: {id(var)}")
    print(total)

pass_by_reference()
print(f"After pass by reference value: {var}")
print(f"After pass by reference id: {id(var)}")

def pass_by_value(var_1):
    # var_1 is a place holder and whatever is passed to the function 
    # then uses that label. When var is passed to this function it is 
    # still the same object at this point.
    print(f"Pass by value: {var_1}")
    print(f"Pass by value id: {id(var_1)}")
    var_1 = var_1 + 20
    print(f"pass by value final value: {var_1}")
    print(f"pass by value final id: {id(var_1)}")

pass_by_value(var)
print(f"Var's final value: {var}")
print(f"Var's final id: {id(var)}") 

In programming it is generally considered to be advantageous to avoid side effects in code, as others might reuse the code without understanding that it contains side effects, and it also helps to avoid bugs in your own projects. Understanding these concepts is also a good first step to becoming a secure programmer and finding zero days through reading code.

Function Annotations

Python also supports function annotations, which have no effect directly on the code they are in, but serve as a prompt to other programmers (and PyCharm if you use it) as to what the expected inputs and outputs of the functions are.

example-4.py from the beginning to the end
#!/usr/bin/env python3
# Showing examples of functions containing annotations 

def annotated_function(var1: int, var2: int, var3: str) -> int:
    """
    Example of an annotated function in python3:
    Args:
        var1 is obviously an int, cause thats what the annotation says
        var2 is the same... but it could have been any other basic type
        var3 is a string type, cause it was declared as one in the annotations. 

    Returns:
        returns an integer because the annotations said so... at least it's supposed to... riiiiight??
    """
    return(var3, (var1 * var2))  # What's wrong with this picture??



print(annotated_function(3.5, True, 'tiger' )) 

Example

As you can see we have a fully annotated, commented and docstringed function, the problem is that all of the annotations and the docstring doesn't tell you anything that is happening with the function. More importantly the information is wrong, our example doesn't return the type it's prompted/hinted to return.

When you write annotations for your functions, after each one of the variables passed into a function you can type a 'basic type prompt' or even a short message technically (although not in the PEP style guides). This will be ignored by the python interpreter at runtime and is only used to prompt other users as to what the programmer intended the functions for. This increases readability and makes it easier to understand code at a glance, but if the information isn't correct then there is nothing that python natively does to police that.

To write your own type hint return values, anything after the -> at the end of the function definition is the expected returns of running the function. For further guidance on other types of annotations see below. But you will mostly see them in functions so they are presented here.

For more information on annotations and good style practices, you can find it in the python style documentation and the PEP484 official guide to type hints. There is a great article on realpython that you should also read on annotations and ways to check annotations in code if you're interested.