South Plantation High School logo
South Plantation High School · Procedural Programming with Python

Functions & Recursion

def · return & None · Docstrings · help() & pydoc · Recursion · Lesson review

Review the lesson slides, then complete the assignment at the bottom of this page. Your assignment work is saved in this browser on this computer. When you check a task, your name, period, and which tasks are complete are shared with your teacher (your code is not). When you finish, print it or save it as a PDF to turn in.
1 / 17
Lesson Review

Functions: def, return, Docstrings & Recursion

def greet(name):return x * 2None"""Docstring."""help(greet)python -m pydoc

A function is a named, reusable block of code. Write it once, then call it whenever you need it.

By the end of this lesson you will be able to:

  • Define your own function with def and invoke (call) it.
  • Tell the difference between a function that returns a value and one that implicitly returns None.
  • Write a docstring for a function and view it with help() or pydoc.
  • Describe recursion in your own words using a simple example.
How to use this page: Use Next and Back (or the arrow keys). Click Step in the editors to follow the program as it jumps into a function and back out again. The Call stack panel shows which function calls are running.

Words you will hear today

WordWhat it meansExample
defineCreate a function with defdef greet():
call / invokeRun a function by writing its name and parenthesesgreet()
parameterA name in the def line that receives a valuename in def greet(name):
argumentThe value you pass in when you call"Ava" in greet("Ava")
return valueWhat the call gives back12 from area(3, 4)
docstringA description string at the top of the function"""Return the area."""
recursionA function that calls itselffactorial(n - 1)
  • def, call, return vs None, docstrings, help and pydoc, recursion.
  • Define, call, parameter, argument, return value, docstring, recursion.

You have been calling functions since the first day: print, input, len, range. Today you build your own. By the end you will know how to send information into a function, get an answer back out, write instructions for the people who use your function, and even write a function that calls itself.

def

Anatomy of a function

The tutorial: "The keyword def introduces a function definition. It must be followed by the function name and the parenthesized list of formal parameters. The statements that form the body of the function start at the next line, and must be indented."

def area(width, height):        # header
    """Return the area of a rectangle."""
    result = width * height    # body (indented)
    return result              # send the answer back

print(area(3, 4))              # call: prints 12
PartIn the example
Keyworddef
Function namearea (same naming rules as variables)
Parameterswidth, height inside the parentheses
Colon and indented bodyeverything indented under the header
Docstring (optional)the """...""" string on the first body line
return (optional)the value the call gives back

Why write functions?

  • Reuse: write the steps once, call them many times.
  • Name a task: area(3, 4) says what it does.
  • Fix once: a bug fixed in the function is fixed everywhere it is called.
  • Break big problems into small, testable pieces.

You have been calling functions all year

print(), len(), input(), int(), and range() are all functions that someone else wrote. Now you get to write your own, and they work exactly the same way: a name, parentheses, arguments in, and (often) a value out.

A function is like a recipe card

Writing the card (def) does not cook anything. Cooking happens when you follow the card (call it), and you can follow the same card as many times as you want with different ingredients (arguments).

Naming tip: start function names with a verb, since functions do things: calculate_tax, print_report, is_even.

  • def name(parameters): then an indented body.
  • Docstring and return are optional.
  • Function names should start with a verb.

Every function has the same shape: the word def, a name, parentheses with parameters, a colon, and an indented body. Compare the two versions in the editor: one repeats the same three lines over and over, the other puts them in a function and calls it. Same output, much less code to fix later.

def

Defining is not running: you must call it

  • Running a def line only creates the function and stores it under its name. The body does not run yet.
  • To invoke (call) the function, write its name followed by parentheses: greet().
  • When a function is called, Python jumps to its body, runs it, then comes back to the line that called it.
  • A function must be defined before the line that calls it runs.
def greet():
    print("Hello!")
    print("Welcome to class.")

greet()     # runs the body
greet()     # runs it again
greet       # no parentheses: nothing happens
Click Step and watch the highlight jump from the call into the function and back.

The order Python runs things

def greet():              # 1. creates greet; body skipped for now
    print("Hello!")       # 4. and 6. run only during a call
print("Start")            # 2. the first thing printed
greet()                   # 3. jump into greet, then come back
greet()                   # 5. jump in again, then come back
print("End")              # 7.

Python reads top to bottom, but a def just stores the function. The body runs only when the program reaches a call. After the body finishes, Python returns to the line right after the call and keeps going.

Without parentheses, greet just refers to the function, like saying a recipe's name without cooking it. In the interactive shell it shows something like <function greet at 0x...>.

  • def creates, name() calls.
  • Define before you call.
  • After the body finishes, Python returns to the line after the call.

A def is like writing a recipe card. Writing the card does not cook anything. Cooking happens when you call the recipe by name with parentheses. Step through the options and watch where the green highlight goes. Try calling before defining, and try forgetting the parentheses.

Parameters

Parameters and arguments

  • A parameter is a name in the def line: name, greeting.
  • An argument is the value you pass in the call: "Ava", "Hi".
  • When the call starts, each parameter is set to its argument, like an assignment.
def greet(name, greeting="Hello"):
    return greeting + ", " + name + "!"

greet("Ava")                    # 'Hello, Ava!'
greet("Ava", "Hi")              # 'Hi, Ava!'
greet(greeting="Yo", name="Cy") # 'Yo, Cy!'
KindExampleRule
Positionalgreet("Ava", "Hi")matched in order
Keywordgreet(name="Ava")matched by name, any order
Defaultgreeting="Hello"used when you leave it out; defaults go last in the def

What happens at the moment of the call

For greet("Ava", "Hi"), Python does the equivalent of:

name = "Ava"        # first argument to first parameter
greeting = "Hi"     # second argument to second parameter
# ...then runs the body

The argument can be any expression: a variable, math, or even another call. greet(input("Name? ")) works: Python gets the input first, then passes it in.

Variable names do not have to match. If you call greet(student), the value of student is copied into the parameter name. The names outside and inside the function are separate.

  • Parameter = name in def. Argument = value in call.
  • Wrong count = TypeError.
  • A call assigns each argument to its parameter, then runs the body.

Parameters are the blanks; arguments fill in the blanks. Python matches them by position unless you name them. Try the calls with too few or too many arguments, and read the error carefully: it names the function and the missing parameter.

return

return sends a value back

The tutorial: "The return statement returns with a value from a function." The call then becomes that value, so you can store it, print it, or use it in math.

def add(a, b):
    return a + b          # gives the answer back

def show_sum(a, b):
    print(a + b)          # only displays it

total = add(2, 3)         # total is 5
print(total * 10)         # 50

result = show_sum(2, 3)   # prints 5
print(result)             # None
print vs. return: print shows something on the screen for a person. return hands a value back to your program. A function can do both, one, or neither.

return also ends the function right away. Any lines after it in that function do not run.

Why return is more useful than print

A returned value can be used again. You can combine calls in bigger expressions, store results, or pass one function's answer into another:

total = area(3, 4) + area(1, 2)    # 12 + 2 = 14
big = area(area(2, 3), 2)          # area(6, 2) = 12

A function that only prints is a dead end: the answer appears on screen, but your program cannot do anything else with it.

Returning more than one value

return low, high sends back a tuple. Unpack it at the call: smallest, largest = min_max(scores).

Using return to stop early:

def check_age(age):
    if age < 0:
        return "Invalid age"   # the function ends here
    return "OK"
  • return gives a value back and ends the function.
  • print only displays.
  • Returned values can be stored, combined, and passed on.
  • return a, b sends back a tuple.

This is the most important idea today. When a function returns a value, the call itself turns into that value, like a calculator showing an answer you can keep using. A function that only prints shows the answer to a person, but your program cannot use it. Compare the add and show_sum options.

None

No return? You get None

The tutorial: "even functions without a return statement do return a value, albeit a rather boring one. This value is called None." Also: "return without an expression argument returns None. Falling off the end of a function also returns None."

The function...The call gives back
runs return valuethat value
runs a bare returnNone
reaches the end with no returnNone
only returns inside an if that is FalseNone

Built-ins work the same way: print() returns None, and so does nums.sort() (it changes the list but gives back nothing). sorted(nums) returns a new list.

How None sneaks in

The most common way is a function with return only inside an if:

def grade(score):
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"

print(grade(75))    # None, because no return ran

Make sure every path through the function reaches a return. Adding an else: return "C or lower" (or a final return at the end) fixes it.

Spotting it: if you see None printed where you expected a value, or an error mentioning NoneType, a function on that line probably did not return anything.

  • No return, bare return, or falling off the end = None.
  • None + 1 is a TypeError.
  • Every path through a function should reach a return.

Every call gives something back. If the function never says what, Python quietly hands back None, which means "nothing." That is fine if you only wanted the side effect, like printing. It becomes a bug when you try to use the result, like adding one to it. Try the option where return is inside an if.

Practice

Predict the output: value or None?

For each program, choose what Python prints, then click Check. Practice only; nothing is recorded.

How to predict

  1. Find each call and trace into the function with the argument values.
  2. Follow the ifs to see which lines actually run.
  3. Did a return value run? That is the result. Otherwise the result is None.
  4. Remember a print inside the function shows its text during the call, before the caller prints the result.
  • Look for a return on the path that actually runs.

For each one, follow the call into the function and ask: which line runs last? If it is a return with a value, that is the answer. If the function runs out of lines, or hits a bare return, the answer is None. Remember that printing inside the function also shows up on the screen.

Scope

Local variables live inside the call

  • Parameters and variables created inside a function are local: they exist only while that call runs.
  • Each call gets its own fresh set of local variables.
  • A function can read a variable made outside it (a global).
  • If a function assigns to a name anywhere in its body, that name is local for the whole function.
def double(n):
    answer = n * 2      # local
    return answer

print(double(5))       # 10
print(answer)          # NameError: name 'answer' is not defined
Best practice: pass values in as arguments and send results out with return. Python has a global statement, but functions that only use their parameters are easier to test and reuse.

Why local variables are a good thing

Because each function keeps its own variables, you can use a name like total in ten different functions without them interfering with each other. Each function is a closed box: values go in through parameters and come out through return.

The UnboundLocalError trap

score = 0
def add_point():
    score += 1      # error: score is local here, but has no value yet

Because the function assigns to score, Python treats it as local, so it cannot read the outside score first. The clean fix is to pass it in and return it:

def add_point(score):
    return score + 1
score = add_point(score)
  • Locals disappear when the call ends.
  • Assigning inside makes a name local: UnboundLocalError if read first.
  • Pass values in and return results instead of changing globals.

Think of each call as a separate scratch pad. The function writes on its own pad, and when it returns, the pad is thrown away. Only the returned value survives. Watch the Variables panel heading change between Global variables and Variables in a function as you step.

Docstrings

Docstrings: instructions for the user

PEP 257: "A docstring is a string literal that occurs as the first statement in a module, function, class, or method definition. Such a docstring becomes the __doc__ special attribute of that object."

def perimeter(width, height=1):
    """Return the perimeter of a rectangle.

    If height is left out, it is 1.
    """
    return 2 * (width + height)
  • It must be the first line of the body, or it is just an ordinary string.
  • PEP 257: "For consistency, always use """triple double quotes""" around docstrings."
  • Start with a one-line summary written as a command: "Return the...", not "Returns the...".
  • For more detail, add a blank line, then the description.
  • A comment (#) is for people reading the code. A docstring is for people using the function, and Python can show it to them.

Write your own docstring and see what help() shows.

What makes a good docstring

  • Say what it returns or does, in one sentence: "Return the average of a list of scores."
  • Mention the parameters if it is not obvious what they are: "scores is a list of numbers."
  • Mention special cases: "Return 0 if the list is empty."
def average(scores):
    """Return the average of a list of numbers.

    Return 0 if scores is empty.
    """
    if len(scores) == 0:
        return 0
    return sum(scores) / len(scores)

Someone should be able to use your function correctly by reading only the docstring.

  • First statement, triple double quotes, command-style summary.
  • A good docstring lets someone use the function without reading its code.

A docstring is a note you leave for anyone who uses your function, including future you. Because it is the first line of the body, Python stores it with the function. Type a summary in the lab, then try the option that moves the string below the first statement: watch the help text lose it.

help()

Reading docstrings with help() and __doc__

The pydoc docs: "The built-in function help() invokes the online help system in the interactive interpreter, which uses pydoc to generate its documentation as text on the console."

>>> help(area)
Help on function area in module __main__:

area(width, height)
    Return the area of a rectangle.

>>> area.__doc__
'Return the area of a rectangle.'
  • Pass the function itself, without parentheses: help(area), not help(area()).
  • The first line shows the signature: the name and its parameters (with any defaults).
  • No docstring? help() still shows the signature, and __doc__ is None.
  • It works on built-ins too: try help(len) in real Python. (In the interactive shell, press q to leave a long help page.)

Why write docs you can print?

Professional programmers use other people's functions all day without reading their code. They type help(something) to find out what it does and what arguments it needs. When your functions have docstrings, they work the same way for your classmates (and for future you).

Try it on built-ins: help(print) shows that print has options like sep and end. help(str.upper) explains the string method you already know.

  • help(fn) shows the signature and docstring.
  • fn.__doc__ is the raw string or None.
  • help() works on built-ins like print and str.upper too.

help is how Python reads your docstring back to you. It prints the function's name, its parameters, and then the docstring, indented. This works for every function in Python, which is why good programmers write docstrings: their functions explain themselves.

pydoc

pydoc: documentation from the command line

The docs: "The pydoc module automatically generates documentation from Python modules." Run it from a terminal in the folder with your file, giving the module name (the file name without .py):

CommandShows
python -m pydoc shapesevery function in shapes.py with its docstring
python -m pydoc shapes.areaone function
python -m pydoc -w shapeswrites the docs to an HTML file
python -m pydoc -p 1234serves the docs to your web browser
Heads up: pydoc imports your file to read it, so any code at the left edge runs. The docs: "any code on module level will be executed on that occasion." Keep test calls out of files you document this way.

The module's own docstring (the first line of the file) appears after NAME.

Keeping test code from running

Programmers often put test calls under a special if so they run only when the file is run directly, not when it is imported (by pydoc or another program):

def area(width, height):
    """Return the area of a rectangle."""
    return width * height

if __name__ == "__main__":
    print(area(3, 4))     # runs only when you run this file itself

__name__ is a special variable that is "__main__" when the file is the one you ran, and the module's name when it is imported.

  • python -m pydoc modulename. Module name = file name without .py.
  • if __name__ == "__main__": keeps test code from running on import.

help works inside Python. pydoc does the same job from the terminal, and it can document a whole file at once. Notice how the functions are listed in alphabetical order and every docstring shows up. The function with no docstring shows only its signature.

Recursion

Recursion: a function that calls itself

A recursive function solves a problem by calling itself on a smaller version of the same problem.

def countdown(n):
    if n == 0:                 # base case
        print("Liftoff!")
        return
    print(n)
    countdown(n - 1)           # recursive case

countdown(3)
PartJob
Base casethe simplest version, answered directly with no more calls; it makes the recursion stop
Recursive casedoes a little work, then calls the function again on a smaller input
Picture nesting dolls: open one, and there is a smaller one inside, until you reach the tiny doll that does not open. That tiny doll is the base case.

Trace countdown(3)

  1. countdown(3): 3 is not 0, so it prints 3 and calls countdown(2).
  2. countdown(2): prints 2, calls countdown(1).
  3. countdown(1): prints 1, calls countdown(0).
  4. countdown(0): base case! Prints Liftoff! and returns.
  5. Each earlier call then finishes, in reverse order: 1, then 2, then 3.

Two questions for any recursive function: "What is the smallest case I can answer right away?" (the base case) and "How do I make the problem one step smaller?" (the recursive case).

  • Base case stops it. Recursive case moves toward the base case.
  • Ask: what is the smallest case, and how do I make it smaller?

Recursion sounds strange at first: how can a function use itself before it is finished? The trick is that each call works on a smaller problem, and eventually the problem is so small that the answer is obvious. Watch the call stack grow as countdown calls itself, then shrink as each call finishes.

Recursion

Tracing a recursive call: factorial

5! (5 factorial) means 5 × 4 × 3 × 2 × 1. Notice that 5! = 5 × 4!: the same problem, one size smaller.

def factorial(n):
    """Return n! for a whole number n of 1 or more."""
    if n == 1:
        return 1                      # base case
    return n * factorial(n - 1)       # recursive case
Going down (calls)Coming back up (returns)
factorial(4) waits for factorial(3)returns 4 * 6 = 24
factorial(3) waits for factorial(2)returns 3 * 2 = 6
factorial(2) waits for factorial(1)returns 2 * 1 = 2
factorial(1) hits the base casereturns 1

Each waiting call is a frame on the call stack. The last call made is the first to finish.

The "trust the smaller call" idea

When writing return n * factorial(n - 1), do not try to picture every call at once. Just trust that factorial(n - 1) gives the right answer for the smaller problem, and ask: "If I had that answer, how would I finish the job?" For factorial, multiply by n.

Another example: adding up a list

def total(nums):
    """Return the sum of a list of numbers."""
    if nums == []:
        return 0                        # empty list: sum is 0
    return nums[0] + total(nums[1:])    # first item + sum of the rest

print(total([4, 5, 6]))    # 15

The list gets one item shorter on every call, so it is guaranteed to reach the empty-list base case.

  • Calls stack up going down; answers multiply on the way back up.
  • Trust that the smaller call works, then finish the job.

Step slowly through factorial of 4. Each call has to wait for the one below it, so the stack grows four frames tall. When the base case returns 1, the answers flow back up: 2, then 6, then 24. That "go down, then come back up" shape is the heart of recursion.

Recursion

When recursion never stops

If the base case is missing, or the input never gets closer to it, the function keeps calling itself. Python stops it with:

RecursionError: maximum recursion depth exceeded
  • Python's limit is 1,000 by default (sys.getrecursionlimit() returns 1000).
  • Every recursive function needs a base case and a step toward it, like n - 1.
  • Check what happens with unexpected input: factorial(0) never reaches n == 1. Writing if n <= 1: fixes it.
  • Anything recursion can do, a loop can also do. Recursion shines when a problem contains smaller copies of itself.

To keep the demo quick, this practice terminal stops much sooner than real Python does.

Check every recursive function with this list

  1. Is there a base case that returns without calling itself?
  2. Does every call move closer to it? (n − 1 heading down to 1, a list getting shorter.)
  3. Can any input skip past it? n == 1 misses 0 and negative numbers; n <= 1 catches them.

The loop version

def factorial(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

Same answers, no call stack. For simple counting problems, a loop is usually the more practical choice; recursion is a powerful way of thinking that becomes very useful for problems shaped like trees, mazes, and folders inside folders.

  • No base case or no progress = RecursionError.
  • Base case exists, every call moves toward it, no input skips it.

An infinite loop runs forever, but infinite recursion cannot, because every waiting call takes up memory. Python sets a limit and raises RecursionError when you hit it. When you see that error, ask two questions: do I have a base case, and does every call move closer to it?

Debugging

Common mistakes

MistakeCodeWhat happens (Python 3.13)Fix
Forgot the parenthesesgreetNo error. The function does not rungreet()
Calling before the defgreet() above def greet():NameError: name 'greet' is not definedPut the def first
Missing an argumentarea(3)TypeError: area() missing 1 required positional argument: 'height'area(3, 4)
Too many argumentsarea(3, 4, 5)TypeError: area() takes 2 positional arguments but 3 were givenMatch the parameters
print instead of returntotal = show_sum(2, 3)No error. total is NoneUse return
Using a None resultshow_sum(2, 3) + 1TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'Return a value
Using a local outsideprint(answer) after the callNameError: name 'answer' is not definedStore the returned value
Reading a global you also assignscore += 1 inside a functionUnboundLocalError: cannot access local variable 'score' where it is not associated with a valuePass it in and return it
Missing indentationbody not indentedIndentationError: expected an indented block after function definition on line 1Indent the body 4 spaces
Docstring not firststring on the second body lineNo error. __doc__ is NoneMake it the first statement
No base caserecursion with no stopRecursionError: maximum recursion depth exceededAdd a base case

Try these at the prompt. After a def line, type the body, then press Enter on an empty line to finish:

  • def double(n):
  • """Return n times 2."""
  • return n * 2
  • (empty line)
  • double(21)
  • help(double)
  • x = print("hi") then print(x)

Sort the error

  • NameError: the function is called before its def, the name is misspelled, or you used a local variable outside its function.
  • TypeError ... argument(s): count the arguments in the call and the parameters in the def.
  • TypeError ... NoneType: a function returned None (no return on that path, or it only printed).
  • RecursionError: check the base case and the step toward it.
  • Nothing happens: missing parentheses on the call.
  • Parentheses, argument counts, return vs print, scope, docstring position, base case.
  • NoneType in an error: a function did not return a value.

Three of these give no error at all: forgetting the parentheses, printing instead of returning, and putting the docstring in the wrong place. Those are the hardest to find, because the program runs and just does the wrong thing. When a result is None and you expected a value, look for a missing return.

Practice

Check yourself

Choose an answer, then click Check. Practice only; nothing is graded or recorded.

For each question:
  • Is the function actually called (with parentheses)?
  • Which parameter gets which argument?
  • Which return runs? If none, the answer is None.
  • For recursion, write each call on its own line, then fill in the answers from the bottom up.
  • Trace each call; look for the return on the path that runs.

For each question, find the call, jump into the function, and follow the lines that actually run. Ask yourself what the call gives back. If you never see a return with a value, the answer is None.

Summary

Cheat sheet

Define and call

def area(w, h=1):
    """Return w times h."""
    return w * h

area(3, 4)        # 12
area(3)           # 3 (h=1)
area(h=2, w=5)    # 10
x = area(2, 2)    # store it

return and None

  • return value gives a value back and ends the call
  • No return or a bare return gives None
  • print shows; return hands back
  • Locals vanish when the call ends

Docs

help(area)
area.__doc__
# terminal:
python -m pydoc shapes

Recursion

def factorial(n):
    if n <= 1:        # base case
        return 1
    return n * factorial(n - 1)
  • A base case that stops
  • A smaller input on every call
  • Missing either: RecursionError

Remember:

  • def creates a function; parentheses call it
  • Parameters are names in the def; arguments are values in the call
  • If you need the answer later, return it, do not just print it
  • Put a """docstring""" on the first line of every function
  • Every recursive function needs a base case and a step toward it

Key words to know

def · call / invoke · parameter · argument (positional, keyword, default) · return · None · local / global · docstring · help() / pydoc · recursion · base case · call stack

  • def, return, None, docstrings, help/pydoc, recursion.
  • Know the key words: parameter, argument, return, None, base case.

Let's wrap up. A function is a named block of code you define once with def and call as many times as you like. Use return to hand back an answer; without it you get None. Document every function with a docstring so help and pydoc can explain it. And remember that recursion is just a function calling itself on a smaller problem until it reaches a base case.