◀ Course contents Part 3 · Module 3-01

Functions

Give a piece of work a name and stop repeating yourself

A loop stopped you repeating a line. A function stops you repeating an idea. Name a piece of work once, and every place that needs it says the name instead of the work — which means there is one place to fix it, one place to test it, and a program that reads as a description of what it does rather than a list of how.

Ready?

1

def Names Some Work; Calling Runs It

Defining a function does not run it. It writes the work down under a name and waits.

def line_total(quantity, price):
    return quantity * price

line_total(3, 1250)      # 3750
line_total(price=1250, quantity=3)   # the same, by name

quantity and price are parameters — names the function uses for whatever it is given. 3 and 1250 are arguments — the actual values at the call. The distinction is worth keeping straight because error messages use both words and mean different things by them.

Arguments go in by position, or by name. Naming them is longer and often much clearer: send(retry=True) tells the reader something that send(True) does not.

A parameter can have a default, which makes the argument optional:

def line_total(quantity, price, discount=0):
    return quantity * price * (1 - discount)

The body does not run until it is called

A function whose body has a bug in it will define perfectly happily. The error appears the first time something calls it — which is why an untested function is not a working one, however carefully it was written.

Quick check

What is the difference between a parameter and an argument?

2

return Hands a Value Back; print Does Not

This is the confusion that costs beginners the most time, and it is worth being blunt about.

def total_a(q, p):
    print(q * p)        # puts it on the screen

def total_b(q, p):
    return q * p        # hands it back to the caller

x = total_a(3, 1250)    # prints 3750, and x is None
y = total_b(3, 1250)    # prints nothing, and y is 3750

print is for a person. return is for the rest of the program. A function that prints has done something you cannot use, test, add up, or put in a file.

A function with no return hands back None — not an error, not zero, and not the last value it happened to calculate. So the mistake surfaces later, somewhere else: TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'.

Dead end

def f(x):
    print(x * 2)

The answer reached the screen and nothing else can reach it.

Usable

def f(x):
    return x * 2

Print it at the call if you want to. Or add it up, or store it.

The rule that follows: calculate in functions, print at the edges. A function that both works something out and decides how it should look is two functions in a trench coat.

Quick check

A function ends with print(total) and no return. What does the caller get?

3

Leaving Early, and Handing Back More Than One Thing

return ends the function immediately. That makes the guard clause from module 2-01 much cleaner inside a function — deal with the impossible case and leave, so the rest of the body has nothing to indent past:

def average(values):
    if not values:
        return 0          # nothing to average; done
    return sum(values) / len(values)

Without the early return that becomes an if/else wrapping the whole body. With three such checks it becomes four levels of indentation, and the actual work is the hardest thing on screen to find.

A function can hand back several values by returning a tuple — usually written without brackets, and usually unpacked at the call:

def summarise(values):
    return min(values), max(values), sum(values)

low, high, total = summarise(scores)

This is the same tuple unpacking from module 2-06, and it is where the habit pays off. Two or three values is comfortable; past that, a dictionary usually says more about what each one means.

Quick check

What happens to the lines after a return that runs?

4

What Makes a Function Worth Having

Three things, and none of them is length.

1

It does one job

If the name needs an "and" in it, it is two functions. parse_and_save cannot be tested without a database.

2

The name says what, not how

line_total, not multiply_two_numbers. The caller should not need to know the method.

3

Same input, same output

A function that only reads its parameters and returns a value can be tested in one line. One that reaches out to the clock, the network or a global cannot.

A docstring — a string as the very first thing in the body — says what the function is for. Tools read it, help() prints it, and it survives being moved in a way a comment above the def often does not:

def line_total(quantity, price):
    """Return the cost of one order line, before tax."""
    return quantity * price

Write what it returns and anything surprising. Do not write """Calculates the line total.""" — the name already said that, and a docstring that only repeats the name is a line everyone learns to skip.

The real test

Can you describe what the function does in one sentence, without using the word "and", and without mentioning where it is called from? If yes, it will be easy to test, easy to reuse, and easy to leave alone in six months.

Functions are not for saving typing. They are for giving a name to an idea so the code above can talk about it.

Quick check

Which name is doing its job?

0 of 9 completed

Real Python runs right here in your browser — nothing to install, nothing sent to a server. The interpreter downloads once the first time you press Run, then stays cached.

01

To do

def writes some work down under a name. Nothing runs until something calls it.

def greet(name):
    return f"Welcome, {name}"

print(greet("Kenji"))

name is a parameter — the name the function uses for whatever it is handed. "Kenji" is an argument — the value at the call.

Your task: write line_total(quantity, price) which hands back the two multiplied together, then print two calls:

3750
11880
your_code.py
Python
Hint

One line in the body: return quantity * price. The word return is what makes the answer available to the print outside.

Output

      
    02

    To do

    This function puts the right number on the screen and hands back None. So total below is None, and adding to it raises.

    print is for a person. return is for the rest of the program. A function that prints has produced something you cannot use, test, add up, or write to a file.

    Your task: run it, read the TypeError, then make the function return instead. The printing belongs at the call:

    3750
    3760
    your_code.py
    Python
    Hint

    Swap print for return inside the function. The two print() lines at the bottom then have a real number to work with.

    Output
    
          
      03

      To do

      A parameter with a default becomes optional. Callers who do not care get the sensible behaviour; callers who do can say so.

      def line_total(quantity, price, discount=0):
          return quantity * price * (1 - discount)

      Parameters with defaults must come after those without, because otherwise Python could not tell which value you meant to skip.

      Your task: add a discount parameter defaulting to 0, taken off the line as a proportion. Then print three calls — without a discount, with ten per cent passed by position, and with ten per cent passed by name:

      3750
      3375.0
      3375.0
      your_code.py
      Python
      Hint

      def line_total(quantity, price, discount=0): and then return quantity * price * (1 - discount). A discount of 0 leaves the total untouched, which is why 0 is the right default.

      Output
      
            
        04

        To do

        A function with no return hands back None — not zero, not an error, and not the last value it happened to calculate.

        That is the quiet part. The loud part arrives later, wherever the None is finally used, in a message that names a type nobody wrote down.

        Your task: run it, read where the error actually appears, then fix the function so the report prints:

        Subtotal: 17880
        your_code.py
        Python
        Hint

        The loop works out the right number and then the function ends without handing it over. One line at the end of the body: return running.

        Output
        
              
          05

          To do

          return ends the function on the spot. That makes a guard clause cleaner inside a function than anywhere else: handle the case that would break everything, leave, and the rest of the body has nothing to indent past.

          def average(values):
              if not values:
                  return 0
              return sum(values) / len(values)

          Your task: write average(values) which hands back 0 for an empty list and the mean otherwise. Print two calls:

          76.25
          0

          Use an early return rather than wrapping the work in an else.

          your_code.py
          Python
          Hint

          `if not values: return 0` catches the empty list, because an empty list is falsy. Then the last line is the ordinary calculation, unindented.

          Output
          
                
            06

            To do

            A function returns several values by returning a tuple, usually written without brackets and unpacked at the call. This is the tuple unpacking from module 2-06, doing the job it was built for.

            def summarise(values):
                return min(values), max(values), sum(values)

            low, high, total = summarise(scores)

            Your task: write summarise(values) handing back the lowest, the highest and the total, unpack the result into three names, and print them:

            54
            92
            305
            your_code.py
            Python
            Hint

            return min(values), max(values), sum(values) — the commas build a tuple. Then low, high, total = summarise(scores) takes it apart.

            Output
            
                  
              07

              To do

              A function is worth having the moment something calls it more than once — and a loop is the most common second caller.

              Notice what this buys: the rule for what a line costs lives in one place. Change it and every caller changes with it.

              Your task: using the line_total already written, print one row per line and then the order total:

              tea         3750
              whisk      11880
              cloth       2250
              Total      17880

              The name is left-aligned in ten characters and the figure right-aligned in six.

              your_code.py
              Python
              Hint

              Loop over rows, split each on the comma, call line_total(int(quantity), int(price)), print the row, and add the same value to order_total. A row is f"{name:<10}{value:>6}".

              Output
              
                    
                08

                To do

                Functions build on each other. Each one does its own job and trusts the one below it to do theirs, which is how a program stays readable as it grows.

                Your task: write two functions. line_total(quantity, price) as before, and order_total(rows) which walks the rows and uses line_total for each one. Then print two calls:

                17880
                250

                order_total must not do the multiplication itself. The rule for what a line costs lives in exactly one place.

                your_code.py
                Python
                Hint

                Start a running total at 0, loop over rows, split each into three, and add line_total(int(quantity), int(price)). Return the running total after the loop.

                Output
                
                      
                  09

                  To do

                  calc tells the reader nothing. A good name says what you get, not how it is worked out — change the method later and the name should still be true.

                  A docstring is a string as the very first thing in the body. Tools read it and help() prints it, which a comment above the def cannot claim.

                  Your task: rename calc to days_until_renewal and give it a docstring of at least twenty characters saying what it returns. Then print two calls:

                  26
                  0

                  Do not write a docstring that only repeats the name — say what comes back and what happens at the edge.

                  your_code.py
                  Python
                  Hint

                  Rename the function and both calls. The docstring is a plain string on the first line of the body, before `remaining` — something like "Return whole days left in the billing period, or 0 once it has passed."

                  Output
                  
                        

                    The invoice toolkit

                    To do

                    Three small functions and one report that uses them. The point of the exercise is the split: each function does one job, hands its answer back, and knows nothing about how the report is laid out.

                    Write these three, with exactly these names:

                    • line_total(quantity, price, discount=0) — the cost of one line, with the discount taken off as a proportion. Rounded to two decimal places.
                    • parse_line(row) — takes "tea,3,1250" and hands back the name, the quantity and the price, with the two numbers converted. Three values, one return.
                    • order_total(rows, discount=0) — the whole order. It must use the other two rather than repeating their work, and must hand back 0 for an empty list without touching anything else.

                    Every one of them needs a docstring of at least twenty characters, and none of them may print.

                    Then print the report: one row per line at ten per cent off, then the total.

                    tea         3375.0
                    whisk      10692.0
                    cloth       2025.0
                    Total      16092.0

                    Name left-aligned in ten, figure right-aligned in eight.

                    your_code.py
                    Python
                    Hint

                    line_total is round(quantity * price * (1 - discount), 2). parse_line splits on the comma and returns name, int(quantity), int(price) — three values, one return. order_total guards the empty list with an early return, then loops calling parse_line and line_total and adds them up. In the report, call parse_line for the name and line_total for the figure; a row is f"{name:<10}{value:>8}".

                    Output
                    
                          

                      Notification