Functions · about 35 min

Defining and calling functions

Why this matters

So far your programs have been one long sequence of statements. Real programs are built from functions: named, reusable pieces of behavior that take inputs and produce a result. Functions let you write a calculation once and use it in many places, give a name to an idea (is_valid_email, total_price), and test each piece on its own. Every later topic in this track, from collections to classes, is expressed through functions, so this is a foundation you will lean on daily.

Defining a function

def greet(name):
    message = "Hello, " + name + "!"
    return message

Line by line:

Defining a function does not run its body. It creates a function object and binds the name greet to it. The body runs only when you call the function:

text = greet("Ada")
print(text)
print(greet("Grace"))
Hello, Ada!
Hello, Grace!

The values in the call, "Ada" and "Grace", are arguments. When the call happens, Python binds the parameter name to the argument object, runs the body, and the call expression greet("Ada") evaluates to whatever was returned.

return versus print

This distinction trips up nearly everyone at first, so take it slowly.

def square_print(n):
    print(n * n)

def square_return(n):
    return n * n

total = square_return(3) + square_return(4)   # 9 + 16
print(total)                                  # 25

total = square_print(3) + square_print(4)
25
9
16
Traceback (most recent call last):
  ...
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'

square_print showed 9 and 16 but gave back nothing usable, because a function that finishes without executing return returns None. A bare return (with no value) also returns None. Adding None + None is a TypeError. Rule of thumb: functions that compute should return; only the outer layer of a program, the part that talks to the user, should print.

return ends the function

return exits immediately, even from inside a loop or an if. That makes early returns a clean way to handle cases:

def sign(n):
    if n > 0:
        return "positive"
    if n < 0:
        return "negative"
    return "zero"

print(sign(5), sign(-2), sign(0))
positive negative zero

No elif is needed: once a return runs, nothing below it in the function executes.

def first_multiple_of_7(start):
    n = start
    while True:
        if n % 7 == 0:
            return n
        n += 1

print(first_multiple_of_7(30))
35

The while True loop would never end on its own; the return both stops the loop and leaves the function.

What happens underneath: frames and the call stack

Each time a function is called, CPython creates a frame: a small record holding the function's local names (its parameters and any names assigned inside it), the position of the instruction currently executing, and a pointer back to the caller's frame. Frames are kept on the call stack:

def inner():
    return 1

def outer():
    x = inner()
    return x + 1

print(outer())
while inner is running, the call stack is:

  +-------------------------+   <- top: running now
  | inner   (no locals)     |
  +-------------------------+
  | outer   x: (not yet)    |   paused at "x = inner()"
  +-------------------------+
  | <module>  (the script)  |   paused at "print(outer())"
  +-------------------------+

When inner returns 1, its frame is discarded and outer resumes exactly where it paused, now with the value 1 to bind to x. Then outer returns 2 and the module-level code resumes and prints it.

This is what a traceback shows you: the stack of frames at the moment an exception happened, oldest at the top ("most recent call last"):

Traceback (most recent call last):
  File "demo.py", line 9, in <module>
    print(outer())
  File "demo.py", line 5, in outer
    x = inner()
  File "demo.py", line 2, in inner
    return 1 / 0
ZeroDivisionError: division by zero

Read it bottom-up to find what went wrong, top-down to see how the program got there.

Because each call gets a fresh frame, local names in one call never collide with those of another call, even of the same function. That is what will make recursion (lesson 5) work.

Functions are objects

def is an ordinary statement that, when executed, creates a function object and binds a name to it, just as x = 5 binds x to an int object. So a function name is just a name like any other:

def shout(text):
    return text.upper() + "!"

yell = shout          # no parentheses: no call, just another name for the same object
print(yell("hi"))
print(type(shout))
print(shout)
HI!
<class 'function'>
<function shout at 0x7f3a2c1b8e00>

The address differs on each run. The parentheses are what make a call: shout is the function, shout("hi") is the result of calling it. You can pass functions as arguments to other functions:

def apply_twice(func, value):
    return func(func(value))

print(apply_twice(shout, "hey"))
HEY!!

This idea (functions as values) comes back in the iteration module with sorting keys, and in module 13 with closures and decorators.

Order of definition

Python executes a file top to bottom. A function must be defined (its def executed) before it is called, but the body can refer to functions defined later, because names in the body are looked up only when it runs:

def main():
    helper()          # fine: looked up when main() runs

def helper():
    print("helping")

main()                # by now both defs have executed

A common layout is: imports, then function definitions, then one call such as main() at the bottom.

Common mistakes

Forgetting the parentheses. print(greet) prints <function greet at 0x...> instead of calling it.

Calling before defining.

greet("Ada")
def greet(name):
    return "Hello, " + name
NameError: name 'greet' is not defined

Wrong number of arguments.

TypeError: greet() missing 1 required positional argument: 'name'
TypeError: greet() takes 1 positional argument but 2 were given

These messages name the function and say precisely what is wrong. Read them.

Printing where you meant to return, leading to None showing up later, or to TypeError: ... 'NoneType'. If you see None where you expected a value, look for a missing return.

Code after return. Lines after a return in the same block never run. Linters call this "unreachable code".

Recap

Going further: in the REPL, define a function and run import dis; dis.dis(name) to see its bytecode, including RETURN_VALUE. Also try help(len) to see how built-in functions document themselves; you will write such documentation in lesson 4.

Exercises

Exercise 1 · quiz

print is not return

What does this program print?

def double(n):
    print(n * 2)

result = double(5)
print(result)
Answer
  • 10 then None

Calling double(5) runs its body, which prints 10. The function has no return statement, so the call evaluates to None, and result is bound to None. Printing shows something to a human; returning hands a value back to the caller. They are different jobs, and a function that should produce a value must return it.

Exercise 2 · write code

Rectangle area

In solution.py, define a function area(width, height) that returns the area of a rectangle (width times height). It must not print anything.

For example area(3, 4) returns 12 and area(2.5, 2) returns 5.0.

File: solution.py

Starting point

def area(width, height):
    pass
Hint 1

pass does nothing, so the function returns None. Replace it.

Hint 2

return width * height

Reference solution
def area(width, height):
    return width * height

Exercise 3 · write code

Letter grade

Define letter_grade(score) that returns a one-letter string for a score from 0 to 100:

scorereturns
90 and above"A"
80-89"B"
70-79"C"
60-69"D"
below 60"F"

Use early return statements so the function stops as soon as it knows the answer.

File: solution.py

Starting point

def letter_grade(score):
    if score >= 90:
        print("A")
Hint 1

Check from the highest band down; the first matching return ends the function.

Hint 2

The last line, reached only when every test failed, is simply return "F".

Reference solution
def letter_grade(score):
    if score >= 90:
        return "A"
    if score >= 80:
        return "B"
    if score >= 70:
        return "C"
    if score >= 60:
        return "D"
    return "F"

Exercise 4 · write code

Watch the call stack

Complete the program so that its output is exactly:

main start
  outer start
    inner
  outer end
main end

main must call outer, and outer must call inner. Each function prints its own lines (with the indentation shown). The last line of the file calls main(). Do not print any of these lines outside the functions.

File: main.py

Starting point

def inner():
    print("    inner")

def outer():
    print("  outer start")
    print("  outer end")

def main():
    print("main start")
    print("main end")

main()

Example

expected output

main start
  outer start
    inner
  outer end
main end
Hint 1

A call pauses the current function until the called one returns, then continues on the next line.

Hint 2

Put outer() between main's two prints, and inner() between outer's two prints.

Reference solution
def inner():
    print("    inner")

def outer():
    print("  outer start")
    inner()
    print("  outer end")

def main():
    print("main start")
    outer()
    print("main end")

main()