# Functions

> Package code into reusable functions with parameters and return values, understand scope, and break problems into small named steps.

Learn to Code roadmap · The building blocks · 16 min read · https://levelcode.org/roadmaps/learn-to-code/functions

As programs grow, the same few lines start appearing in several places, and a 200-line file becomes hard to follow. **Functions** fix both problems. A function is a named, reusable piece of code: you write it once, give it a clear name, and call it wherever you need it. Good functions are also the main way programmers break a big problem into small ones they can think about one at a time. This lesson covers how to define and call functions, parameters and return values, scope, and how to decide what deserves a function.

## Defining and calling a function

You've been calling functions since your first program: `print`, `input`, `len`, `int`. Now you'll write your own with `def`:

```python
def greet():
    print("Hello!")
    print("Welcome to the program.")

greet()
greet()
```

```text
Hello!
Welcome to the program.
Hello!
Welcome to the program.
```

`def` defines the function: its name, parentheses, a colon, and an indented body. Defining a function doesn't run it. The body runs each time you **call** it, by writing its name followed by `()`. A function must be defined before the line that calls it runs, which is why definitions usually sit at the top of a file.

## Parameters: giving a function input

**Parameters** are variables listed in the parentheses. When you call the function, you pass **arguments**, which become the parameters' values for that call:

```python
def greet(name, times):
    for _ in range(times):
        print(f"Hello, {name}!")

greet("Ana", 2)
greet("Ben", 1)
```

Arguments are matched to parameters by position: `"Ana"` goes to `name`, `2` to `times`. (The `_` is a conventional name for a loop variable you don't use.)

You can also pass arguments **by name**, which makes calls easier to read, and give parameters **default values**, which makes them optional:

```python
def greet(name, times=1, punctuation="!"):
    for _ in range(times):
        print(f"Hello, {name}{punctuation}")

greet("Ana")                         # uses both defaults
greet("Ben", times=3)
greet("Cy", punctuation="?")
```

## return: getting a result back

Most useful functions calculate something and **return** it to the caller:

```python
def area_of_rectangle(width, height):
    return width * height

room = area_of_rectangle(4, 5)
hall = area_of_rectangle(1.5, 8)
print(f"Total floor area: {room + hall} m²")    # Total floor area: 32.0 m²
```

When Python reaches `return`, the function stops immediately and the value becomes the result of the call expression. That's why you can store it, add it, or pass it straight into another function: `print(area_of_rectangle(2, 3))`.

### Returning vs printing

This is the most common confusion for new programmers. Compare:

```python
def add_print(a, b):
    print(a + b)

def add_return(a, b):
    return a + b

x = add_print(2, 3)     # prints 5
y = add_return(2, 3)    # prints nothing
print(x)                # None
print(y)                # 5
```

`print` shows a value to the human at the screen; the program can't use it afterwards. `return` hands the value back to the code that called the function. A function without a `return` returns the special value `None`, meaning "nothing".

Prefer functions that **return** values and let the caller decide what to do with them, such as print them, save them or use them in a calculation. That makes functions reusable and testable.

A function can return early, which often makes logic clearer:

```python
def describe_temperature(celsius):
    if celsius < 0:
        return "freezing"
    if celsius < 15:
        return "cold"
    if celsius < 25:
        return "mild"
    return "hot"
```

## Scope: variables inside functions

Variables created inside a function are **local**: they exist only while the function runs and can't be seen from outside.

```python
def calculate_total(prices):
    total = 0
    for p in prices:
        total += p
    return total

print(calculate_total([3, 4, 5]))   # 12
print(total)                        # NameError: name 'total' is not defined
```

That's a feature, not a limitation. Each function is a sealed box: you can understand it by reading only its body and parameters, and two functions can both use a variable called `total` without interfering.

A function *can* read variables defined at the top level of the file (called **global** variables), but relying on that makes functions harder to understand and test. Pass what a function needs as parameters, and return what it produces.

> [!TIP]
> A good function is like a vending machine: inputs go in through the parameters, a result comes out through `return`, and it doesn't depend on, or secretly change, anything else. Functions like that are easy to test, reuse and trust.

## Docstrings: saying what a function does

The first line of a function's body can be a string that describes it, called a **docstring**:

```python
def bmi(weight_kg, height_m):
    """Return the body mass index for a weight in kilograms and a height in metres."""
    return weight_kg / height_m ** 2
```

Editors show docstrings when you hover over a call, and `help(bmi)` in the REPL prints it. A one-line docstring saying what the function returns, and in what units, prevents a lot of bugs.

## Breaking problems into functions

Functions are a thinking tool, not just a way to avoid repetition. Faced with a big problem, describe it as a few steps, then write one function per step. Here's a program that reports on a list of exam scores:

```python
def average(numbers):
    """Return the mean of a non-empty list of numbers."""
    return sum(numbers) / len(numbers)


def letter_grade(score):
    """Return A-F for a score from 0 to 100."""
    if score >= 90:
        return "A"
    if score >= 80:
        return "B"
    if score >= 70:
        return "C"
    if score >= 60:
        return "D"
    return "F"


def report(scores):
    """Print a summary of a list of scores."""
    avg = average(scores)
    print(f"Students: {len(scores)}")
    print(f"Average:  {avg:.1f} ({letter_grade(avg)})")
    print(f"Highest:  {max(scores)}")
    print(f"Lowest:   {min(scores)}")


report([72, 95, 88, 64, 91])
```

```text
Students: 5
Average:  82.0 (B)
Highest:  95
Lowest:   64
```

Each function does one thing and has a name that says what. `report` reads almost like the description of the problem. If the grading rules change, you know exactly which function to edit. (`sum`, `max` and `min` are built-in functions; lists, written with square brackets, are the subject of the next lesson.)

Signs that a piece of code wants to be a function:

- You've copied the same lines to a second place.
- You can describe what a block does in a short phrase ("work out the shipping cost"). That phrase is its name.
- A function has grown past what fits on one screen.

## Practice

Write `dates.py` with three functions, each building on the previous one:

1. `is_leap_year(year)` returns `True` or `False`. A year is a leap year if it's divisible by 4, except years divisible by 100, which aren't, except years divisible by 400, which are. (2024 and 2000 are leap years; 1900 isn't.)
2. `days_in_month(year, month)` returns the number of days in that month (month is 1 to 12).
3. `day_of_year(year, month, day)` returns which day of the year a date is: 1 January is 1, 31 December is 365 or 366.

At the bottom, check your functions by printing a few results whose answers you know.

**Solution**

```python
# dates.py

def is_leap_year(year):
    """Return True if year is a leap year in the Gregorian calendar."""
    if year % 400 == 0:
        return True
    if year % 100 == 0:
        return False
    return year % 4 == 0


def days_in_month(year, month):
    """Return the number of days in a month (1-12) of a given year."""
    if month == 2:
        return 29 if is_leap_year(year) else 28
    if month in (4, 6, 9, 11):
        return 30
    return 31


def day_of_year(year, month, day):
    """Return the day number within the year, from 1 to 365 or 366."""
    total = 0
    for m in range(1, month):
        total += days_in_month(year, m)
    return total + day


print(is_leap_year(2024), is_leap_year(1900), is_leap_year(2000))  # True False True
print(days_in_month(2023, 2), days_in_month(2024, 2), days_in_month(2024, 4))  # 28 29 30
print(day_of_year(2024, 1, 1))     # 1
print(day_of_year(2024, 3, 1))     # 61  (31 + 29 + 1)
print(day_of_year(2023, 12, 31))   # 365
print(day_of_year(2024, 12, 31))   # 366
```

Checking 400 first, then 100, then 4 turns the rule's "except... except..." into three early returns, from most specific to least. `29 if is_leap_year(year) else 28` is a **conditional expression**: a compact `if`/`else` that produces a value. `day_of_year` reuses `days_in_month`, which reuses `is_leap_year`: small functions combining into bigger ones is exactly how larger programs are built.

## Key takeaways

- `def name(parameters):` defines a function; calling it with `name(arguments)` runs its body.
- Arguments can be positional or named, and parameters can have default values.
- `return` sends a value back and ends the function; a function without `return` gives `None`. Prefer returning over printing.
- Variables inside a function are local; pass inputs as parameters instead of relying on globals.
- Break problems into small functions with clear names and docstrings; that's how big programs stay understandable.

## Quiz

1. def double(n): print(n * 2). What is the value of result after result = double(4)?

   A. 8
   B. None
   C. "8"
   D. An error

   **Answer: B.** The function prints 8 but doesn't return anything, so the call evaluates to None. Use return n * 2 to get the value back.

2. What happens when a function reaches a return statement in the middle of a loop?

   A. The function stops immediately and returns the value
   B. The loop finishes first, then the function returns
   C. Only the current loop iteration ends
   D. Python raises an error

   **Answer: A.** return ends the whole function at once. That's what makes early returns useful.

3. def send(to, subject="Hi", urgent=False): ... Which call sets urgent to True and keeps the default subject?

   A. send("ana@x.com", True)
   B. send(urgent=True)
   C. send("ana@x.com", urgent=True)
   D. send(True, "ana@x.com")

   **Answer: C.** Passing urgent by name skips subject, which keeps its default. send("ana@x.com", True) would set subject to True.

4. Why is a function that takes inputs as parameters and returns a result easier to work with than one that reads and changes global variables?

   A. It runs faster
   B. Python doesn't allow functions to read globals
   C. Global variables can't hold numbers
   D. You can understand, test and reuse it by looking only at its parameters, body and return value

   **Answer: D.** A self-contained function depends on nothing hidden, so its behaviour is predictable wherever it's called, and testing it is just calling it with inputs.
