Writing functions

In previous sections you edited a script that encodes messages to Morse code. The script is good, but is not very easy to use or reusable. For someone to make use of the script, they will have to edit it and copy and paste your code every time they want to encode a different message.

Functions provide a way of packaging code into reusable and easy-to-use components. We saw plenty of examples of functions in the last chapter, e.g. print() wraps up all the logic about exactly how to print things, all you need to do is pass in some arguments and it handles the rest. Likewise with math.sqrt(), you don’t need to understand the algorithm it uses, simply what it needs you to pass it, and what it returns back to you.

You can also bundle up your own logic into functions, allowing you to avoid repeating yourself and make your code easier to read. To explain how they work, lets imagine we are writing some code to help us with baking recipes. Often you will need to convert between different units, for example from ounces to grams. Create a new script convert.py with the below code and run it.

convert.py
weight_in_ounces = 6

weight_in_grams = weight_in_ounces * 28.3495

print(f"{weight_in_grams} g")
170.09699999999998 g

You can see this script has three main parts:

The data processing section will work regardless of what data is inside the variable weight_in_ounces and so we can grab that bit of code and make it usable in other contexts quite easily, using functions.

Defining functions

We can turn this into a function that can convert ounces to grams by using def. To do this, type:

convert.py
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

This has created a new function called ounces_to_grams which we can now call. In a similar fashion to other constructs in Python (like for loops and if statements) it has a rigid structure.

First we must use the def keyword to start a function definition:

def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

Then we specify the name that we want to give the function. This is the name which we will use when calling the function. Like anything in Python, choose a descriptive name that describes what it does:

           ↓
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

Function definitions must then be followed by a pair of round brackets. This is a similar syntax to that used when calling a function and giving it arguments but here we’re just defining it. Between those brackets go the names of the parameters we want the function to accept (can be zero or more parameters). Here we are defining one:

                   ↓      ↓
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

Finally, the line is completed with a colon. And since we’ve used a colon, we must indent the body of the function as we did with loops and conditional statements.

                           ↓
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

Most functions will also want to return data back to the code that called it. You can choose what data is returned using the return keyword followed by the data you want to return:

def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight
      ↑

Note that the body of the function has been copied from our script above with the only change being that the variables have different names and we added a return statement.

Calling functions

You can now call the function using:

convert.py
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

weight_in_ounces = 6

weight_in_grams = ounces_to_grams(weight_in_ounces)

print(f"{weight_in_grams} g")
170.09699999999998 g

In this case you have called the function ounces_to_grams and passed in the argument weight_in_ounces. In the fuction, weight_in_ounces is copied to its internal variable, weight. The function ounces_to_grams then acts on weight, creating the new varaible new_weight. It then returns new_weight, which is assigned to weight_in_grams.

You can use your new ounces_to_grams function to convert any numbers. Try typing:

convert.py
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

weight_in_ounces = 999

weight_in_grams = ounces_to_grams(weight_in_ounces)

print(f"{weight_in_grams} g")
28321.1505 g

Note that we can also pass the values to the function directly, e.g. type:

convert.py
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

weight_in_grams = ounces_to_grams(12)

print(f"{weight_in_grams} g")
340.19399999999996 g
Exercise

Take the following code:

my_num = 10

doubled = my_num * 2

print(doubled)

and convert the multiplication part to a function called double which can be called like:

doubled = double(my_num)
doubler.py
def double(num):
    return num * 2

my_num = 10

doubled = double(my_num)

print(doubled)
Terminal/Command Prompt
python doubler.py
20
Exercise

Take the following code:

my_list = [5, 7, 34, 5, 3, 545]

big_numbers = []
for num in my_list:
    if num > 10:
        big_numbers.append(num)

print(big_numbers)

and convert the data-processing parts to a function called big which can be called like:

my_list = [5, 7, 34, 5, 3, 545]

large_numbers = big(my_list)

print(large_numbers)

giving

[34, 545]

Be careful to pay attention to the indentation, ensuring that it is consistent with the original code. Particularly, note that the return statement will cause the function to exit, so make sure that it doesn’t run until after the loop has finished.

Starting from the initial code:

bignums.py
my_list = [5, 7, 34, 5, 3, 545]

big_numbers = []
for num in my_list:
    if num > 10:
        big_numbers.append(num)

print(big_numbers)
[34, 545]

We pull out the middle four lines, indent them, put def big(numbers): in front and add return big_numbers to the end, paying careful attention to the indentation of the return statement. Finally, we update the variable name used in the function to match the argument name numbers:

bignums.py
def big(numbers):
    big_numbers = []
    for num in numbers:
        if num > 10:
            big_numbers.append(num)
    return big_numbers


my_list = [5, 7, 34, 5, 3, 545]

large_numbers = big(my_list)

print(large_numbers)
[34, 545]

How many arguments?

Note that you must pass in the right number of arguments to a function. ounces_to_grams expects one arguments, so if you pass more or less you will get an error. Try this now:

convert.py
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

weight_in_grams = ounces_to_grams()  # We've removed the arguments to this function

print(f"{weight_in_grams} g")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 5
      2     new_weight = weight * 28.3495
      3     return new_weight
----> 5 weight_in_grams = ounces_to_grams()  # We've removed the arguments to this function
      7 print(f"{weight_in_grams} g")

TypeError: ounces_to_grams() missing 1 required positional argument: 'weight'

As you can see, Python tells you that you’ve given the function a wrong number of arguments. It expects 1 (weight). Likewise, if you give too many arguments you get a similar error:

convert.py
def ounces_to_grams(weight):
    new_weight = weight * 28.3495
    return new_weight

weight_in_grams = ounces_to_grams(12, 10)  # We've passed too many arguments now

print(f"{weight_in_grams} g")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[11], line 5
      2     new_weight = weight * 28.3495
      3     return new_weight
----> 5 weight_in_grams = ounces_to_grams(12, 10)  # We've passed too many arguments now
      7 print(f"{weight_in_grams} g")

TypeError: ounces_to_grams() takes 1 positional argument but 2 were given

It is possible to define functions that take no arguments:

def pi():
    return 3.14159

answer = pi()

single arguments:

def double(x):
    return x * 2

answer = double(4)

or lots of arguments:

def lots_of_args(a, b, c, d, e):
    return {"a": a, "b": b, "c": c, "d": d, "e": e}

answer = lots_of_args(1, 2, 3, 4, 5)
Exercise

Take encode.py from the previous chapter and edit it so that the part that does the conversion is moved into a function called encode. The function should take one argument and return the encoded morse string.

To be more explicit, replace the following lines of code:

morse = []

for letter in message:
    letter = letter.lower()
    morse_letter = letter_to_morse[letter]
    morse.append(morse_letter)

morse_message = " ".join(morse)

with:

def encode(message):
    ...
    
    return ...

morse_message = encode(message)

where the ... should be replaced with the code to do the conversion and the variable to be returned.

We add a new function called encode which takes the exact code as before but with three changes:

  1. def encode(message): added to the beginning
  2. The code is indented by four spaces
  3. return morse_message added to the end

We then call our function with morse_message = encode(message)

encode.py
letter_to_morse = {'a':'.-', 'b':'-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
                   'g':'--.', 'h':'....', 'i':'..', 'j':'.---', 'k':'-.-', 'l':'.-..', 'm':'--', 
                   'n':'-.', 'o':'---', 'p':'.--.', 'q':'--.-', 'r':'.-.', 's':'...', 't':'-',
                   'u':'..-', 'v':'...-', 'w':'.--', 'x':'-..-', 'y':'-.--', 'z':'--..',
                   '0':'-----', '1':'.----', '2':'..---', '3':'...--', '4':'....-',
                   '5':'.....', '6':'-....', '7':'--...', '8':'---..', '9':'----.',
                   ' ':'/'}

message = "SOS We have hit an iceberg and need help quickly"


def encode(message):
    morse = []

    for letter in message:
        letter = letter.lower()
        morse_letter = letter_to_morse[letter]
        morse.append(morse_letter)

    morse_message = " ".join(morse)
    
    return morse_message


morse_message = encode(message)

print(f"Incoming message: {message}")
print(f"   Morse encoded: {morse_message}")
Incoming message: SOS We have hit an iceberg and need help quickly
   Morse encoded: ... --- ... / .-- . / .... .- ...- . / .... .. - / .- -. / .. -.-. . -... . .-. --. / .- -. -.. / -. . . -.. / .... . .-.. .--. / --.- ..- .. -.-. -.- .-.. -.--
Exercise
  • Make a second file called decode.py with the contents:

    letter_to_morse = {
        'a':'.-', 'b':'-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
        'g':'--.', 'h':'....', 'i':'..', 'j':'.---', 'k':'-.-', 'l':'.-..', 'm':'--', 
        'n':'-.', 'o':'---', 'p':'.--.', 'q':'--.-', 'r':'.-.', 's':'...', 't':'-',
        'u':'..-', 'v':'...-', 'w':'.--', 'x':'-..-', 'y':'-.--', 'z':'--..',
        '0':'-----', '1':'.----', '2':'..---', '3':'...--', '4':'....-',
        '5':'.....', '6':'-....', '7':'--...', '8':'---..', '9':'----.', ' ':'/'
    }
    
    # We need to invert the dictionary. This will create a dictionary
    # that can go from the morse back to the letter
    morse_to_letter = {}
    for letter in letter_to_morse:
        morse = letter_to_morse[letter]
        morse_to_letter[morse] = letter
    
    message = "... --- ... / .-- . / .... .- ...- . / .... .. - / .- -. / .. -.-. . -... . .-. --. / .- -. -.. / -. . . -.. / .... . .-.. .--. / --.- ..- .. -.-. -.- .-.. -.--"
    
    english = []
    
    # Now we cannot read by letter. We know that morse letters are
    # separated by a space, so we split the morse string by spaces
    morse_letters = message.split(" ")
    
    for letter in morse_letters:
        english.append(morse_to_letter[letter])
    
    # Rejoin, but now we don't need to add any spaces
    english_message = "".join(english)
    
    print(english_message)
  • Edit decode.py so that the part that does the conversion (everything from english = [] to "".join(english)) is moved into a function. The function should take one argument, message and return the decoded english message.

Here we make the same changes as with encode.py:

  1. def decode(message): added to the beginning
  2. The code is indented by four spaces
  3. return english_message added to the end
  4. Call the function
decode.py
letter_to_morse = {
    'a':'.-', 'b':'-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
    'g':'--.', 'h':'....', 'i':'..', 'j':'.---', 'k':'-.-', 'l':'.-..', 'm':'--', 
    'n':'-.', 'o':'---', 'p':'.--.', 'q':'--.-', 'r':'.-.', 's':'...', 't':'-',
    'u':'..-', 'v':'...-', 'w':'.--', 'x':'-..-', 'y':'-.--', 'z':'--..',
    '0':'-----', '1':'.----', '2':'..---', '3':'...--', '4':'....-',
    '5':'.....', '6':'-....', '7':'--...', '8':'---..', '9':'----.', ' ':'/'
}

# We need to invert the dictionary. This will create a dictionary
# that can go from the morse back to the letter
morse_to_letter = {}
for letter in letter_to_morse:
    morse = letter_to_morse[letter]
    morse_to_letter[morse] = letter

message = "... --- ... / .-- . / .... .- ...- . / .... .. - / .- -. / .. -.-. . -... . .-. --. / .- -. -.. / -. . . -.. / .... . .-.. .--. / --.- ..- .. -.-. -.- .-.. -.--"


def decode(message):
    english = []

    # Now we cannot read by letter. We know that morse letters are
    # separated by a space, so we split the morse string by spaces
    morse_letters = message.split(" ")

    for letter in morse_letters:
        english.append(morse_to_letter[letter])

    # Rejoin, but now we don't need to add any spaces
    english_message = "".join(english)
    
    return english_message


print(decode(message))
sos we have hit an iceberg and need help quickly