1 Before you start
Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.
What does a function return if you forget to write a return statement?
You define def greet(name, greeting="Hello"): return f"{greeting}, {name}". Which call uses keyword arguments correctly?
Given def greet(name, greeting="Hi"): return f"{greeting}, {name}", what does greet("Sam") return?
Which is equivalent to def square(x): return x**2?
What does sorted([5, -3, 1], key=lambda x: abs(x)) return?
You define def f(*args): print(args). What type is args inside the function?
You define def f(**kwargs): print(kwargs). What type is kwargs inside the function?
A variable is created inside a function with an ordinary assignment (no global keyword). After the function returns, what happens to it?
I can write a function with parameters and a default value, and call it using either positional or keyword arguments.
I can write a lambda to use as the key= argument of sorted, max, or min.
I can read a function signature that uses *args or **kwargs and tell what kind of inputs it accepts.
2 Introduction
Up to now every program you have written lives in one block. You assign variables, run a loop, print a result. As soon as your scripts get longer or you want to reuse the same logic, you need a way to package a piece of behaviour under one name and call it whenever you like. That is what a function does.
This is the first of two parts. Part I covers functions themselves — how to define them, pass arguments, set defaults, write anonymous lambdas, and reason about which variables a function can see. Part II covers reading and writing files, and accepting input from the command line.
This part covers five tools:
- Functions and parameters — define a piece of behaviour once, call it from anywhere with whatever inputs you want.
- Positional vs keyword arguments — two ways of telling a function which value goes where, and when each is better.
- Anonymous functions with
lambda— one-line functions forsortkeys,max/min, and quick transforms. - Flexible arguments with
*argsand**kwargs— letting a function accept any number of positional or keyword arguments. - Variable scope — why a variable defined inside a function vanishes when the function returns, and what that buys you.
Try every snippet in the Python Scratchpad on the right. By the end of this part you will write a function with a default value, a lambda passed to sorted, and a small function that takes *args.
3 Functions and parameters
You compute a BMI. Then a few lines later you compute another one. Then in a different file, for a different patient, you compute another. Each time you write the same expression, weight_kg / (height_m ** 2), and each time there is a small risk of typing it wrong.
A function lets you write that expression once, give it a name, and call it whenever you need the result.
A function definition starts with the word def, then the name you want to give it, then a list of parameters in round brackets, then a colon. The lines that make up the function's body are indented by four spaces, just like the body of an if or a for loop. The word return tells Python what value to send back to whoever called the function.

Try and create your own functions!
Try this snippet in the Python Scratchpad on the right.
def calculate_bmi(weight_kg, height_m):
return weight_kg / (height_m ** 2)
print(calculate_bmi(72, 1.75))
Three pieces of vocabulary that get used interchangeably in conversation but mean slightly different things.
- A parameter is a name in the function's definition — the
weight_kgandheight_mabove. - An argument is the actual value you pass in when you call the function — the
72and1.75above. - A return value is what the function hands back. If you forget the
return, the function still runs but it hands backNone, and any code that tried to use the result will quietly behave strangely.
Functions can take any number of parameters, including zero. When you call the function, you pass that many values, in the same order. If you pass too few or too many, Python raises a TypeError before the function body even runs.
The code below is broken. Type a fixed version into the editor, then click Run & check. Success means your code runs without errors and produces output. Use Show hint only if you get stuck.
def add(a, b):
return a + b
print(add(3))
- Pass two values when you call the function: add(3, 5) ★
- Replace return with print inside the function
- Remove the colon at the end of the def line
A small habit worth forming early: write a one-line description of what the function does on the line right under def, wrapped in triple quotes. That string is called a docstring, and Python treats it specially — help(calculate_bmi) will show it, and editors like VS Code pop it up as a tooltip when you hover the function name. Future-you, six months from now, trying to remember what this function does, will thank you.
def calculate_bmi(weight_kg, height_m):
"""Return body-mass index from weight in kg and height in metres."""
return weight_kg / (height_m ** 2)
help(calculate_bmi) # shows the docstring4 Positional vs keyword arguments
Once a function takes more than two parameters, calling it gets risky.
The code calculate_energy(72, 1.75, 54, "M") is fine if you remember the order, but is the third number the age or the heart rate? Did "M" mean male or metric? Python gives you a second way to pass arguments that removes the guessing.
When you write add(3, 5), the values 3 and 5 are positional arguments. Python matches them to the parameters by their position. The first value goes to the first parameter, the second value to the second, and so on. That is what you have been doing so far.
The second call uses keyword arguments — you write the parameter name, then =, then the value. The two calls do exactly the same thing, but the second one is self-documenting: anyone reading the call sees which value is which. For a function with two parameters that is overkill; for a function with five it is the difference between readable code and a guessing game.

You can mix the two styles in a single call, but positional arguments must come first. calculate_bmi(72, height_m=1.75) is fine. calculate_bmi(weight_kg=72, 1.75) is a SyntaxError — once you start using keywords, you have to stick with keywords for the rest of the call.
The code below is broken. Type a fixed version into the editor, then click Run & check. Success means your code runs without errors and produces output. Use Show hint only if you get stuck.
def calculate_bmi(weight_kg, height_m):
return weight_kg / (height_m ** 2)
print(calculate_bmi(weight_kg=72, 1.75))
- Make both arguments keyword so order no longer matters: print(calculate_bmi(weight_kg=72, height_m=1.75)) ★
- Swap to positional first, keep the keyword second: print(calculate_bmi(1.75, weight_kg=72))
- Give the first parameter a default in the definition: def calculate_bmi(weight_kg=72, height_m):
The other reason keyword arguments matter is default values. When you define a parameter with an = and a value in the def line, you are saying "if the caller does not pass this, use this value instead." Default values must come after non-default parameters in the signature.
Try and practice writing functions below:
Try this snippet in the Python Scratchpad on the right.
def prescribe(patient_id, drug, dose_mg, frequency="daily", route="oral"):
print(f"{patient_id}: {dose_mg}mg {drug}, {frequency}, {route}")
prescribe("P01", "metformin", 500)
prescribe("P02", "heparin", 5000, "twice daily")
prescribe("P03", "insulin", 10, route="subcutaneous")
5 Anonymous functions with lambda
When you only need a tiny throwaway function - one expression, used once - giving it a name with def is overkill.
Python lets you write the function inline with lambda. The shape is lambda parameters: expression. It is a function value with no name attached. The most common place you will see lambda is as the key= argument to sorted, max, or min, where you want to tell Python which part of each item to compare on.
Here is a simple example to illustrate:
Try this snippet in the Python Scratchpad on the right.
patients = [("Alice", 27.3), ("Bob", 22.1), ("Chen", 31.5)]
patients.sort(key=lambda p: p[1])
print(patients)
And a slightly more complicated version:
Try this snippet in the Python Scratchpad on the right.
patients = [
{"id": "P001", "age": 54},
{"id": "P002", "age": 38},
{"id": "P003", "age": 72},
]
by_age = sorted(patients, key=lambda p: p["age"])
print(by_age)
oldest = max(patients, key=lambda p: p["age"])
print(oldest)
square = lambda x: x ** 2
def square_def(x):
return x ** 2
print(square(5), square_def(5))

6 Flexible arguments with *args and **kwargs
Sometimes you want a function to accept any number of positional arguments, or any number of keyword arguments. Python lets you do this with two special parameter forms:
*argscollects any extra positional arguments into atuple**kwargscollects any extra keyword arguments into a dictionary.
The names args and kwargs are just conventions; the * and ** operators do the heavy lifting. You’ll see this pattern across almost all machine learning libraries, allowing a function to accept a few specific parameters while using a catch-all to forward any remaining arguments it doesn't process itself.

Let’s practice the syntax.
All the lines you need are in the Line bank on the left — some may be distractors you should leave behind. Drag the lines you need into the Your solution column on the right, in the correct order, then click Check.
Task: Define a function that takes a patient ID, any number of symptoms, and any number of named admission details, then call it for one patient.
admit_patient("P042", "fever", "cough", ward="A3", priority="urgent")def admit_patient(patient_id, *symptoms, **details):print(f"Symptoms: {symptoms}")print(f"Admitting {patient_id}")admit_patient("P042", ward="A3", "fever", "cough")print(f"Symptoms: *symptoms")print(f"Details: {details}")def admit_patient(patient_id, **details, *symptoms):
- Drop lines here, in order.
Try the following code now:
Try this snippet in the Python Scratchpad on the right.
def mean(*values):
return sum(values) / len(values)
print(mean(54, 61, 47, 72))
print(mean(1, 2, 3))
def describe_patient(**fields):
for key, value in fields.items():
print(f"{key}: {value}")
describe_patient(id="P001", age=54, weight_kg=72)
# Combining both - the standard signature you will see in ML libraries
def fit(model, X, y, *args, **kwargs):
print("model:", model)
print("extra positional:", args)
print("extra keyword:", kwargs)
fit("logreg", [[1,2]], [0], "fast", verbose=True, max_iter=100)
7 Variable scope (local and global)
A function is a self-contained unit of work. Any variables you create inside it (including its parameters) only exist while the function is running. Once it finishes, they're gone. This is called local scope.
Local scope is what makes functions safe to reuse: you can call calculate_bmi from twenty different places in your code and none of them will interfere with each other.
The tradeoff is that you can't access those variables from outside the function. Try to read bmi after the function returns, and Python will tell you it doesn't exist.
Try this snippet in the Python Scratchpad on the right.
def calculate_bmi(weight_kg, height_m):
bmi = weight_kg / (height_m ** 2)
return bmi
print(calculate_bmi(72, 1.75))
print(bmi)
The print(calculate_bmi(...)) line works because the function returned a value. The print(bmi) line fails because bmi only ever existed inside the function. The way to get a value out of a function is return, not "look at its variables from outside."
Variables defined at the top level of a script — outside any function — live in what is called global scope.

8 Putting it together
The five tools in this part work together in almost every function you write. A typical helper function has named parameters with sensible defaults, perhaps a *args or **kwargs catch-all, and may use a lambda inline for a quick sort or filter. Variables inside the function vanish when it returns, leaving only what you return behind.
Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.
Problem: Write a function summarise(patients, sort_by="age") that takes a list of patient dicts and an optional sort key, returns the list sorted by that key, and prints a one-line summary per patient. Use a lambda for the sort key.
def summarise(patients, sort_by="age"):
sorted_patients = sorted(patients, key=lambda p: p[sort_by])
for p in sorted_patients:
print(f"{p['id']}: {p['age']} years, BMI {p['bmi']}")
return sorted_patients
patients = [
{"id": "P001", "age": 54, "bmi": 27.4},
{"id": "P002", "age": 72, "bmi": 29.1},
{"id": "P003", "age": 38, "bmi": 24.8},
]
summarise(patients, sort_by="bmi")
- Define the function with a default sort key of "age". Inside, call sorted() on the patient list with a lambda that pulls out the chosen key from each dict — this returns a new list without mutating the original.
- Loop over the sorted result and print one formatted line per patient using an f-string. Each line shows the id, age, and BMI in a consistent layout.
- Return the sorted list so callers can use it downstream. Then build a small list of patient dicts and call summarise(patients, sort_by="bmi") to see them ordered by BMI ascending.
The same solution with key parts replaced by ???.
Fill in every ??? so the code matches the reference,
then ask the tutor to check it.
Your turn: Write a function rank_genes(genes, by="p_value") that takes a list of gene dicts (each with keys "symbol", "p_value", "effect_size") and an optional sort key, sorts the list ascending using a lambda, prints one line per gene as "{symbol}: p={p_value}, effect={effect_size}", and returns the sorted list. Call it on [{"symbol": "APOE", "p_value": 4.2e-8, "effect_size": 0.42}, {"symbol": "TOMM40", "p_value": 1.1e-6, "effect_size": 0.31}, {"symbol": "PVRL2", "p_value": 8.3e-5, "effect_size": 0.58}] with by="effect_size".
9 Check your understanding
You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.
What does a function return if you forget to write a return statement?
You define def greet(name, greeting="Hello"): return f"{greeting}, {name}". Which call uses keyword arguments correctly?
Given def greet(name, greeting="Hi"): return f"{greeting}, {name}", what does greet("Sam") return?
Which is equivalent to def square(x): return x**2?
What does sorted([5, -3, 1], key=lambda x: abs(x)) return?
You define def f(*args): print(args). What type is args inside the function?
You define def f(**kwargs): print(kwargs). What type is kwargs inside the function?
A variable is created inside a function with an ordinary assignment (no global keyword). After the function returns, what happens to it?
I can write a function with parameters and a default value, and call it using either positional or keyword arguments.
I can write a lambda to use as the key= argument of sorted, max, or min.
I can read a function signature that uses *args or **kwargs and tell what kind of inputs it accepts.
10 Your results
Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.
Submit the post-test to see your results.
What is the one thing from this module that is still unclear to you?