IFI 8410 — Session 4: Functions and Decomposition

Functions as Decomposition

From tasks and handoffs to contracts and return values

The core idea

A function is a boundary around one task

Decomposition gave us tasks with inputs, dependencies, and an owner. In Python, a function is how we draw that boundary explicitly.

Inputs
parameters
→
clean_age_column
one job, a clear name
→
Output
return value

Everything the task needs comes in through parameters.
Everything it produces goes out through return.

The map

Four payoffs → four design habits

Clarity → explicit contracts

Vague goals become concrete actions; dependencies surface.

Descriptive names, explicit parameters, and return values — not global state or print.

Reuse → local scope, pure functions

Proven approaches preserved for future use.

Isolated local variables, new data returned, no unannounced side effects or mutation.

Checking → validation and tests

Review intermediate results before problems spread.

Guard clauses raising TypeError / ValueError; pytest assertions at the boundary.

Collaboration → docstrings, type hints

Clear handoffs and responsibilities.

A public contract others can call without reading the implementation.

Pillar 1 of 4

Clarity ↔ explicit function contracts

A task with visible inputs and outputs↔name · parameters · return value · assumptions

Hidden dependency, output for humans only

TAX_RATE = 0.08
prices = [4.50, 3.25, 5.00]

def total():
    print(sum(prices) * (1 + TAX_RATE))

total()   # can't reuse the number

Inputs explicit, result returned

def order_total(prices: list[float],
                tax_rate: float) -> float:
    return sum(prices) * (1 + tax_rate)

total = order_total([4.50, 3.25, 5.00], 0.08)
print(f"${total:.2f}")   # printing is the caller's choice
  • Parameters replace hidden globals — every dependency is listed in the signature.
  • Return instead of print — the next step (or a test) can use the result.

Pillar 2 of 4

Reuse ↔ local scope and pure functions

A reusable template↔no state leaks in or out of a call
def without_missing(values: list) -> list:
    """Return a NEW list with None values removed. Input is unchanged."""
    return [value for value in values if value is not None]

measurements = [2.1, None, 3.4]
cleaned = without_missing(measurements)

print("returned:", cleaned)          # [2.1, 3.4]
print("original:", measurements)     # [2.1, None, 3.4] -- still auditable
  • Local scope: temporary names disappear when the function returns.
  • Return new objects instead of mutating inputs — the raw data keeps its provenance.
  • No unannounced side effects → safe to reuse in any script or parallel pipeline.

Pillar 3 of 4 — at run time

Checking ↔ validating inputs

A checkpoint before the next task starts↔guard clauses at the function boundary
def mean(values: list[float]) -> float:
    if not isinstance(values, list):
        raise TypeError("values must be a list")
    if len(values) == 0:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

TypeError

Wrong kind of value — a string where a list was expected.

ValueError

Right kind, unusable value — an empty list, an age of −5.

Fail before computing — so a bad input never becomes a plausible-looking wrong answer downstream.

Pillar 3 of 4 — before you ship

Checking ↔ unit tests

Review each result against the standard↔pytest runs the contract as code
def test_validate_age():
    assert validate_age("42") == 42        # normal
    assert validate_age(" 7 ") == 7        # messy but valid
    assert validate_age("0") == 0          # boundary
    assert validate_age("120") == 120      # boundary
    assert validate_age("121") is None     # just outside
    assert validate_age("abc") is None     # not a number
    assert validate_age(None) is None      # missing

Each function is tested in isolation under normal, boundary, and extreme conditions — exactly the "check each piece" idea from decomposition.

Pillar 4 of 4

Collaboration ↔ contracts others can call

A clear handoff between people↔a contract you can call without reading the code
def validate_age(age: object) -> "int | None":
    """Return age as an int, or None when it is
    missing or implausible.

    Args:
        age: A raw age value from the survey,
             typically a string.

    Returns:
        An int between 0 and 120 inclusive,
        or None if the value cannot be used.
    """
    ...
  • Type annotations state what goes in and comes out.
  • The docstring states the promise — including edge cases.
  • A colleague calls it using help(validate_age), not by reading the body.
  • The author can refactor the inside freely, as long as the contract holds.

The whole process

Decomposition steps → pipeline design

Decomposition processPython pipeline
1. Describe the final outcomeDefine the pipeline's return type and output schema.
2. Identify major tasksTop-level functions: load_data, clean_data, summarize, export.
3. Divide into smaller stepsFocused helpers for granular work, e.g. validate_age.
4. Order tasks, find dependenciesChain functions: each return value becomes the next argument.
5. Assign responsibility and checksGuard clauses (TypeError / ValueError) and pytest tests per function.
6. Review and adjustRefactor internals while keeping docstrings and type hints consistent.

Putting it together

Design the stages first

def load_survey_data(path: str) -> list[dict]: ...
def validate_age(age: object) -> "int | None": ...
def clean_age_column(rows: list[dict]) -> list[dict]: ...
def summarize_by_region(rows: list[dict]) -> list[dict]: ...
def export_summary(summary: list[dict], path: str) -> None: ...

rows    = load_survey_data("survey.csv")
cleaned = clean_age_column(rows)
summary = summarize_by_region(cleaned)
export_summary(summary, "summary_by_region.csv")

Five named stages. Each one can now be discussed, tested, and replaced on its own — before a single body is written.

Now: hands-on

Over to the notebooks

Folder 04-Functions-Decomposition — same material at three altitudes. Work them in this order:

1Examples

Practice

25 worked examples, easy to medium, each with a Try it cell: parameters and return, tables, edge cases, functions as values.

2Anatomy

Mechanics

Every part of a function: scope and LEGB, arguments, defaults, *args / **kwargs, annotations, lambda, closures.

3Deep Dive

Design reasoning

Why functions are designed this way: contracts, side effects, validation, the survey pipeline from today's slides.

As you work, ask of every function: is it clear, reusable, checkable, and callable by someone else?

Let's code

Open your first notebook

04-Functions-Decomposition/Examples_of_Python_Functions_orig.ipynb

  1. Pull the latest course repository.
  2. Open the notebook and run the cells from the top.
  3. Stop at each Try it cell — change something and predict the result before you run it.

Reference while you work: Decomposition deck · Decomposition reading

◀ Slides