Preparation#

Reading material#

In the Think Python (TP) book, void functions are covered in Chapter 3 Functions (you can skip the section on stack diagrams). Functions with return statements are covered in the first 5 sections of Chapter 6 Return Values.

In the CS50 course, the concept of functions was introduced already in Lecture 0 Functions, Variables, section Def.

Moving to using VS Code#

During the live demo this week, we will start using Visual Studio Code (VS Code) as our primary development environment. As part of your preparation, you should look at the material provided by the Python Support regarding VS Code and running Python scripts.

Copy-and-Run#

Explanation of the Copy-and-Run exercises. In Copy-and-Run (CaR) exercises, you’ll copy the provided code into Python, run it, and observe the output. Try to understand why the code produces that result and, if possible, predict the output before running it.

Prep 5.1: Basics of Functions#

Copy and run the following code. Make sure to copy it exactly as it is written. Try to predict what will happen before running it.

def hello_world():
    print("Hello, world!")
    print("This is my first Python function!")

The reason nothing was printed is that the function hello_world() was defined, but it was never called. Now, run this code:

def hello_world():
    print("Hello, world!")
    print("This is my first Python function!")
    
hello_world()
hello_world()

Now you have called the function twice, and it executed twice.

Looking back at the first code snippet, you might think that nothing happened when you executed the code where the function was defined but not called. However, something did happen. The function was defined, and it was stored in memory. This allows you to call the function later in the code.

Try running this code:

hello_world()

def hello_world():
    print("Hello, world!")
    print("This is my first Python function!")

Read the error message. You have seen a similar error message in the first week of the course, where you learned that Python executes the code line by line. Here, you see that the function always needs to be defined before it is called.

Functions are blocks of code that perform a specific task and can be called from other parts of the code. A function definition contains:

  1. The keyword def, which indicates the beginning of the function definition.

  2. The function name, chosen by the programmer (in the example above: hello_world).

  3. Parentheses, which may be empty, though they can contain arguments (we will see this later).

  4. A colon, indicating that the function body is about to start.

  5. The function body, indicated by indentation.

Run the code below to see how to define and call a function with arguments. Observe what happens.

def fancier_greet(name, day):
    print("Greetings, my good fellow " + name + "!")
    print("How are you doing this fine " + day + "?")

fancier_greet("Anders", "Thursday")
fancier_greet("Sandra", "Monday")

What do you think will happen if you try executing fancier_greet("Friday", "Isabella") in the same script? Try it.

Now, try predicting what will happen when you run this code. Run it and see if you were right.

def sum_and_print(x, y):
    print(x + y)

a = 5
b = 10
sum_and_print(2 * b, b + a)

Next, try predicting what will happen when you run this code. Run it and see if you were right.

def another_greeting(name, day):
    greeting = "Hello, " + name + "! What a lovely " + day + "!"
    print(greeting)

name = "Martin"
day = "Wednesday"
another_greeting(day, name)

In the two examples above, you have seen that the function only cares about the order and the values of the arguments, not their names.

When we write about functions, it is common to write the function name followed by parentheses. For example, we might write another_greeting() to indicate that this is a function.

Prep 5.2: Functions That Return Values#

Now, run this code which contains a function and a function call. Try to predict what will happen before running it.

def silly_square(x):
    k = -x * x
    return k

a = 5
b = silly_square(a)
print(b)

Functions can return values using the return keyword. The return statement ends the execution of the function and sends the value back to the caller. What is the type of the value returned by the function above? You can check it by using print(type(b)). Try also writing print(type(silly_square)) to see what type the function itself is.

Try running this code to learn more about how return works.

def my_function(x, y):
    k = x + y
    if k > 10:
        return k
    return 0

print(my_function(3, 4))
print(my_function(30, 40))

A function can have multiple return statements, but function execution ends when the first return statement is reached.

Try now executing this function, which has multiple return statements. Which one will be executed?

def another_function(hours):
    if hours < 12:
        return "Good morning!"
    elif hours < 18:
        return "Good afternoon!"
    else:
        return "Good evening!"

print(another_function(15))

Next, try running this function that returns something. What does it return?

def returning_expression(x):
    return x * 2 + 20

a = 10
b = returning_expression(a)
print(b)

As you can see, if there is an expression after the return keyword, it will be evaluated and returned.

Try now executing this function. Try predicting what will happen. What is the type of the value it returns?

def is_even(x):
    return x % 2 == 0
    
print("Is 5 even?", is_even(5))
print("Is 6 even?", is_even(6))

It is very common to create functions that check some condition and return a boolean value. In the example above, the expression x % 2 == 0 gets evaluated and returned as True or False. This is a common pattern in Python. A less experienced programmer might have written the function like this, which works correctly but is needlessly complicated.

def is_even(x):
    if x % 2 == 0:
        return True
    else:
        return False

Now, try running this code. What will get printed, and in which order?

def doing_two_things(name):
    print("Hello, " + name + "!")
    return "Your name is " + str(len(name)) + " characters long."
    
comment = doing_two_things("Michael")
print(comment)

Next, try executing this code, which has a function with no return statement. Do you think it will work? Will anything get printed? And if so, how many lines will be printed? Check it out.

def say_hello():
    print("Hello!")

a = say_hello()
print(a)

If the function doesn’t have a return statement, it will return None. This is why the second line of the printed output is None.

Now, look carefully at the two blocks of code. What will be the difference in the printed output when executing them?

def function_1(name, age):
    print("Hi, " + name + ". When do you turn " + str(age + 1) + "?")

function_1("Alice", 25)
def function_2(name, age):
    return "Hi, " + name + ". When do you turn " + str(age + 1) + "?"

print(function_2("Alice", 25))

While there is no difference in the printed output, the two functions are very different. The first function prints a message and returns None. The second function returns the message, which is then printed after the function call.

It is very important that you understand the difference between printing and returning. In many of the exercises in this course, you will be asked to make a function that returns a value. When testing your function, we will call it and check the returned value. If you print the value instead of returning it, the tests will fail.

Prep 5.3: Function Scope#

Try running this code. What do you expect will happen?

def convert_imperial_to_metric(feet, inches):
    length_in_inches = feet * 12 + inches
    length_in_cm = length_in_inches * 2.54
    return length_in_cm

feet = 6
inches = 2
converted = convert_imperial_to_metric(feet, inches)
print(length_in_cm)

Read the error message carefully. You got the error because variables created inside a function are only accessible inside that function. This is called the scope of the variable. The variables defined inside a function are called local variables. When Python stops executing the function, it deletes the local variables as they are no longer needed. Change the argument given to the print statement in the example above to correctly print the length in centimeters.

The variables defined outside of a function are called global variables. Global variables can be accessed from anywhere in the script, including inside functions. The example below shows how a global variable is used in the function.

inch_per_cm = 2.54

def convert_imperial_to_metric(feet, inches):
    length_in_inches = feet * 12 + inches
    length_in_cm = length_in_inches * inch_per_cm
    return length_in_cm

feet = 6
inches = 2
my_height_in_cm = convert_imperial_to_metric(feet, inches)
print(my_height_in_cm)

It is better not to depend on global variables in functions. For example, if the function above gets copied to another script, it will not work because the variable inch_per_cm is not defined. In this course, you should always write functions that take all necessary information from arguments.

What do you think will happen when you run the following code? Try it.

def add_one_to_x(x):
    x = x + 1
    
x = 1
add_one_to_x(x)
print(x)

Reassigning a variable inside a function will not change the value of the variable outside the function. You can still use the changed value inside the function. Try this by printing the value of x after the reassignment inside the function.

For the variable types we have worked with so far, functions cannot change the value of a variable and have this change propagate outside the function. Later, you will learn about other data types where this is possible.

If you want to change the value of a variable based on some calculation in the function, you should return the new value and assign it to the variable outside the function. We have modified the function above to achieve this. Try running the code below.

def add_one_to_x(x):
    x = x + 1
    return x
    
x = 1
x = add_one_to_x(x)
print(x)

Prep 5.4: Functions That Call Other Functions#

Look at the code below and understand what it does. Test it by running it. The code contains three print statements that have been commented out. After running the code and understanding what it does, figure out how many print statements will be executed and in which order. Uncomment the print statements and run the code again to check if you were right.

import math

def disc_area(radius):
    # print("print 1")
    return math.pi * radius ** 2

def ring_area(inner_radius, outer_radius):
    # print("print 2")
    return disc_area(outer_radius) - disc_area(inner_radius)

# print("print 3")
inner_radius = 5
outer_radius = 10
area = ring_area(inner_radius, outer_radius)
print(area)

What you’ve seen in the example above is that functions can call other functions. If you are in doubt about why the print statements are executed in the order they are, you can use Python Tutor to visualize the execution of the code.

Usually, functions are written to perform a specific task, and if the task is too complex, it can be divided into smaller tasks which are solved by other functions. Deciding how to divide the code into functions is a skill that develops with experience. In this course, we will always state clearly if you should divide the code into functions and what the functions should do.

Do you think that the following code will work without any errors? Notice that the function body of ring_area() contains a call to disc_area(), which is defined later in the code. Try running the code.

import math

def ring_area(inner_radius, outer_radius):
    return disc_area(outer_radius) - disc_area(inner_radius)

def disc_area(radius):
    return math.pi * radius ** 2

print(ring_area(5, 10))

As you can see, since the ring_area() function is called at the end of the script, the function disc_area() is already defined when it is called.

Prep 5.5: Tracebacks#

Run this code with a function that intentionally contains an error. Read the whole error message. What is Python trying to tell you?

def function_with_error():
    a = 5
    b = "The number is " 
    print(b + a)

function_with_error()

Confirm that the error message mentions two lines: the line with the function call that led to the error, and the line in the function where the error occurred.

Such error messages are called tracebacks. They trace the error back to the line where it occurred.

Try running this code, which contains three functions calling each other.

def print_message(x):
    print("For",  x, "the value is", compute(x))

def inverse(x):
    return 1 / x

def compute(x):
    return 2 * inverse(x)

print_message(0)

Answer the following questions about the code you just ran:

  1. In which order were the functions called?

  2. In which function did the error occur?

When many functions call each other, the traceback can be long, but it’s always the line at the bottom where the error occurred. However, this does not necessarily mean that there is something wrong with the function where the error occurred. The error might have been caused by passing incorrect arguments to the function.

In the example above, change the function call to print_message(5). What will happen? Try running the code.

Prep 5.6: Testing Functions#

When a function runs without errors, it doesn’t mean that it works correctly. Figuring out whether a function works correctly is called testing. To test a function you need to write a piece of code that calls the function and checks if the function does what it is supposed to do.

Below we show four different ways to test a function. In all examples, we test a function square() which is intended to return the square of the input number. In all examples, this function was intentionally written incorrectly. Run the test with the faulty function and see what happens. Then correct the function and run the test again.

def square(x):
    return x * 2

print("Testing the square function. The result should be correct.")
print("Square of -3:", square(-3))
print("Square of 0:", square(0))
print("Square of 5:", square(5))
print("Square of -4:", square(-4))

For the test above to be useful, you need to know what the function is supposed to return. If the function is complex, it might be difficult to know what the correct output should be.

def square(x):
    return x * 2

print("Testing the square function. Returned value should be the same as expected.")
print("Returned", square(-3), "; Expected", 9)
print("Returned", square(0), "; Expected", 0)
print("Returned", square(1.5), "; Expected", 2.25)
print("Returned", square(2), "; Expected", 4)
print("Returned", square(25), "; Expected", 625)

Here, we have printed the returned values against the expected values. You can see that the incorrect function still returns correct values for some inputs. Furthermore, this type of testing relies on you to look at the printed output, and does not actually check the values. We do that next.

def square(x):
    return x * 2

print("Testing the square function. These should all be True.")
print("Test 1:", square(-3) == 9)
print("Test 2:", square(0) == 0)
print("Test 3:", square(1) == 1)
print("Test 4:", square(2) == 4)
print("Test 5:", square(25) == 625)

You can also create another function with the sole purpose of testing the function square(). This testing function should return a boolean True if the function passes all the tests.

def square(x):
    return x * 2

def test_square():
    test1 = square(-3) == 9
    test2 = square(0) == 0
    test3 = square(1) == 1
    test4 = square(2) == 4
    test5 = square(25) == 625

    all_tests_passed = test1 and test2 and test3 and test4 and test5
    return all_tests_passed

if test_square():
    print("Your function passed all tests.")
else:
    print("Your function did not pass all tests.")

Prep 5.7: Debug Common Function Errors#

Below we have several examples, where some contain syntax errors, while some code runs but encounters errors. For all the examples below, first try to find the error by reading the code. Then run the code to see what happens, and read the error message. Finally, try to fix the error. We have provided hints for fixing the errors, but you should try to fix the errors without looking at the hints.

def calculate_density(mass, volume)
    return mass / volume

print(calculate_density(10, 2)) 
def convert_to_celsius(fahrenheit):
return (fahrenheit - 32) * 5 / 9

print(convert_to_celsius(67))
def calculate_velocity(distance, time):
    return distance / time

print(calculate_velocity(32))
print(calculate_velocity(32, 17, 86))
def factorial(n):
    output = 1
    for i in range(1, n + 1):
        output = output * i

print("2 * 5! =", 2 * factorial(5))
import math

def cone_volume(radius, height):
    return math.pi * (radius ** 2) * height / 3

cone_volume = cone_volume(3, 4)
print("The volume of a cone with radius 3 and height 4 is", cone_volume)
cone_volume = cone_volume(5, 6)
print("The volume of a cone with radius 5 and height 6 is", cone_volume)
def get_greeting():
    return "Hello there... "

print(get_greeting + "Simon") 
def print_message(print):
    print("The message is:", print)

print_message("Don't do this!")

Prep 5.8: Predict the Output#

For each of the following code snippets, try to predict what will be printed before running the code. Then run the code and observe the output.

def f(x):
    return x + 1

print(f(5))
def f(x):
    return x + 1

print(f(f(5)))
def calculate_age(birth_year, had_birthday):
    if had_birthday:
        return current_year - birth_year
    else:
        return current_year - birth_year - 1

current_year = 2024
my_birth_year = 1997
print("If you have had your birthday this year,")
print("then you are", calculate_age(my_birth_year, True), "years old.")
print("and if you didn't you are", calculate_age(my_birth_year, False), "years old.")
def print_favorite_fruit(fruit):
    favorite_fruit = fruit
    print("My favorite fruit is", favorite_fruit)
    
favorite_fruit = "apple"
print_favorite_fruit("banana")
print("favorite_fruit =", favorite_fruit)
def count1(counter):
    counter = counter + 1
    counter = count2(counter)
    counter = count2(counter)
    counter = count2(counter)
    return counter

def count2(counter):
    counter = counter + 10
    return counter

print(count1(0))
def print_message():
    print("will A be first?")
    return "will B be first?"

print(print_message())

Self quiz#

Question 5.1#

What happens when executing this code?

def print_date(month, day):
    print("Month:", month, "Day:", day)

month = "January"
day = 31

Question 5.2#

What happens when executing this code?

def compute(first, second):
    return first - second

first = 5
second = 3
print(compute(second, first))

Question 5.3#

What happens when executing this code?

g = 7

def velocity(time):
    g = 9.81
    return 0.5 * g * time**2

print(g)

Question 5.4#

What happens when executing this code?

day = "Monday"
def print_day(day):
    print("Day:", day)

day = "Tuesday"
print_day()

Question 5.5#

In this code, which of the functions f1() and f2() are executed and in what order?

def f1(x):
    return x + 1

def f2(x):
    return x + 2

f1(f2(4))

Question 5.6#

What code could replace the function body without changing its behavior?

def is_divisible_by_3(x):
    if (x % 3) == 0:
        return True
    else:
        return False

Question 5.7#

What code could replace the function body without changing its behavior?

def is_weekend(day):
    if day == "Saturday":
        return True
    elif day == "Sunday":
        return True
    else:
        return False

Question 5.8#

What value does the variable month have after the following code is executed?

month = "December"
def new_year():
    month = "January"
new_year()

Question 5.9#

What is the value of the variable text after the following code is executed?

def double(n):
    return 2 * n

text = 'one'
text = double(text)

Question 5.10#

What is the value of the variable grade after the following code is executed?

def grade_from_score(score):
    if score > 90:
        return "A"
    elif score > 80:
        return "B"
    elif score > 70:
        return "C"

grade = grade_from_score(50)

Question 5.11#

How many lines are printed when the following code is executed?

def important_message():
    print("Attention!")
    print("Attention!")

def more_important_message():
    important_message()
    important_message()

more_important_message()

Question 5.12#

How can you call the following function without encountering an error?

def polynomial(x, a, b, c):
    return a * (x**2) + b * x + c

Question 5.13#

What is the value of the variable a after the following code is executed? Be aware that the code has a mistake.

def sum_numbers(n):
    my_sum = 0
    for i in range(1, n + 1):
        my_sum = my_sum + i
        return my_sum

a = sum_numbers(3)

Question 5.14#

What value does the variable a have after the following code is executed?

def f(a, x):
    a = a + x
    return a

a = 1
f(a, 2)

Question 5.15#

What value does the variable good have after the following code is executed?

def compare_numbers(temp, bigger):
    if bigger:
        good = temp >= 100
    else:
        good = temp >= 212
    return good
temp = 144
good = compare_numbers(temp, True)

Question 5.16#

Which of the following statements are correct?

Question 5.17#

What is the value of x after the following code is executed?

def square(x):
    return x ** 2

def add_5(x):
    return x + 5

x = add_5(square(2) - square(1))

Question 5.18#

What is the value of assessment after the following code is executed?

def assess_level(level):
    level = 5
    if level >= 9:
        return "Advanced"
    elif level >= 5:
        return "Intermediate"
    else:
        return "Beginner"

assessment = assess_level(10)

Question 5.19#

What are the values of test1 and test2 after the following code is executed?

def get_level(level):
    if level > 5:
        print("High")
    else:
        print("Low")

test1 = get_level(3) == "Low"
test2 = get_level(7) == "High"

Question 5.20#

What is the value of the variable acceleration after the following code is executed?

def get_acceleration(planet):
    if planet == "Mars":
        return 3.71
    elif planet == "Earth":
        return 9.81
    else:
        return 0

acceleration = get_acceleration("Jupiter")

Question 5.21#

What happens when executing this code?

k = 3
def func(var):
    k = 13
    return k + var
print(k)
func(-1)
print(k)