Functions

Function videos

Anatomy of a Function

_images/functions_anatomy.png

Calling a function

Up to this point, you are familiar with coding a single program. In reality, a program is made up of smaller sub-programs. We call those sub-programs functions. If you look closely at the following code, you will notice you have been using functions the whole time:

1name = input("Please enter your name: ")
2message = f"Hello, {name}. Nice to meet you."
3print(message)

On line #1, we are calling the input function, and on line #2, we are calling the print function. Any time you see some name with parentheses following it some_name(), it is most likely a function.

_images/functions_coffee.png

Calling a function is like having someone get something for you. Hopefully they bring back what you were asking for.

The input and print functions were written by someone else and Python automatically includes them as built-in functions. We can also define our own functions that we can use. In fact, when working as a software developer, the majority of the code you write will be in functions. We will see how to define custom functions a little later.

_images/function_not_running.png

A function will not run unless you call it.

Passing arguments

Most functions require some information so they can do their job. Take a look at the function below.

1def add(a: int, b: int) -> int:
2    return a + b

In the function definition on line #1, in the brackets, the defined function tells us it needs two integers to do its job. Therefore, when we call the funciton we need to give it those arguments. In this case, two integers.

add(5, 7)

Each individual argument is separated by a comma. Arguments can be of any data-type. The function definition tells us exactly how many arguments and what datatypes.

_images/functions_passing_arguments.png

Calling add(5, 7) will place a 5 in box ‘a’ and a 7 into box ‘b’.

Here is another function definition with different datatypes.

def repeat_message(message: str, times: int) -> str:
    pass

It defines two required arguments: a message of type str and the number of times (of type int) to repeat the message. To call repeat_message we need to supply a str and an int as arguments.

repeat_message("hello", 5)
_images/functions_missing_argument.png

If you don’t pass the required arguments, Python gets upset.

Handling return values

Most of the time when writing and using functions, you want the function to go off, do some work for you and return with some result.

Imagine you have a friend (some function) that will do your homework for you. You give it the specific questions you need to complete (arguments) and then wait for them to finish it.

Now imagine, they look at the questions and just yelled out the answers wherever they were. Aside from that being hilarious, it would be useless to you since you would not have anything to turn in to your teacher. This is like using print in your functions. A lot of beginner programmers love to use print for everything. Sometimes you can use print in functions, but moving forward, I would like to formally discourage using print in functions (outside of debugging or showing a user interface like printing menu options).

When your friend (the function) completes your homework, it is critical that they give it back to you so you can accomplish the next step of turning it in. If this situation were code, it could potentially look like.

1homework_subject = "math"
2homework_chapter = "chapter 1"
3completed_work = get_friend_to_do_your_homework(homework_subject, homework_chapter)
4hand_in(completed_work)

Line #3: call the function get_friend_to_do_your_homework and give it the subject and chapter arguments. The function will go off and do its job, then return the completed homework. Your program will then store that completed homework in a variable called completed_work.

Line #4: Calls the function hand_in and gives it the completed_work.

In a more practical example, line 9 below calls a function to calculate the tax and stores the result in a variable called tax.

 1def calc_tax(amount: float) -> float:
 2    TAX_RATE = 0.13
 3    return amount * TAX_RATE
 4
 5
 6cost = 6.99
 7quantity = 3
 8subtotal = cost * quantity
 9tax = calc_tax(subtotal)
10total = subtotal + tax

Most of the time the pattern will be like that.

def some_function():
    return "some return value"


result = some_function()

Call the function, get the result and decide what to do with the result outside of the function.

Note

Why not print things from within the functions themselves?

One person using the function might want to print out the result right away. Someone else might want to do some further processing on the result before printing it out. We want to have our functions do only the bare minimum (i.e., just calculate a value rather than also printing it out) to prevent any undesired side-effects. At best, other developers will not want to use our functions, at worse they will curse us because they will have to modify/rewrite them.

_images/functions_handling_return_value.png

If you call a function that returns a value, you should receive it and store it in a variable.

Defining a Function

Defining a function is much like initializing a variable. Instead of storing a number or a string like in a variable, in a function we store a whole bunch of code.

What we need (at a minimum):

  • a function name

  • some code to save in the function.

def function_name():
   print("Some code saved in a function")

Every line of code that is stored in the function is indented underneath the function header starting with def. Recall that, in order to actually run the function’s code, we need to call the function.

Returning a value

When a function gives back some result. See line 2:

1def add(a: int, b: int) -> int:
2    return a + b

I like to think of functions as a worker or a friend you know, who can get a specific job done for you. Imagine an intelligence agency needs someone to go and search the database for all mentions of a praticular person of interest.

Intelligence Director: I need the full profiles of everyone matching the last name McGee. I will wait here patiently until you come back with that list. Hurry up!

What are they looking for and what information are they providing? They are looking for “profiles” and they are poviding a last name, “McGee” to be exact. In code, this could look like:

full_profiles = get_profiles_with_name("McGee")

The intelligence director would be Calling a function (get_profiles_with_name) and functions:passing an argument ("McGee").

Low-level Intelligence Agency Data-gatherer: Yes, ma’am! I will take this name and get to work!

The low-level intelligence agency data-gatherer was trained to return a list of profiles when someone provided a name. The function definition for this data-gatherer would look like:

def get_profiles_with_name(name: str) -> List[Profile]:
    # some code
    # processing....
    # still processing....
    return list_of_profiles

Notice the last thing the data-gatherer does is return the result they were trained to do.

Low-level Intelligence Agency Data-gatherer: Here is the folder full of the profiles you requested.

Intelligence Director: Thank you for these profiles. Now I must sort through them and come up with a candidate for my boss.

The flow of information would look like:

Warning

Under construction

Defining parameters

Some functions require no outside information in order to run. This can be slightly restrictive:

def move_ten_meters():
   """The robot moves ten meters""".
   pass


move_ten_meters()

What if you wanted the robot to move 15 meters? Would you create a move_fifteen_meters function? More useful functions require some outside information to run. We can define functions with certain parameters. In the case below, we are defining a move function to have a meters parameter:

 1def move(meters: float):
 2   """The robot will move the provided number of meters.
 3
 4   Args:
 5      meters: The number of meters you want the robot to move.
 6   """
 7   print(f"Moving robot {meters} meters.")
 8   # more code actually moving the robot
 9   # ...
10
11move(15)

Notice how the function has access to the value (15) that was passed to it. It accesses the value (on line #7) through the parameter-variable named meters.

Type annotations

Below is a list of common types to use when annotating Python Functions. Some need to be imported from the Python typing library:

from typing import List, Tuple, Dict, Optional


some_integer: int
some_string: str
some_float: float
list_of_integers: List[int]
tuple_with_an_int_and_a_string: Tuple[int, str]
two_dimensional_list_of_integers: List[List[int]]
either_an_integer_or_none: Optional[int] = None

Docstrings

A docstring is a multi-line comment to explain in a very concise manner the purpose of a function and how to use it.

To use a funciton you need to know:

  • its name

  • what arguments to pass to it

  • what it will return back to you

Have a look at the basic format for docstrings:

def function_with_pep484_type_annotations(param1: int, param2: str) -> bool:
    """Example function with PEP 484 type annotations.

    Args:
       param1: The first parameter.
       param2: The second parameter.

    Returns:
       The return value. True for success, False otherwise.
    """
    pass

For docstrings pertaining to Classes, see Docstrings (Classes).