Skip to content

Functions

Functions are the way in which we encapsulate code so that it can be reused multiple times. Sometimes this is in a single program and other times it's to include inside our own custom library.

Any time code needs to be executed or evaluated in the same way, it can also be passed into a function to have the same process done to it. By passing functions to functions we can build up any complex arrangement you can think of, and is how all modern programs are constructed.

Some functions inside python are built in functions that do a specific job or exhibit a specific behaviour, the print function is one for example. There are also ways to perform small inline functions in this manner. This will be covered in the built in functions section. Let's look at some of the reasons we use functions in more depth:

Scope

Scope is the technical term for if python can see a variable or function at any point in your program. As scope is one of those things that makes more sense when you see it, consider this example:

intro-example.py from 6 to 23
# into-example.py first example
# Displaying scope as a call to a variable 

variable_1 = 70  # this is in global scope

def add_two(number):
    result = number + 2  # this is local scope
    print(f"the square of {number} is: {result}")

add_two(variable_1)

try:
    print("testing functions...")
    # this isn't accessable from outside the square() function
    print(result)
    print(base)
except NameError as inst:
    print(inst)  # prints the error raised and what raised it
Output
the square of 70 is: 72
testing functions...
name 'result' is not defined

Example

In this example, you can see that we have created a variable named "variable_1" and we have created a function using the key word "def", for more information on creating functions you can find it here in the user functions section.

Knowing when a variable or another function is out of scope is very important in programming. We use scope specifically to allow us to avoid naming collisions, but it's also has unwanted side effects if you're not conscious of it. Consider this example:

intro-example.py from 25 to 43
# intro-example.py second example
# Showing scope limitations
outside_variable = 20  # this is in the global scope

def container_function():
    inside_variable = 120  # this is local to container_function
    # inside_variable is also global scope to inner_function
    def inner_function():
        inner_variable = 30  # can't be seen by the try statement
        print(inner_variable)
    inner_function()
    try:
        print(inside_variable)
        print(outside_variable)
        print(inner_variable)  # this fails as it's out of scope
    except NameError as inst:
        print(inst)

container_function()
Output
30
120
name 'outside_variable' is not defined

Example

In this example we are trying to use the internal variables of all of the functions. The program can only see the variables above it and inside the function that has the try functions. This is because the rules of scope in python search for things in a certain order, but must allow for isolation of functions so that names can be reused and variables don't get accidentally modified.

Python looks for variables and functions in its scope using the following rule...

The LEGB rule

LEGB stands for; local, enclosing, global and built-in in that order. Python when first looking for the location of the variable or function you have called looks in the local scope first. The local scope is ONLY within the function it is called in. Next it looks at the enclosing scope, if the function is in another function then it will look at the enclosing function (the outer function) to see if the variable is there. Next is global, global is best defined as variables that are outside of any other function, so far most of our examples have declared variables in the global scope. Finally it checks the built-ins such as print, more of these keywords can be found in the built-in functions section.

Namespaces

Namespaces is the technical term for these ares of scope. In python, every time you create a function in python you create a new namespace that encapsulates all of the information inside of it in a new object. In python there is no way to create namespaces other than through creating new objects, but in other programming languages you can specify namespaces directly so you should know the lingo. Using the in-built functions global() and local() you can show all of the functions in that namespace/scope.

Abstraction and reusability

To understand abstraction, you must first understand how we make code reusable. Consider this code:

intro-example.py from 46 to 55
# intro-example.py third example
# Showing bad coding through lack of reusable functions
integer_1 = 3
integer_2 = 9 
adding_two_numbers = 5 + 8 
total = integer_1 + integer_2 
adding_more_numbers = integer_1 + 7 
print(adding_two_numbers)
print(total)
print(adding_more_numbers)
Output
13
12
10

This code does the thing many times, but without functions its long to type and would be a pain if we needed to find every addition in a large code base say, if we needed to add 1 to every sum. Consider this function instead:

intro-example.py from 58 to 72
# intro-example.py fourth example
# functions for reusability 
def add_two_numbers(number_1, number_2):
    """
        A function that takes in two numbers.
        expected input: int or float
        expected output: print
    """
    print(number_1 + number_2)

integer_1 = 3
integer_2 = 9 
add_two_numbers(5, 8)
add_two_numbers(integer_1, integer_2)
add_two_numbers(integer_1, 7)
Output
13
12
10

Example

See how much cleaner that looks!! We can now modify one function and it will affect all the times it's been called by the rest of the program. We can also write another thousand lines of code and as soon as we need the addition again we can call on it again saving us time and energy. Because we named it something that makes sense it's very readable and easy to remember when we're using it later on in the development life cycle.

This kind of reusability is paramount and is also how we can keep code modular, more on that in a minute. The point is that we can take that reusable code now and use it all over the code with a simple call. But it also only adds two integers, could we make is even more helpful?? This is what abstraction is for, by making the code more abstract we can make it so that it takes in any number of integers and floats and adds them together. Consider this code:

intro-example.py from 75 to 93
# intro-example.py fifth example
# Function abstraction for greater use cases
def adding_numbers(*args):
    """
        A function that takes in two integers.
        expected input: int or float
        expected output: print
    """
    total = 0 
    for i in args:
        total += i 
    print(total)


integer_1 = 3
integer_2 = 9 
adding_numbers(5.4, 8.6)
adding_numbers(integer_1, integer_2)
adding_numbers(integer_1, 7, 11, 22)
Output
14.0
12
43

Note

Don't worry if much of this feels a little advanced for you right now, much of this is broken down in the later sections and explained in greater detail. It's only important that you understand that these principles exist and very roughly how they work at this stage.

Modularity

The term modular programming lends itself to the idea that each function contains all of the code required to do a single job, and then every time you need to do that job again the function is called. If you think about how a character in a computer game is controlled, every time the character turns left it must make a call to a turn left function in the code. The same if it needs to jump or run backwards, directional animations and everything that makes a game run, all could be their own independent functions containing all the code to do each specific job. Consider this pseudo code:

    # outline of a modular program
    def open_file():
        <code that opens files>
        <code that opens files>
        <code that opens files>

    def process_file_data():
        <code that reads files>
        <code that reads files>
        <code that reads files>

    def close_file():
        <code that closes files>
        <code that closes files>
        <code that closes files>

    open_file()
    process_file_data()
    close_file()

In this pseudo code, you can see that we have three functions all doing different jobs. Each one of the functions can contain code for the testing and for completing the steps required to do the job for which its named. In this way, you create modular code that can be used later in the program or exported into other projects, because you can call the function into another file. You can find more information on importing modules and taking code from other files in the modules section here.