04 - Functions¶
What this session is¶
About an hour. You'll learn how to define your own functions, default arguments, keyword arguments, return values, and a glimpse of type hints - Python's optional way to declare what types your functions expect and return. By the end you can break programs into named pieces.
The problem functions solve¶
So far every program has been a single block of statements. That works for tiny programs. Once you get past 30-40 lines, it stops working - you can't see structure, you can't reuse anything, and you can't test pieces in isolation.
A function is a named, reusable block of code that takes some input and (usually) returns some output.
The shape¶
Concrete:
Type and run.
Walk through:
def double(x):- defines a function calleddoublewith one parameterx. The colon ends the signature; the indented body follows.return x * 2- computex * 2and send it back to whoever called the function.double(5)- call the function. The result is10.
def is short for "define." The function exists after the def runs, ready to be called.
Multiple parameters¶
Parameters are separated by commas.
Default values¶
A parameter can have a default - used when the caller doesn't supply one:
def greet(name, greeting="hello"):
return f"{greeting}, {name}"
print(greet("Alice")) # hello, Alice
print(greet("Alice", "hi")) # hi, Alice
print(greet("Alice", greeting="hey")) # hey, Alice
The third call uses a keyword argument - naming the parameter explicitly. Useful when a function has many parameters; the call site is self-documenting.
Default values must come after any non-default parameters: def f(a, b=2): is fine; def f(a=1, b): is a syntax error.
The trap
Don't use mutable defaults like def f(items=[]):. The list is created once when the function is defined and shared between all calls - modifying it changes the default for all future callers. Universal advice: use def f(items=None): if items is None: items = [] instead. This is the classic Python beginner bug.
Functions that don't return anything¶
If you don't write a return, the function returns None (Python's "nothing" value):
def say_hi(name):
print(f"Hi, {name}")
result = say_hi("Alice") # prints, but returns nothing
print(result) # None
Type hints (optional, increasingly standard)¶
Python lets you annotate the types of parameters and return values. These are hints - Python doesn't enforce them at runtime - but tools (mypy, pyright, IDE inspections) catch type bugs before you run.
def double(x: int) -> int:
return x * 2
def greet(name: str, greeting: str = "hello") -> str:
return f"{greeting}, {name}"
Reading the syntax:
- x: int - parameter x is an int.
- -> int after the parameter list - the function returns an int.
You can use type hints from the start (recommended) or never (also fine - old code rarely has them). Modern Python projects use them. We'll use them lightly in this path; you'll get used to seeing them.
Returning multiple values¶
In some languages (Go) functions can return multiple values. In Python they "can't" - but they can return a tuple (an ordered group), which the caller can unpack:
def divide(a: int, b: int) -> tuple[int, int]:
quotient = a // b
remainder = a % b
return quotient, remainder
q, r = divide(17, 5)
print(f"quotient: {q}, remainder: {r}") # quotient: 3, remainder: 2
The return quotient, remainder actually creates a 2-tuple (quotient, remainder). The q, r = ... on the receiving end unpacks it back into two variables. You'll see this pattern often.
Functions calling functions¶
def square(x: int) -> int:
return x * x
def sum_of_squares(a: int, b: int) -> int:
return square(a) + square(b)
print(sum_of_squares(3, 4)) # 9 + 16 = 25
Functions calling functions is how programs get built up - small named pieces, composed.
Why functions matter¶
- Naming.
double(7)reads better than re-typing7 * 2, especially when the operation is more complex. - Reuse. Write once, call many times.
- Testing. You can test
doubleby itself, separately from the rest of the program (page 10). - Structure. Reading a 500-line script is awful. Reading 20 small named functions tells you what the program does at a glance.
Variables inside vs outside¶
A variable created inside a function exists only inside that function. The technical word is scope.
Each function has its own world. Get information in via parameters; get information out via return values.
(There's a way to share variables across functions called "global" state - generally avoided. Pass values explicitly.)
Exercise¶
In a new file iseven.py:
-
Write a function
is_even(n: int) -> boolthat returnsTrueifnis even,Falseotherwise. Use the%operator. -
Print
is_even(4)andis_even(7). You should seeTrueandFalse. -
Write a function
count_evens(max: int) -> intthat counts even numbers in1, 2, ..., max. Use aforloop and callis_even. -
Print
count_evens(10). Expected:5. -
Print
count_evens(100). Expected:50. -
Now write a function
greet(name, greeting="hello")with a default argument. Call it asgreet("Alice")andgreet("Alice", "hey").
What you might wonder¶
"What's *args and **kwargs?"
A way to accept any number of positional or keyword arguments. def f(*args, **kwargs): - args becomes a tuple of all extra positional args; kwargs becomes a dict of extra named args. You'll see them constantly in framework code. For your own functions, prefer named parameters when you know what you're accepting.
"Are type hints required?" No. Old code rarely has them. New code increasingly does. They help tools catch bugs and make code self-documenting. Use them when you can.
"What if I don't write a return?"
The function returns None. Calling f() and assigning the result gives you None.
"Can a function call itself?" Yes - that's recursion. A useful tool for certain problems (tree traversal, divide-and-conquer). We'll meet a use case later.
"Why is the mutable-default thing a trap?"
Because of when the default value is created. Python evaluates default expressions once, when def runs. The list survives across calls; every call modifying it sees changes from previous calls. It's surprising. Use None as the default and create the list inside.
Done¶
You can now:
- Define your own functions with def.
- Use default and keyword arguments.
- Optionally add type hints.
- Return zero, one, or many values (via tuple unpacking).
- Call functions from other functions.
- Avoid the mutable-default trap.
You now have all the fundamentals: variables, types, control flow, functions. Every Python program is built from these. The next pages extend the toolkit: classes (your own types), collections (lists/dicts/sets), errors, iterators, files, tests, and packages.