2. Functions#

In this notebook, we cover the following subjects:

  • Function Calls;

  • Creating Functions;

  • Functions in Functions.


# To enable us to state if the return typehint can take multiple datatypes, we need to import the following:
# Moreover, to use the correct typehint for list we need to import the following:

from typing import Union, List
# Union[int, float] means that type can be either int or float.

2.1. Function calls#

Functions let us execute processes multiple times without repeating the code. Unlike iterative loops (discussed in notebooks 4 and 5), we can call the function whenever we want to run a specific sequence of code. Plus, we can use parameters to run the same process with different values for certain variables.

Before we will construct our own functions, we touch upon function calls first. Luckily, these aren’t new to you, and you have already used them multiple times by now. A function call is when you run a function in your code, for example:

type("animal")
str

In this example, the name of the function is type. The expression in parentheses is called the argument of the function. The result, for this function, is the type of the argument.

It is common to say that a function “takes” an argument and “returns” a result. The result is called the return value.

2.2. Built in Functions#

Python’s built-in functions are a set of functions that are always available for you to use without needing to import any additional modules (we will cover modules in a later notebook, you do not have to worry about them right now). They provide a variety of useful functionalities, making it easier to perform common tasks without writing extra code.

Let’s go through some of the most common built-in functions and see how they work:

2.2.1. The print() Function#

Purpose: As we know already, the print() function is used to display information on the screen.

Example:

print('Hello, World!')

Output:

'Hello, World!'

2.2.2. The type() Function#

Purpose: Again, as we know already, the type() function is used to find the type of an object (e.g., string, integer, float, etc.)

Example:

number: int = 5
print(type(number))

Output:

"<class 'int'>"

2.2.3. The len() Function#

Purpose: The len() function returns the number of items in an object. When the object is a string, it returns the number of characters in the string. It only works for iterable objects, and not integers or floats etc.

Example:

word: str = "Python"
print(len(word))

Output:

6

2.2.4. The input() Function#

Purpose: The input() function is used to take input from the user. It pauses the program and waits for the user to type something.

Example:

name = input("Enter your name: ")
print("Hello, " + name + "!")

Output:

"Hello, Bob!"

2.2.5. The int(), float(), str() Functions#

Purpose:: These functions convert values to different data types.

  • int() converts to an integer.

  • float() converts to a floating-point number (a number with a decimal).

  • str() converts to a string.

Example:

number: str = "10"

print(int(number) + 5)
print(float(number) + 0.5)
print(str(100))

Output:

15 # Converts "10" to integer 10  
10.5 # Converts "10" to float 10.0 and adds 0.5 to it
'100' # Converts the integer 100 to the string '100'

2.2.6. The max(), min(), sum() Functions#

Purpose: These built-in functions work quite intuitively. The max() and min() functions return the largest or smallest item in an iterable (like a list or a string), while the sum() function adds up all the items in an iterable (like a list or a tuple).

Example:

numbers: List = [1, 2, 3, 4, 5]

print(max(numbers))
print(min(numbers))
print(sum(numbers))

Output:

5
1
15
# try it yourself

lengthy_string: str = ''
print(len(lengthy_string))

# aesthetic print
print('-------------')

# what about now?
lengthier_string: str = ' '
print(len(lengthier_string))
0
-------------
1

There are many more built-in functions in Python. If you would like to see more, visit this link.

2.3. Creating Your Own Functions#

Along with using built-in functions, you can create your own. This allows you to package up computations in functions that you can reuse later, making your code more flexible.

To define a function, just use the def keyword, give it a name, optionally add some parameters, and then write out the sequence of instructions you want it to run. This is called a function definition, and it looks like this:

def function_name(parameters) -> type:
    '''
    Docstring
    '''
    # Code

Note that the return type can be added to the function definition via -> type. Let’s look at a very simple example where we simply greet a person.

def greet(name: str) -> None:
    print(f"Hello, {name}!")
    
greet("Roger")
Hello, Roger!

Let’s break down all the components of this function definition and call. From the example, we can clearly see that this is a function because of the def keyword. The function’s name is greet, and it has one parameter called name. In the function body, we simply print the greeting f"Hello, {name}!", therefore returning None. When we call the function, the argument passed is the string "Roger".

Note

The variables that are defined in the function definition are what we call parameters, while actual values that are passed to the function in the function call are named arguments.

2.3.1. Generalization#

It’s crucial for a function to be clear and reusable. To make your life easier, it’s best if you can use the same function multiple times and with different parameter values.

Let’s look at a poor example.

def travel_time() -> None:
    time = 240000 / 1600
    print(f"The travel time between Earth and Moon for this ship is {time} hours.")

travel_time()

## What is the problem with this function?
The travel time between Earth and Moon for this ship is 150.0 hours.

Now, we’ll improve this function definition.

distance_Earth_Moon: int = 240000
travel_speed: int = 1600

def travel_time(distance: int, speed: int, planet1: str, planet2: str) -> None:
    time = distance / speed
    print(f"The travel time between {planet1} and {planet2} for this ship is {time} hours.")

travel_time(distance_Earth_Moon, travel_speed, "Earth", "Moon")
The travel time between Earth and Moon for this ship is 150.0 hours.

That looks much better already! However, we can still make the function definition easier to reuse by adding a docstring.

Note

If your function doesn’t take any parameters, that’s a warning sign. It means the function can only handle a single, specific computation, which is sometimes okay, but not ideal in most cases. So, consider this carefully.

2.3.2. Docstring#

To explain the interface of your function, you use a docstring (“doc” is short for “documentation”), which is a string placed at the beginning of your function. The main aim of a docstring is to clearly describe what the function does without delving into the details of how this is done. For example:

Note

Whenever your create a new function always add typehints to all parameters, moreover, to include the return typehint and write a docstring.

def square(number: Union[int, float]) -> Union[int, float]:
    """
    Returns the square of the given number.
    """
    return number ** 2

print(square(2))
4

A docstring starts with triple-quoted strings, allowing the text to span multiple lines. If it’s not immediately clear what each parameter does, its purpose in the function should also be explained. This looks as follows:

def format_date(day: int, month: int, year: int) -> str:
    """
    Formats a date in the DD-MM-YYYY format.

    Parameters:
    day (int): The day of the month (1-31).
    month (int): The month of the year (1-12).
    year (int): The full year (e.g., 2024).

    Returns:
    str: A string representing the date in the format DD-MM-YYYY.
    """
    return f"{day:02d}-{month:02d}-{year}"

format_date(1,1,1999)
'01-01-1999'

Note

A well designed interface should be simple to explain; if you have a hard time explaining one of your functions, maybe the interface could be improved.

2.3.3. Scope#

When we define variables, we distinguish between local and global variables. Local variables are those defined inside a function, meaning they only exist within that function and cannot be accessed outside of it. Let’s have a look.

def explorer() -> None:
    """Explores the planets Mars and Venus."""
    planet: str = "Mars"
    planet2: str = "Venus"
    
print(planet)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 6
      3     planet: str = "Mars"
      4     planet2: str = "Venus"
----> 6 print(planet)

NameError: name 'planet' is not defined

When we try to access the variable planet outside the function and print it, we get an error saying that the name planet is not defined. The same holds true for parameters, as they are also invisible outside the function.

def concat(part1: str, part2: str) -> None:
    """Concatenates two strings and prints the result twice."""
    cc_result: str = part1 + part2
    print(cc_result)
    
concat('Data ', 'Science')
print(part1)

In contrast, global variables are defined outside of functions and can be accessed and used throughout the whole program, also in a function.

the_best_footballer: str = "Messi"

def the_best(human: str) -> str:
    """Checks if the given human is the footballer Messi."""
    if human == the_best_footballer:
        return f"{human} is the best footballer!"
    else:
        return f"{human} is great, but {the_best_footballer} is the best!"

result = the_best("Bellingham")
print(result)

Now let’s test if we clearly understand the difference between local and global variables. What happens when we run the following cell, what is the exact output?

number: int = 3

def square(number: Union[int, float]) -> Union[int, float]:
    """
    Returns the square of the given number.
    """
    print(number)
    return number ** 2

square_of_4: Union[int, float] = square(4)

print(number)
print(square_of_4)

2.3.4. print() vs return #

It’s important to note that some functions just execute code, while others return a result. Depending on the function’s purpose, calling it can differ:

  • If the function simply runs some code, just call it on a line by itself, like a full statement (e.g., a void function);

  • If the function returns a result, call it where you need the result, like saving it in a variable or printing it (e.g., a fruitful function).

Although a void function might display something or have some other effect, it doesn’t return anything. Therefore, using it on the right side of an assignment statement is pointless. If you do, the variable will receive a special value called None. Here’s an example.

def message(text: str) -> None:
    """Prints a message."""
    print(f"Message: {text}")

result: str = message("Hello World!")

print(result)

A fruitful function does return a result which can be stored in a variable.

# A simple example of a function that returns the sum of 2 numbers

def sum_int_numbers(nr1: int, nr2: int) -> int:
    """Returns the sum of two integers numbers"""
    return nr1 + nr2

sum_var: int = sum_int_numbers(5, 6)
print(sum_var)

Note

The return type can be added to the function definition via -> type.

Consider the following 2 cases:

In Case 1, I am trying to create a function knowing that I will need to use its output later on.
In Case 2, I am creating a function knowing that I only need to display the result immediately and won’t need to use it later.


Question: Based on these 2 cases, when should I use print() and when should I use return?

Let’s test our knowledge. What does this piece of code print?

def explorer(celestial_body: str) -> str:
    """Returns a landing message for a given celestial body."""
    return f"I am now landing on {celestial_body}."

message1 = explorer("Moon")
message2 = explorer("Mars")

message = "Hurray! " + message1
message = message2 + " " + message

print(message)

2.4. Calling Functions in Functions#

Python also allows you to call a function within another function, which modularizes the code, allowing the same function to be used for different purposes.

def square(number: Union[int, float]) -> Union[int, float]:
    """
    Returns the square of the given number.
    """
    return number ** 2

def sum_of_squares(number_1: Union[int, float], number_2: Union[int, float]) -> Union[int, float]:
    """Calculates the sum of the squares of two numbers."""
    return square(number_1) + square(number_2)  # Calls to square()

# Example usage
number_1: Union[int, float] = 3.0
number_2: Union[int, float] = 4.0

result = sum_of_squares(number_1, number_2)
print(f"The sum of the squares of {number_1} and {number_2} is {result}.")

2.5. Exercises#

Let’s practice! Mind that each exercise is designed with multiple levels to help you progressively build your skills. Level 1 is the foundational level, designed to be straightforward so that everyone can successfully complete it. In Level 2, we step it up a notch, expecting you to use more complex concepts or combine them in new ways. Finally, in Level 3, we get closest to exam level questions, but we may use some concepts that are not covered in this notebook. However, in programming, you often encounter situations where you’re unsure how to proceed. Fortunately, you can often solve these problems by starting to work on them and figuring things out as you go. Practicing this skill is extremely helpful, so we highly recommend completing these exercises.

For each of the exercises, make sure to add a docstring and type hints, and do not import any libraries unless specified otherwise.

2.5.1. Exercise 1#

Level 1: Create a Python function named calculate_triangle_area() that calculates and returns the area of a triangle given its base and height as input arguments. The function should use the formula 0.5 * base * height to compute the area and return the result. After calling the function, print the area using an f-String outside of the function. Assume both the base and height are positive numbers.

Example input: you pass these in a function call.

base: int = 6
height: int = 8

Example output:

"The area of the triangle with base 6 and height 8 is 24.0."
# TODO.

Level 2: Create a Python function named calculate_triangle_fit_with_remainder() that takes five parameters: the base and height of a triangle (base, height), the unit of measurement for the triangle (unit), and the dimensions of a rectangular surface (surface_length, surface_width). The function should:

  • Calculate the area of the triangle using the calculate_triangle_area() (function from Level 1).

  • Calculate the area of the rectangular surface using surface_length * surface_width .

  • Determine how many whole triangles can fit into the rectangular surface.

  • Calculate the remaining area on the surface after placing as many whole triangles as possible.

After having calculated the above mentioned information, return a formatted string indicating:

  • The area of the triangle.

  • The area of the surface.

  • The number of triangles that can fit.

  • The remaining area on the surface.

Example input: you pass these in a function call.

base = 5.0
height = 6.0
unit = "cm"
surface_length = 30.0
surface_width = 10.0

Example output:

"The area of the triangle is: 15.00 cm. The area of the surface is: 300.00 cm. Based on this, the number of triangles that fit is: 20. While the remaining surface area is: 0.00 cm."
# TODO.

Level 3: Upgrade the calculate_triangle_area() function to handle multiple triangles by accepting a list of tuples, where each tuple contains a base-height pair. For each pair, calculate the area using the formula 0.5 * base * height and return a list of all the areas. Make sure both bases and heights are positive numbers. Use the return statement to get the list of areas, and don’t forget to print the result outside the function.

Example input: you pass this in a function call.

base_height_pairs: List[tuple] = [(6, 8), (10, 5), (7, 12)]

Example output:

[24.0, 25.0, 42.0]
# TODO.

2.5.2. Exercise 2#

Level 1: Imagine you’re at a delightful farm, surrounded by various animals. To keep yourself entertained, you decide to count the number of legs each type of animal has. Your task is to create a function named count_animal_legs() that takes three inputs: the number of chickens, cows, and dogs you see. The function should then calculate and return the total number of legs based on the counts you provide alongside with the number of each animal.

Hint:

  • Chickens have 2 legs.

  • Cows have 4 legs.

  • Dogs have 4 legs

Example input: you pass this in a function call.

num_of_chickens: int = 6
num_of_cows: int = 3
num_of_dogs: int = 1

Example output:

"I can see 6 chickens, 3 cows and 1 dog. In total, I can see 28 legs.
# TODO.

Level 2: You’re back at the farm, and this time you need a more detailed analysis of the animals you encounter. Your task is to create a function called animal_info() which, beyond counting the number of legs and the total number of animals, can also calculate the total weight of all the animals combined. Additionally, you’ll need to calculate the cumulative weight of each species and present a message that includes all these insights. Your function should take 5 inputs representing the number of animals you see from each species. Use the information provided regarding the weight of each animal.

Details:

  • Chickens weigh 1.5 kg each.

  • Cows weigh 500 kg each.

  • Dogs weigh 30 kg each.

  • Pigs weigh 150 kg each.

  • Horses weigh 600 kg each.

Note: Think about what you learned in the Scope section, and where could that be used.

Example input: you pass this in a function call.

num_of_chickens: int = 5
num_of_cows: int = 3
num_of_dogs: int = 2
num_of_pigs: int = 4
num_of_horses: int = 1

Example output:

"I can see 5 chickens, 3 cows, 2 dogs, 4 pigs, and 1 horse. In total, I can see 32 legs and the total weight of the animals is 2,767.5 kg. The animals cumulative weight by species is: chicken (7.5 kg), dog (60 kg), pig (600 kg), cow (1,500 kg), and horse (600 kg)."

# TODO.

Level 3: You’re back at the farm for a final observation. This time you need to measure the speed of the animals as they cover different distances. Unfortunatelly, it is raining today so all animals are in the barn. Therefore, you decide to conduct a survey and gather information about the speed of each animal. Create a function called sprinter_of_the_farm() which takes no input, however, asks the user for the following information:

  • the name of the animal

  • the species of the animal

  • the distance it covered (in kilometers)

  • the time it took to cover that distance (in hours

Then, the function should calculate the speed of the animal, in meters per second (m/s) and show a summary message to the user.

Hint:

  • Speed is calculated using the formula: Speed = Distance / Time

  • You can find inspiration about how to ask the user for input on this site

Example user input:

name = 'Pedro'
species = 'Teacup Pig'
distance_covered = 0.3
time_required = 0.5

Example output:

"Based on the provided information, Pedro - the Teacup Pig - runs with 0.167 m/s."
# TODO.

2.5.3. Exercise 3#

Level 1: You are an astronaut on a mission to explore the mysterious Planet Numerica. Your spaceship has landed on the planet, and you’ve encountered several tasks that require your Python Programming skills to solve. First, you need to calculate the total amount of fuel required for a round-trip to a nearby research station. Create a function called fuel_calculator() which takes as input the spaceship’s fuel consumption rate (litre/hour) and the time it requires to reach the nearest research station (in hours). Your function should return a detailed summary including the spaceship’s fuel consumption rate, the distance to the nearest research station and total amount of fuel (in litres) required for the round-trip.

Example user input: you pass this in a function call.

fuel_consumption_rate:int = 35
distance_to_research_station:int = 8

Example output:

"The spaceship's fuel consumption rate is 35 litres/hour and the nearest research station is 8 hours away. Therefore, we need 560 litres of fuel for a round-trip.
# TODO.

Level 2: As part of your mission on Planet Numerica, you need to perform a special calculation to navigate through a dangerous asteroid field. The calculation involves determining the position of your spaceship after a series of complex movements. This will require you to carefully apply a sequence of operations to ensure your spaceship arrives at the correct position. Create a function called navigation_system() that takes three parameters:

  • initial_position (an integer representing your starting position)

  • movement_units (an integer representing how many units your spaceship will move in each step)

  • total_steps (an integer representing the total number of units you will take). Meaning if you take 1 step, you move 4 units.

The function should return a message with your final position after performing the following sequence of calculations:

  1. Find the remainder when the total number of steps is compared to the movement units.

  2. Calculate how many whole units of movement can be performed given the total number of steps.

  3. Determine the impact of the calculated units on your movement by multiplying the number of whole units by the remainder.

  4. Adjust your movement by dividing the impact by a combined value derived from the movement units and the initial remainder.

  5. Finally, update your initial position by adding the adjusted movement to it.

Example user input: you pass this in a function call.

initial_position: int = 10
movement_units:int = 4
total_steps:int = 25

Example output:

"Your final position after the calculation is 11.2."
# TODO.

Level 3: As you navigate the vibrant intergalactic market on Planet Numerica, you encounter a dazzling array of space rocks, each with a unique name and price tag. To fully understand the market dynamics, you need to analyze these rocks and uncover three key pieces of information:

  • Most Expensive Rock: Identify the rock with the highest price.

  • Least Expensive Rock: Find the rock with the lowest price.

  • Average Price: Calculate the average price of all the space rocks.

To complete this task, create a function called market_analysis() that takes two lists as inputs:

  1. prices: A list of numerical values representing the prices of the space rocks.

  2. space_rocks: A list of names for each space rock, corresponding to the prices list.

Your function should return a summary including the most expensive rock, the least expensive rock, and the average price.

Note: The rocks and prices go in pairs, meaning that the first price in the prices list belongs to the first rock in the space rocks list and so on.

Example user input: you pass this in a function call.

prices: List[int] = [450, 120, 600, 200, 95]
space_rocks: List[str] = ["Nebula Shard", "Quantum Crystal", "Dark Matter Orb", "Stellar Gem", "Cosmic Dust"]

Example output:

"Based on my findings, the most expensive space rock is called Dark Matter Orb and is worth 600. The least expensive's name is Cosmic Dust with a price of 95. While, in average, a space rock in this market costs 293."
# TODO.

Material for the VU Amsterdam course “Introduction to Python Programming” for BSc Artificial Intelligence students. These notebooks are created using the following sources:

  1. Learning Python by Doing: This book, developed by teachers of TU/e Eindhoven and VU Amsterdam, is the main source for the course materials. Code snippets or text explanations from the book may be used in the notebooks, sometimes with slight adjustments.

  2. Think Python

  3. GeekForGeeks