def print_lyrics():
print("Almost Heaven, West Virginia")
print("Blue Ridge Mountains, Shenandoah River")
03 | Functions, Flow Control, & Conditionals
Designing Reusable Code, Looping, & Decision Making
1 Overview
So far, we’ve used Python’s built-in functions. In this module,
you’ll learn to create and run your own functions, and see how one
function can call another. We’ll also introduce the for
loop to repeat computations and the if statement to execute different
code based on program state. Finally, we’ll learn how to handle external
files, update variables, and perform searches in strings.
2 Defining new functions
A function definition specifies the name of a new function and the sequence of statements that run when the function is called:
def
is a keyword that indicates a function definition.
The name of this function is print_lyrics
. The empty
parentheses after the name indicate that this function doesn’t take any
arguments.
The first line of the function definition is the
header, and the rest is the body. The
header must end with a colon, and the body must be indented (by
convention, four spaces). The body of this function is two
print()
statements, but in general, the body can include
any number of statements.
Defining a function creates a function object, which you can display:
print_lyrics
<function __main__.print_lyrics()>
The output shows that print_lyrics
takes no arguments.
__main__
is the name of the module that contains
print_lyrics
.
You can call this function the same way as any other Python function:
print_lyrics()
Almost Heaven, West Virginia
Blue Ridge Mountains, Shenandoah River
3 Parameters
Some of the functions we’ve seen require arguments:
abs()
takes a number, and math.pow()
takes two
arguments (the base and the exponent). We can make our own:
def print_twice(string):
print(string)
print(string)
The variable in parentheses is called a parameter. When the function is called, the value of the argument is assigned to the parameter. For example:
"Hail WV!") print_twice(
Hail WV!
Hail WV!
This has the same effect as assigning the argument to the parameter and then executing the body:
= "Hail WV!"
string print(string)
print(string)
Hail WV!
Hail WV!
You can also pass a variable as an argument:
= "Hail WV!"
line print_twice(line)
Hail WV!
Hail WV!
4 Calling functions
Once you define a function, you can use it inside another. Here’s a playful example of printing lyrics for “Turn Down For What” by DJ Snake & Lil Jon:
Verse:
Fire up that loud
Another round of shots
Chorus:
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Build:
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
As Lil Jon suggests, we need to buy a lot of rounds of shots. We can start with a helper function:
def repeat(word, n):
print(word * n)
"shots! ", 3) repeat(
shots! shots! shots!
def print_build():
"shots! ", 4)
repeat("shots! ", 4)
repeat("shots! ", 4)
repeat("shots! ", 4)
repeat(
print_build()
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
print_build()
calls repeat()
, which then
calls print()
. We could do the same with fewer functions,
but this illustrates how functions can work together.
If we want to control how many times repeat()
repeats,
we add a parameter to print_build()
:
def print_build(n):
"shots! ", n)
repeat("shots! ", n)
repeat("shots! ", n)
repeat("shots! ", n) repeat(
Then we can call:
4) print_build(
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
Next, let’s add verse and chorus functions:
def print_verse():
print("Fire up that loud")
print("Another round of shots")
print_verse()
Fire up that loud
Another round of shots
def print_chorus():
print("Turn down for what?")
print("Turn down for what?")
print("Turn down for what?")
print("Turn down for what?")
print("Turn down for what?")
print_chorus()
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Now bring it all together:
print_verse()
print_chorus()
print_verse()
print_chorus()4) print_build(
Fire up that loud
Another round of shots
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Fire up that loud
Another round of shots
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
We’re repeating some lines of code explicitly, which isn’t ideal. We’ll address that soon.
5 Repetition
To print something multiple times, you can use a for
loop. Here’s a simple example:
for i in range(2):
print(i)
0
1
range(2)
creates a sequence of two values:
0
and 1
. The loop assigns each value to
i
and then runs the body. When the sequence ends, the loop
ends.
Here’s a loop that prints the verse twice:
for i in range(2):
print("Verse", i)
print_verse(),print() # adds a blank line
Verse 0
Fire up that loud
Another round of shots
Verse 1
Fire up that loud
Another round of shots
A for
loop can appear inside a function, such as this
one that prints the verse m
times:
def print_m_verse(m):
for i in range(m):
print_verse()
4) print_m_verse(
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
In this example, we don’t use i
in the body of the loop,
but there has to be a variable in the header anyway.
6 Variables and parameters are local
A variable created inside a function is local, meaning it only exists inside that function. Here’s an example:
def cat_twice(part_1, part_2):
= part_1 + part_2
cat print_twice(cat)
= "Country roads, "
line_1 = "take me home."
line_2 cat_twice(line_1, line_2)
Country roads, take me home.
Country roads, take me home.
Inside cat_twice()
, cat is created. Outside of it,
cat
doesn’t exist:
print(cat)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[21], line 1 ----> 1 print(cat) NameError: name 'cat' is not defined
Parameters are also local. Outside cat_twice()
,
part_1
and part_2
don’t exist.
7 Tracebacks
When a runtime error occurs inside a function, Python shows a
traceback, listing the function that was running, the
function that called it, and so on, up the “stack.” Here’s a
print_twice()
that tries to print cat
, which
is a local variable in a different function:
def print_twice(string):
print(cat) # NameError
print(cat)
cat_twice(line_1, line_2)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[23], line 1 ----> 1 cat_twice(line_1, line_2) Cell In[19], line 3, in cat_twice(part_1, part_2) 1 def cat_twice(part_1, part_2): 2 cat = part_1 + part_2 ----> 3 print_twice(cat) Cell In[22], line 2, in print_twice(string) 1 def print_twice(string): ----> 2 print(cat) # NameError 3 print(cat) NameError: name 'cat' is not defined
The traceback shows that cat_twice()
called
print_twice()
, which caused the error.
8 Refactoring
Let’s reorganize our “Turn Down for What” example to avoid repeated code. This is called refactoring.
# Original
def print_build(n):
"shots! ", n)
repeat("shots! ", n)
repeat("shots! ", n)
repeat("shots! ", n)
repeat(
# Improved
def print_build(repeats, shots):
for i in range(repeats):
"shots! ", shots)
repeat(
4, 4) print_build(
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
# Original
def print_verse():
print("Fire up that loud")
print("Another round of shots")
# Improved
def print_verse(lines):
for i in range(lines):
print("Fire up that loud")
print("Another round of shots")
4) print_verse(
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
# Original
def print_chorus():
print("Turn down for what?")
print("Turn down for what?")
print("Turn down for what?")
print("Turn down for what?")
print("Turn down for what?")
# Improved
def print_chorus(lines):
for i in range(lines):
print("Turn down for what?")
6) print_chorus(
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Putting it all together:
1)
print_verse(print()
5)
print_chorus(print()
1)
print_verse(print()
5)
print_chorus(print()
4)
print_verse(print()
6, 4)
print_build(print()
5) print_chorus(
Fire up that loud
Another round of shots
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Fire up that loud
Another round of shots
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
Fire up that loud
Another round of shots
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Turn down for what?
Refactoring improves the code’s structure without changing its behavior. If we had planned the structure from the start, we might have avoided this step, but sometimes you only see a better design after you start coding.
9 Why functions?
- Readability: Naming groups of statements makes code easier to read and debug.
- Reusability: Functions eliminate repetitive code. Changes become easier to manage.
- Modularity: Breaking down a program into functions lets you debug parts individually.
- Reuse: Well-designed functions can be used by other programs.
Wrapping code in a function is called encapsulation. One advantage is that a name serves as documentation. Another is that calling a function is more concise than copying and pasting its body.
Adding parameters to a function is called
generalization, because it makes the function more
general – for example, printing "shots!"
any number of
times.
When a function has several numerical arguments, it’s easy to mix them up. You can use keyword arguments to specify each argument by name:
def print_build(repeats, shots):
for i in range(repeats):
"shots! ", shots)
repeat(
=8, shots=4)
print_build(repeatsprint()
=4, repeats=8) print_build(shots
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
shots! shots! shots! shots!
10 Docstrings
A docstring is a string at the start of a function that explains its interface:
def print_build(repeats, shots):
"""Prints "shots" for a custom amount of times and lines
repeats: number of lines of lyrics
shots: number of times "shots" is printed per line
"""
for i in range(repeats):
"shots! ", shots) repeat(
A good docstring explains what the function does and the effect of each parameter, without diving into internal details.
11
if
statements
Conditional statements let you check conditions and
change the program’s behavior. The simplest form is the if
statement:
if x > 0:
print('x is positive')
The boolean expression after if
is called the condition.
If it’s true, Python executes the indented block; otherwise, it skips
it.
If you need a block that does nothing, use pass
:
if x < 0:
pass # TODO: need to handle negative values!
12 Boolean expressions and logical operators
A boolean expression is either True
or
False
. For instance:
5 == 5
True
5 == 7
False
The double equal sign ==
compares two values for
equality.
A common error is to use a single equal sign (=
) instead
of a double equal sign (==
). Remember that =
assigns a value to a variable and ==
compares two
values.
Other relational operators include:
!= y # x is not equal to y
x > y # x is greater than y
x < y # x is less than to y
x >= y # x is greater than or equal to y
x <= y # x is less than or equal to y x
You can combine boolean expressions with logical
operators: and
, or
, and
not
. For example:
> 0 and x < 10
x % 2 == 0 or x % 3 == 0
x not x > y
13 The
else
clause
if
can include an else
clause:
if x % 2 == 0:
print('x is even')
else:
print('x is odd')
One branch runs if the condition is true, the other if it’s false.
14 Chained conditionals
When you have more than two possibilities, use elif
:
if x < y:
print('x is less than y')
elif x > y:
print('x is greater than y')
else:
print('x and y are equal')
Conditions are checked in order, and only the first true branch runs.
15 Nested Conditionals
One conditional can be nested within another, but it can be harder to read:
if 0 < x:
if x < 10:
print('x is a positive single-digit number.')
Logical operators often simplify nested conditionals:
if 0 < x and x < 10:
print('x is a positive single-digit number.')
For this kind of condition, Python provides a more concise option:
if 0 < x < 10:
print('x is a positive single-digit number.')
16 Some functions have return values
Functions like abs
, round
,
math.sqrt
, and math.pow
return a value. You
can assign that value to a variable or use it in an expression:
import math
= math.sqrt(42 / math.pi)
radius = math.pi * radius**2 area
You can also write your own function with a return value:
def circle_area(radius):
= math.pi * radius**2
area return area
= circle_area(radius)
a a
42.00000000000001
However, local variables inside a function (like area
)
don’t exist outside that function.
area
42.00000000000001
17 And
some have None
If a function doesn’t use return
, it returns
None
, a special value:
def repeat(word, n):
print(word * n)
This function uses the print
function to display a
string, but it does not use a return
statement to return a
value. If we assign the result to a variable, it displays the string
anyway.
= repeat('Shots! ', 3)
result print(result) # Displays None
Shots! Shots! Shots!
None
If you want a function that returns a string rather than prints it, you can do:
def repeat_string(word, n):
return word * n
Notice that we can use an expression in a return statement, not just a variable.
18 Return values and conditionals
A function can have multiple return statements, such as a
reimplementation of abs
:
def absolute_value(x):
if x < 0:
return -x
else:
return x
Make sure thath every possible path hits a return statement:
def absolute_value_wrong(x):
if x < 0:
return -x
if x > 0:
return x
# If x is 0, returns None (missing a final else branch)
19 Boolean functions
Functions can return the boolean values True
and
False
, which is often convenient for encapsulating a
complex test in a function. For example, is_divisible
checks whether x is divisible by y with no remainder.
def is_divisible(x, y):
if x % y == 0:
return True
else:
return False
6, 4) is_divisible(
False
6, 3) is_divisible(
True
Inside the function, the result of the ==
operator is a
boolean, so we can write the function more concisely by returning it
directly:
def is_divisible(x, y):
return x % y == 0
Boolean functions are often used in conditional statements:
if is_divisible(6, 2):
print('divisible')
divisible
It might be tempting to write something like this:
if is_divisible(6, 2) == True:
print('divisible')
divisible
But the comparison is unnecessary.
20 Input validation
We now have all the tools we need to make sure that the programs we write will be executed fully, regardless of user error.
Remember, if the end use can mess up your instructions they will. Sound familiar?
Let’s go back to the example of calculating the volume of a sphere with a succinct function using what we’ve learned so far:
def volume_of_sphere(radius):
from math import pi
return (4/3) * pi * radius**3
If radius is an integer or float, no problem. But if we get a string, we will get an error:
print(volume_of_sphere(4))
print(volume_of_sphere("4"))
268.082573106329
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[48], line 2 1 print(volume_of_sphere(4)) ----> 2 print(volume_of_sphere("4")) Cell In[47], line 3, in volume_of_sphere(radius) 1 def volume_of_sphere(radius): 2 from math import pi ----> 3 return (4/3) * pi * radius**3 TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
While the default error message gives the user an idea of what’s wrong, we can handle this more gracefully:
def volume_of_sphere(radius):
if type(radius) == int or type(radius) == float:
from math import pi
return (4/3) * pi * radius**3
else:
print("Input was not a number, try again.")
return None
"5") volume_of_sphere(
Input was not a number, try again.
If this were a standalone program, it would return a result instead of stopping execution with an error.
21 Debugging
Debugging can be frustrating, but it is also challenging, interesting, and sometimes even fun. And it is one of the most important skills you can learn.
In some ways debugging is like detective work. You are given clues and you have to infer the events that led to the results you see.
Debugging is also like experimental science. Once you have an idea about what is going wrong, you modify your program and try again. If your hypothesis was correct, you can predict the result of the modification, and you take a step closer to a working program. If your hypothesis was wrong, you have to come up with a new one.
For some people, programming and debugging are the same thing; that is, programming is the process of gradually debugging a program until it does what you want. The idea is that you should start with a working program and make small modifications, debugging them as you go.
If you find yourself spending a lot of time debugging, that is often a sign that you are writing too much code before you start tests. If you take smaller steps, you might find that you can move faster.
When a syntax or runtime error occurs, the error message contains a lot of information, but it can be overwhelming. The most useful parts are usually:
- What kind of error it was, and
- Where it occurred.
Syntax errors are usually easy to find, but there are a few gotchas. Errors related to spaces and tabs can be tricky because they are invisible and we are used to ignoring them.
= 5
x = 6 y
Cell In[50], line 2 y = 6 ^ IndentationError: unexpected indent
In this example, the problem is that the second line is indented by
one space. But the error message points to y
, which is
misleading. Error messages indicate where the problem was discovered,
but the actual error might be earlier in the code.
The same is true of runtime errors. For example, suppose you are trying to convert a ratio to decibels, like this:
import math
= 9
numerator = 10
denominator = numerator // denominator
ratio = 10 * math.log10(ratio) decibels
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[51], line 5 3 denominator = 10 4 ratio = numerator // denominator ----> 5 decibels = 10 * math.log10(ratio) ValueError: math domain error
The error message indicates line 5, but there is nothing wrong with
that line. The problem is in line 4, which uses integer division instead
of floating-point division – as a result, the value of
ratio
is 0
. When we call
math.log10
, we get a ValueError
with the
message math domain error
, because 0
is not in
the “domain” of valid arguments for math.log10
, because the
logarithm of 0
is undefined.
In general, you should take the time to read error messages carefully, but don’t assume that everything they say is correct.
Breaking a large program into smaller functions creates natural checkpoints for debugging. If a function is not working, there are three possibilities to consider:
- There is something wrong with the arguments the function is getting – that is, a precondition is violated.
- There is something wrong with the function – that is, a postcondition is violated.
- The caller is doing something wrong with the return value.
To rule out the first possibility, you can add a print
statement at the beginning of the function that displays the values of
the parameters (and maybe their types). Or you can write code that
checks the preconditions explicitly.
This is a very basic version of logging.
If the parameters look good, you can add a print statement before each return statement and display the return value. If possible, call the function with arguments that make it easy check the result.
If the function seems to be working, look at the function call to make sure the return value is being used correctly – or used at all!
Adding print
statements at the beginning and end of a
function can help make the flow of execution more visible for testing.
For example, here is a version of volume_of_sphere
with
print statements:
def volume_of_sphere(radius):
if type(radius) == int or type(radius) == float:
print(f"Radius is a valid type: {type(radius)}") # Showing radius is fine
from math import pi
return (4/3) * pi * radius**3
else:
print("Input was not a number, try again.") # In original example, showing radius was not fine
return None
= volume_of_sphere(5)
vol print(vol)
Radius is a valid type: <class 'int'>
523.5987755982989
= volume_of_sphere("5")
vol vol
Input was not a number, try again.
22 Exercises
22.1 Right-Align Text
Write a function named print_right()
that takes a string
named text
as a parameter and prints the string with enough
leading spaces that the last letter of the string is in the 40th column
of the display.
Hint: Use len()
, +
, and *
.
Here’s an example output:
"Monty")
print_right("Python's")
print_right("Flying Circus") print_right(
Monty
Python's
Flying Circus
22.2 Draw a Triangle
Write a function called triangle
that takes a string and
an integer, then draws a pyramid of the given height using copies of the
string. For example:
"L", 5) triangle(
L
LL
LLL
LLLL
LLLLL
22.3 Draw a Rectangle
Write a function called rectangle
that takes a string
and two integers, then draws a rectangle of the given width and height
using copies of the string. For example:
"[]", 5, 4) rectangle(
[][][][][]
[][][][][]
[][][][][]
[][][][][]
22.4 Triangle Tester
Write a function named is_triangle
that takes three
integers as arguments and returns True or False depending on whether you
can form a triangle with those lengths. Use the rule:
If any of the three lengths is greater than the sum of the other two, then you cannot form a triangle. Otherwise, you can. (If the sum of two lengths equals the third, they form what is called a “degenerate” triangle.)
Hint: Use a chained conditional.
22.5 Check “Between”
Write a boolean function is_between(x, y, z)
, that
returns True
if \(x < y <
z\) or if \(z < y < x\),
and False
otherwise.