Defining Functions

The Course
•
January 8, 2026
•
7 min read

A function gives a name to a piece of behaviour so you can use it more than once, and so a reader can understand your program without reading every line of it.

Warm-up: from Days 1 to 3

From memory first — the effort of recalling is the point.

1. Predict both lines.

Warm-up: predict Python
prices = {"tea": 2.5, "cake": 4.0}
prices["tea"] += 0.5

order = ["tea", "cake", "tea"]
total = 0
for item in order:
    total += prices[item]

print(total)
print(sorted(set(order)))
Check your prediction
10.0
['cake', 'tea']

Tea costs 3.0 after the update, and it was ordered twice: 3.0 + 4.0 + 3.0. The set drops the second "tea", and sorted puts the rest in order.

2. Fix. This should print each name with its score.

Warm-up: fix Python
scores = {"ada": 91, "alan": 78}
for name, score in scores:
    print(f"{name}: {score}")
Check your fix
scores = {"ada": 91, "alan": 78}
for name, score in scores.items():
    print(f"{name}: {score}")
ada: 91
alan: 78

Looping over a dictionary gives only its keys. Python then tried to unpack the key "ada" — three letters — into two names, and complained. .items() gives the pairs.

3. Write. Make a new list of the temperatures above 20 and print it: [24, 31, 27].

Warm-up: write Python
temps = [18, 24, 31, 27, 15]
Check your answer
temps = [18, 24, 31, 27, 15]
warm = []
for t in temps:
    if t > 20:
        warm.append(t)
print(warm)
[24, 31, 27]

Start empty, loop, append the ones you want. You’ll write this pattern in one line by the end of today.

Define, then call

Define, then call Python
def greet(name):
    return f"Hello, {name}!"

print(greet("Ada"))
print(greet("Alan"))

message = greet("Grace")
print(message.upper())

The anatomy: def, the name, the parameters in parentheses, a colon, then an indented body. return hands a value back to whoever called it.

return, not print

This distinction is worth getting right on day one of functions.

One is usable, one is not Python
def add_printing(a, b):
    print(a + b)          # shows the answer

def add_returning(a, b):
    return a + b          # hands the answer back

add_printing(2, 3)
result = add_printing(2, 3)
print("captured:", result)      # None - nothing was returned

result = add_returning(2, 3)
print("captured:", result)
print(add_returning(2, 3) * 10) # usable in a bigger expression

A function that prints can only ever put text on a screen. A function that returns can be used in arithmetic, stored, tested, and printed if you want to. Return values; print at the edges of your program.

Every function returns something

A function with no return returns None. That is why print(add_printing(2, 3)) shows None — you are printing the absence of a result.

Several parameters, and defaults

A parameter can have a default value, which makes it optional at the call site.

Defaults Python
def price_with_tax(amount, rate=0.2):
    return round(amount * (1 + rate), 2)

print(price_with_tax(100))          # uses the default rate
print(price_with_tax(100, 0.05))    # overrides it

def greet(name, greeting="Hello", punctuation="!"):
    return f"{greeting}, {name}{punctuation}"

print(greet("Ada"))
print(greet("Ada", "Welcome"))
print(greet("Ada", punctuation="?"))   # skip the middle one by naming

Parameters with defaults must come after those without — Python has no way to guess otherwise.

Never default to a list

def add_item(item, basket=[]): looks reasonable and is a genuine trap: the default list is created once, when the function is defined, and is shared by every call that uses it. Use basket=None and create a fresh list inside.

The shared-default trap Python
def broken(item, basket=[]):
    basket.append(item)
    return basket

print(broken("apple"))
print(broken("pear"))      # the apple is still there

def fixed(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

print(fixed("apple"))
print(fixed("pear"))

Docstrings

A string on the first line of a function is its documentation. Tools read it, help() prints it, and your future self will thank you.

Documenting a function Python
def body_mass_index(weight_kg, height_m):
    """Return the BMI for a weight in kilograms and a height in metres.

    The result is rounded to one decimal place.
    """
    return round(weight_kg / height_m ** 2, 1)

print(body_mass_index(70, 1.75))
help(body_mass_index)

Say what it returns and what the arguments mean. Do not restate the code.

Functions calling functions

Small functions that call each other is how programs stay readable as they grow.

Building up Python
def is_vowel(letter):
    return letter.lower() in "aeiou"

def count_vowels(text):
    count = 0
    for letter in text:
        if is_vowel(letter):
            count += 1
    return count

def describe(text):
    vowels = count_vowels(text)
    return f"{text!r} has {vowels} vowels out of {len(text)} characters"

print(describe("Programming"))
print(describe("rhythm"))

Each function does one thing and is testable on its own. {text!r} in the f-string means “show the repr” — the quoted, debugging-friendly form.

Before you write your own

Predict. Three lines are printed. Watch which function prints and which returns — and the order things happen in.

What will this print? Python
def double(n):
    return n * 2

def shout(word):
    print(word.upper())

x = double(4)
y = shout("hi")
print(x, y)
print(double(double(3)))
Check your prediction
HI
8 None
12

HI comes first because shout prints the moment it’s called — on the y = ... line. It returns nothing, so y is None. And a returned value can go straight into another call: double(3) is 6, and double(6) is 12.

Fix. The average should be usable in the f-string, giving Average: 4.0.

An answer you can't use Python
def average(values):
    print(sum(values) / len(values))

result = average([3, 4, 5])
print(f"Average: {result}")
Check your fix
def average(values):
    return sum(values) / len(values)

result = average([3, 4, 5])
print(f"Average: {result}")
Average: 4.0

The original printed 4.0 itself and then handed back None. Return the value and let the caller decide what to do with it.

Exercise

A tiny statistics toolkit

Write three functions — mean(values), largest(values) and range_of(values) (the difference between the largest and smallest) — then use them to report on the list. Each must return, not print. The numbers you should get:

Mean:    22.5
Largest: 30
Range:   12
readings = [18, 21, 25, 19, 30, 22]

# Your code here
Hint 1: the idea

Three small functions, each taking the whole list as its one parameter and returning one number. Inside them, sum, len, max and min do the work. The report is then three print calls that use the functions.

Hint 2: a first step

Write only mean first, and check print(mean(readings)) shows 22.5 before you start the others. A mean is the total divided by how many values there are.

Show one solution
readings = [18, 21, 25, 19, 30, 22]

def mean(values):
    """Return the arithmetic mean of a non-empty sequence of numbers."""
    return sum(values) / len(values)

def largest(values):
    """Return the biggest value."""
    return max(values)

def range_of(values):
    """Return the spread between the largest and smallest values."""
    return max(values) - min(values)

print(f"Mean:    {mean(readings):.1f}")
print(f"Largest: {largest(readings)}")
print(f"Range:   {range_of(readings)}")

largest is a thin wrapper over max and would not be worth writing in real code — but naming the operations you care about is exactly how a domain vocabulary starts.

What you learned

  • def name(parameters): defines a function; calling it runs the body.
  • return sends a value back; without it you get None.
  • Return values rather than printing them, so callers can use the result.
  • Default parameters make arguments optional — never default to a mutable value.
  • A docstring documents what the function returns and what its arguments mean.
Last updated on January 8, 2026

Was this article helpful?

Your response is saved on this device.