Creating and Using Reusable Code Blocks

A function is a named block of code that performs a single, well-defined task. Functions are Python's primary unit of reuse: they let you describe a behaviour once and invoke it from anywhere. Every serious Python program is organised around small functions that compose into larger ones — writing and naming functions well is one of the highest-leverage habits a new programmer can build.

The def keyword introduces a function. The first line contains the name, parameters and optional return type; the body is indented below and may begin with a docstring describing what the function does. Running def at import time does not execute the body — it creates a function object and binds it to the name. The body runs only when the function is called.

Every function returns exactly one object. If you write return with no value, or omit the return entirely, Python returns None. If you return several values separated by commas, Python bundles them into a tuple that the caller can unpack: minimum, maximum = bounds(data). Functions whose sole purpose is to cause a side effect (print something, write to a file) should typically return None to make that intent clear.

Good functions satisfy three properties. They do one thing — the name should describe that thing in present-tense imperative (load_config, count_words). They are short — ideally short enough that the whole body fits on one screen. And they are pure where possible — given the same inputs, a pure function always returns the same output and has no observable side effect. Pure functions are easy to test, easy to reason about, and easy to reuse.

Python lets you attach type hints to parameters and the return value: def area(width: float, height: float) -> float:. They are documentation first, tool fuel second. Modern editors use them for autocomplete and inline errors; checkers like mypy and pyright validate them across a whole codebase. They add no runtime overhead and require zero extra ceremony, so it costs almost nothing to adopt them from day one.

Parameters, return values and docstrings

Parameters are the inputs; the arguments are the values passed in at the call site. Always describe the parameters in the docstring in the order they appear in the signature, and describe what is returned (and any exceptions that can be raised). Tools such as help(), Sphinx, and most IDEs read docstrings, so a good docstring doubles as generated documentation.

Pure functions, side effects, and composition

Pure functions can be tested in isolation, reordered safely and reused across projects. When a function must cause a side effect (write to disk, send an email, update a database), keep the side effect on the outside and keep the calculation inside a pure helper. The resulting shape — read -> compute -> write — is one of the clearest patterns in software design.

The vocabulary you need for writing and invoking functions well.

ToolPurpose
def
keyword
Introduces a function definition.
return
statement
Returns a single object (or None) to the caller.
docstring
string literal
First statement of the function; becomes __doc__.
type hints
annotation
Signal the expected parameter and return types.
help(func)
built-in
Prints the signature and docstring of a function.
*args
parameter
Collects extra positional arguments into a tuple.
**kwargs
parameter
Collects extra keyword arguments into a dict.
callable()
built-in
True if the object can be invoked with ().

Creating and Using Reusable Code Blocks code example

The example refactors a small analysis into a pair of pure helpers plus a single orchestrating function, and demonstrates calling, composition and introspection.

# Lesson: Creating and Using Reusable Code Blocks
# Goal: split a report into small functions with clear names and docstrings.


def clean(values: list[str]) -> list[float]:
    '''Return the parsed, positive values found in `values`; ignore the rest.'''
    parsed: list[float] = []
    for raw in values:
        try:
            v = float(raw)
        except ValueError:
            continue
        if v >= 0:
            parsed.append(v)
    return parsed


def summary(values: list[float]) -> tuple[float, float, float]:
    '''Return (minimum, maximum, average). Raises if `values` is empty.'''
    if not values:
        raise ValueError("cannot summarise an empty list")
    return min(values), max(values), sum(values) / len(values)


def report(raw: list[str]) -> str:
    '''Top-level orchestrator; pure string in, pure string out.'''
    clean_values = clean(raw)
    lo, hi, avg = summary(clean_values)
    return f"n={len(clean_values)} min={lo:.2f} max={hi:.2f} avg={avg:.2f}"


# --- main script ---------------------------------------------------------
raw_inputs = ["1.5", "2", "-3", "nan", "four", "10.25"]
print(report(raw_inputs))
help(summary)

Every function has one job:

1) clean() parses and filters the input; pure, no I/O.
2) summary() computes stats; raises when there is nothing to report.
3) report() composes the two helpers into a single string.
4) help(summary) prints the signature and docstring at runtime.

Two practice snippets: early return and composition.

# Example A: early return keeps the happy path flat
def fahrenheit_to_celsius(f: float) -> float:
    '''Convert F to C; raise for values below absolute zero.'''
    if f < -459.67:
        raise ValueError("below absolute zero")
    return (f - 32) * 5 / 9

# Example B: compose two small functions into a third
def kelvin_to_celsius(k: float) -> float:
    return k - 273.15

def kelvin_to_fahrenheit(k: float) -> float:
    return kelvin_to_celsius(k) * 9 / 5 + 32

print(kelvin_to_fahrenheit(300.0))  # roughly 80.33

Pure functions are easy to pin down with assertions.

assert clean(["1", "2", "-3", "nope"]) == [1.0, 2.0]
assert summary([1.0, 2.0, 3.0]) == (1.0, 3.0, 2.0)
try:
    summary([])
except ValueError:
    pass
else:
    raise AssertionError("empty list should raise")
assert report(["1", "3", "5"]).startswith("n=3 ")

Running the script prints the one-line report and the rendered docstring:

n=3 min=1.50 max=10.25 avg=4.58
Help on function summary in module __main__:

summary(values: list[float]) -> tuple[float, float, float]
    Return (minimum, maximum, average). Raises if `values` is empty.