Chapter 11: Functions and scope

We have seen that Python has several built-in functions (e.g. print() or max()). But you can also create your own function. A function is a reusable block of code that performs a specific task. Once you have defined a function, you can use it at any place in your Python script. You can even import a function from an external module (as we will see in the next chapter). Therefore, they are very useful for tasks that you will perform more often. Plus, functions are a convenient way to order your code and make it more readable!

At the end of this chapter, you will be able to:

  • write your own function
  • work with function inputs
  • understand the difference between (keyword and positional) arguments and parameters
  • return zero, one, or multiple values
  • write function docstrings
  • understand scope of variables

Acknowledgements:

We use an example from this website to show you some of the basics of writing a function. We use some materials from thisother Python course.

Now let's get started!

If you have questions about this chapter, please refer to the forum on Canvas.

1. Writing a function

A function is an isolated chunk of code, that has a name, gets zero or more parameters, and returns a value. In general, a function will do something for you, based on a number of input parameters you pass it, and it will typically return a result. You are not limited to using functions available in the standard library or the ones provided by external parties. You can also write your own functions!

Whenever you are writing a function, you need to think of the following things:

  • What is the purpose of the function?
  • How should I name the function?
  • What input does the function need?
  • What output should the function generate?

1.1. Why use a function?

There are several good reasons why functions are a key component of any non-ridiculous programmer:

  • encapsulation: wrapping a piece of useful code into a function so that it can be used without knowledge of the specifics
  • generalization: making a piece of code useful in varied circumstances through parameters
  • manageability: Dividing a complex program up into easy-to-manage chunks
  • maintainability: using meaningful names to make the program better readable and understandable
  • reusability: a good function may be useful in multiple programs
  • recursion!

1.2. How to define a function

Let's say we want to sing a birthday song to Emily. Then we print the following lines:


In [ ]:
print("Happy Birthday to you!")
print("Happy Birthday to you!")
print("Happy Birthday, dear Emily.")
print("Happy Birthday to you!")

This could be the purpose of a function: to print the lines of a birthday song for Emily. Now, we define a function to do this. Here is how you define a function:

  • write def;
  • the name you would like to call your function;
  • a set of parentheses containing the argument(s) of your function;
  • a colon;
  • a docstring describing what your function does;
  • the function definition;
  • ending with a return statement

Statements must be indented, so that Python knows what belongs in the function and what not. Functions are only executed when you call them. It is good practice to define your functions at the top of your program.

We give the function a clear name, happy_birthday_to_emily and we define the function as shown below. Note that we specify exactly what it does in the docstring in the beginning of the function:


In [ ]:
def happy_birthday_to_emily(): # Function definition
    """
    Print a birthday song to Emily.
    """
    print("Happy Birthday to you!")
    print("Happy Birthday to you!")
    print("Happy Birthday, dear Emily.")
    print("Happy Birthday to you!")

If we execute the code above, we don't get any output. That's because we only told Python: "Here's a function to do this, please remember it." If we actually want Python to execute everything inside this function, we have to call it:

1.3 How to call a function

It is important to distinguish between a function definition and a function call. We illustrate this in 1.3.1. You can also call functions from within other functions. This will become useful when you split up your code into small chunks that can be combined to solve a larger problem. This is illustrated in 1.3.2.

1.3.1) A simple function call

A function is defined once. After the definition, Python has remembered what this function does in its memory. A function is executed/called as many times as we like. When calling a function, you should always use parenthesis.


In [ ]:
# function definition:

def happy_birthday_to_emily(): # Function definition
    """
    Print a birthday song to Emily.
    """
    print("Happy Birthday to you!")
    print("Happy Birthday to you!")
    print("Happy Birthday, dear Emily.")
    print("Happy Birthday to you!")
    
# function call:

print('Function call 1')

happy_birthday_to_emily()

print()
# We can call the function as many times as we want (but we define it only once)
print('Function call 2')

happy_birthday_to_emily()

print()

print('Function call 3')

happy_birthday_to_emily()

print()
# This will not call the function 
#(without the parenthesis, Python thinks that `happy_birthday_to_emily` is a variable and not a function!):

print('This is not a function call')
happy_birthday_to_emily

1.3.2 Calling a function from within another function

We can also define functions that call other functions, which is very helpful if we want to split our task, in smaller, more manageable subtasks:


In [ ]:
def new_line():
    """Print a new line."""
    print()

def two_new_lines():
    """Print two new lines."""
    new_line()
    new_line()

print("Printing a single line...")
new_line()
print("Printing two lines...")
two_new_lines()
print("Printed two lines")

You can do the same tricks that we learnt to apply on the built-in functions, like asking for help or for a function type:


In [ ]:
help(happy_birthday_to_emily)

In [ ]:
type(happy_birthday_to_emily)

The help we get on a function will become more interesting once we learn about function inputs and outputs ;-)

1.4 Working with function input

1.4.1 Parameters and arguments

We use parameters and arguments to make a function execute a task depending on the input we provide. For instance, we can change the function above so we can input the name of a person and print a birthday song using this name. This results in a more generic function.

To understand how we use parameters and arguments, keep in mind the distinction between function definition and function call.

Parameter: The variable name in the function definition below is a parameter. Variables used in function definitions are called parameters.

Argument: The variable my_name in the function call below a value for the parameter name at the time when the function is called. We denote such variables arguments. We use arguments so we can direct the function to do different kinds of work when we call it at different times.


In [ ]:
# function definition with using the parameter `name'
def happy_birthday(name): 
    """
    Print a birthday song with the "name" of the person inserted.
    """
    print("Happy Birthday to you!")
    print("Happy Birthday to you!")
    print("Happy Birthday, dear " + name + ".")
    print("Happy Birthday to you!")

In [ ]:
# function call using specifying the value of the argument
happy_birthday("James")

We can also store the name in a variable:


In [ ]:
my_name="James"
happy_birthday(my_name)

If we forgt to specify the name, we get an error:


In [ ]:
happy_birthday()

Functions can have multiple parameters. We can for example multiply two numbers in a function (using the two parameters x and y) and then call the function by giving it two arguments:


In [ ]:
def multiply(x, y):
    """Multiply two numeric values."""
    result = x * y
    print(result)
       
multiply(2020,5278238)
multiply(2,3)

1.4.2 Positional vs keyword parameters and arguments

The function definition tells Python which parameters are positional and which are keyword. As you might remember, positional means that you have to give an argument for that parameter; keyword means that you can give an argument value, but this is not necessary because there is a default value.

So, to summarize these two notes, we distinguish between the following four categories:

1) positional parameters: (we indicate these when defining a function, and they are compulsory when calling the function)

2) keyword parameters: (we indicate these when defining a function, but they have a default value - and are optional when calling the function)

3) positional arguments: (we MUST specify these when calling a function)

4) keyword arguments: (we CAN specify these when calling a function)

To summarize:

parameters arguments
positional indicated in definition compulsory in call
keyword indicated in definition optional in call

For example, if we want to have a function that can either multiply two or three numbers, we can make the third parameter a keyword parameter with a default of 1 (remember that any number multiplied with 1 results in that number):


In [ ]:
def multiply(x, y, third_number=1): # x and y are positional parameters, third_number is a keyword parameter
    """Multiply two or three numbers and print the result."""
    result=x*y*third_number
    print(result)

In [ ]:
multiply(2,3) # We only specify the positional arguments
multiply(2,3,third_number=4) # We specify both the positional arguments, and the keyword argument

If we do not specify a positional argument, the function call will fail (with a very helpful error message):


In [ ]:
multiply(3)

1.5 Output: the return statement

Functions can have a return statement. The return statement returns a value back to the caller and always ends the execution of the function. This also allows us to use the result of a function outside of that function by assigning it to a variable:


In [ ]:
def multiply(x, y):
    """Multiply two numbers and return the result."""
    result = x * y
    return result

#here we assign the returned value to variable z
result = multiply(2, 5)

print(result)

We can also print the result directly (without assigning it to a varible), which gives us the same effect as using the print statements we used before:


In [ ]:
print(multiply(30,20))

If we assign the result to a variable, but do not use the return statement, the function cannot return it. Instead, it returns None (as you can try out below).

This is important to realize: even functions without a return statement do return a value, albeit a rather boring one. This value is called None (it’s a built-in name). You have seen this already with list methods - for example list.append(val) adds a value to a list, but does not return anything explicitly.


In [ ]:
def multiply_no_return(x, y):
    """Multiply two numbers and does not return the result."""
    result = x * y
    
is_this_a_result = multiply_no_return(2,3)
print(is_this_a_result)

Returning multiple values

Similarly as the input, a function can also return multiple values as output. We call such a collection of values a tuple (does this term sound familiar ;-)?).


In [ ]:
def calculate(x,y):
    """Calculate product and sum of two numbers."""
    product = x * y
    summed = x + y
    
    #we return a tuple of values
    return product, summed

# the function returned a tuple and we unpack it to var1 and var2
var1, var2 = calculate(10,5)

print("product:",var1,"sum:",var2)

Make sure you actually save your 2 values into 2 variables, or else you end up with errors or unexpected behavior:


In [ ]:
#this will assign `var` to a tuple:
var = calculate(10,5)
print(var)

#this will generate an error
var1,var2,var3 = calculate(10,5)

Saving the resulting values in different variables can be useful when you want to use them in different places in your code:


In [ ]:
def sum_and_diff_len_strings(string1, string2):
    """
    Return the sum of and difference between the lengths of two strings.
    """
    sum_strings = len(string1) + len(string2)
    diff_strings = len(string1) - len(string2)
    return sum_strings, diff_strings

sum_strings, diff_strings = sum_and_diff_len_strings("horse", "dog")
print("Sum:", sum_strings)
print("Difference:", diff_strings)

1.6 Documenting your functions with docstrings

Docstring is a string that occurs as the first statement in a function definition.

For consistency, always use """triple double quotes""" around docstrings. Triple quotes are used even though the string fits on one line. This makes it easy to later expand it.

There's no blank line either before or after the docstring.

The docstring is a phrase ending in a period. It prescribes the function or method's effect as a command ("Do this", "Return that"), not as a description; e.g. don't write "Returns the pathname ...".

In practice, there are several formats for writing docstrings, and all of them contain more information than the single sentence description we mention here. Probably the most well-known format is reStructured Text. Here is an example of a function description in reStructured Text (reST):


In [ ]:
def my_function(param1, param2):
    """
    This is a reST style.

    :param param1: this is a first param
    :param param2: this is a second param
    :returns: this is a description of what is returned
    :raises keyError: raises an exception
    """
    return

You can see that this docstring describes the function goal, its parameters, its outputs, and the errors it raises.

It is a good practice to write a docstring for your functions, so we will always do this! For now we will stick with single-sentence docstrings

You can read more about this topic here, here, and here.

1.7 Conditioning the function output

So far we have seen functions that follow a single thread of code (always return the same thing). We can also condition the return value of the function by using if-else statements. For instance, let's have a function that prints "even" or "odd" depending on the input number. If the number is zero, then the function prints "zero".


In [ ]:
def even_or_odd(p):
    """Check whether a number is even or odd."""
    if p==0:
        return "zero"
    if p%2 ==1: # odd number
        return "odd"
    else: # even number
        return "even"

num = int(input("Please enter a number> "))
print(even_or_odd(num))

1.8 Boolean functions

We can also define boolean functions, i.e. functions that return a boolean value: either True or False. We can directly plug such a function into an if statement. Let's consider again the case where we want to check if a number is even or odd.


In [ ]:
def is_even(p):
    """Check whether a number is even."""
    if p%2 ==1:
        return False
    else:
        return True

num = int(input("Please enter a number> "))
if is_even(num):
    print(num, "is even")
else:
    print(num, "is odd")

2. Variable scope

Any variables you declare in a function, as well as the parameters that are passed to a function will only exist within the scope of that function, i.e. inside the function itself. The following code will produce an error, because the variable x does not exist outside of the function:


In [ ]:
def setx():
    """Set the value of a variable to 1."""
    x = 1
    

setx()
print(x)

Even when we return x, it does not exist outside of the function:


In [ ]:
def setx():
    """Set the value of a variable to 1."""
    x = 1
    return x
    
setx()
print(x)

Also consider this:


In [ ]:
x = 0
def setx():
    """Set the value of a variable to 1."""
    x = 1
setx()
print(x)

In fact, this code has produced two completely unrelated x's!

So, you can not read a local variable outside of the local context. Nevertheless, it is possible to read a global variable from within a function, in a strictly read-only fashion. But as soon as you assign something, the variable will be a local copy:


In [ ]:
x = 1
def getx():
    """Print the value of a variable x."""
    print(x)
    
getx()

You can use two built-in functions in Python when you are unsure whether a variable is local or global. The function locals() returns a list of all local variables, and the function globals() - a list of all global variables. Note that there are many non-interesting system variables that these functions return, so in practice it is best to check for membership with the in operator. For example:


In [ ]:
a=3
b=2

def setb():
    """Set the value of a variable b to 11."""
    b=11
    c=20
    print("Is 'a' defined locally in the function:", 'a' in locals())
    print("Is 'b' defined locally in the function:", 'b' in locals())
    
setb()

print("Is 'a' defined globally:", 'a' in globals())
print("Is 'b' defined globally:", 'b' in globals())

print("Is 'c' defined globally:", 'c' in globals())

Finally, note that the local context stays local to the function, and is not shared even with other functions called within a function, for example:


In [ ]:
def setb_again():
    """Set the value of a variable to 3."""
    b=3
    print("in 'setb_again' b =", b)

def setb():
    """Set the value of a variable b to 2."""
    b=2
    setb_again()
    print("in 'setb' b =", b)
b=1
setb()
print("global b =", b)

We call the function setb() from the global context, and we call the function setb_again() from the context of the function setb(). The variable b in the function setb_again() is set to 3, but this does not affect the value of this variable in the function setb() which is still 2. And as we saw before, the changes in setb() do not influence the value of the global variable (b=1).

Exercises

Exercise 1:

Write a function that converts meters to centimeters and prints the resulting value.


In [ ]:
# you code here

Exercise 2:

Add another keyword parameter message to the multiply function, which will allow a user to print a message. The default value of this keyword parameter should be an empty string. Test this with 2 messages of your choice. Also test it without specifying a value for the keyword argument when calling a function.


In [ ]:
# function to modify:

def multiply(x, y, third_number=1): 
    """Multiply two or three numbers and print the result."""
    result=x*y*third_number
    print(result)

Exercise 3:

Write a function called multiple_new_lines which takes as argument an integer and prints that many newlines by calling the function newLine.


In [ ]:
def new_line():
    """Print a new line."""
    print()
    
# you code here

Exercise 4:

Let's refactor the happy birthday function to have no repetition. Note that previously we print "Happy birthday to you!" three times. Make another function happy_birthday_to_you() that only prints this line and call it inside the function happy_birthday(name).


In [ ]:
def happy_birthday_to_you():
    # your code here

# original function - replace the print statements by the happy_birthday_to_you() function:
def happy_birthday(name): 
    """
    Print a birthday song with the "name" of the person inserted.
    """
    print("Happy Birthday to you!")
    print("Happy Birthday to you!")
    print("Happy Birthday, dear " + name + ".")
    print("Happy Birthday to you!")

Exercise 5:

Try to figure out what is going on in the following examples. How does Python deal with the order of calling functions?


In [ ]:
def multiply(x, y, third_number=1): 
    """Multiply two or three numbers and print the result."""
    result=x*y*third_number
    
    return result
    
print(multiply(1+1,6-2))
print(multiply(multiply(4,2),multiply(2,5)))
print(len(str(multiply(10,100))))

Exercise 6:

Complete this code to switch the values of two variables:


In [ ]:
def switch_two_values(x,y):
# your code here
    
a='orange'
b='apple'

a,b = switch_two_values(a,b) # `a` should contain "apple" after this call, and `b` should contain "orange"

print(a,b)

In [ ]: