IFI 8410 — Session 4: Functions and Decomposition
From tasks and handoffs to contracts and return values
The core idea
Decomposition gave us tasks with inputs, dependencies, and an owner. In Python, a function is how we draw that boundary explicitly.
Everything the task needs comes in through parameters.
Everything it produces goes out through return.
The map
Vague goals become concrete actions; dependencies surface.
Descriptive names, explicit parameters, and return values — not global state or print.
Proven approaches preserved for future use.
Isolated local variables, new data returned, no unannounced side effects or mutation.
Review intermediate results before problems spread.
Guard clauses raising TypeError / ValueError; pytest assertions at the boundary.
Clear handoffs and responsibilities.
A public contract others can call without reading the implementation.
Pillar 1 of 4
TAX_RATE = 0.08
prices = [4.50, 3.25, 5.00]
def total():
print(sum(prices) * (1 + TAX_RATE))
total() # can't reuse the numberdef 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 choiceprint — the next step (or a test) can use the result.Pillar 2 of 4
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 auditablePillar 3 of 4 — at run time
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)Wrong kind of value — a string where a list was expected.
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
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 # missingEach function is tested in isolation under normal, boundary, and extreme conditions — exactly the "check each piece" idea from decomposition.
Pillar 4 of 4
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.
"""
...help(validate_age), not by reading the body.The whole process
| Decomposition process | Python pipeline |
|---|---|
| 1. Describe the final outcome | Define the pipeline's return type and output schema. |
| 2. Identify major tasks | Top-level functions: load_data, clean_data, summarize, export. |
| 3. Divide into smaller steps | Focused helpers for granular work, e.g. validate_age. |
| 4. Order tasks, find dependencies | Chain functions: each return value becomes the next argument. |
| 5. Assign responsibility and checks | Guard clauses (TypeError / ValueError) and pytest tests per function. |
| 6. Review and adjust | Refactor internals while keeping docstrings and type hints consistent. |
Putting it together
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
Folder 04-Functions-Decomposition — same material at three altitudes. Work them in this order:
Practice
25 worked examples, easy to medium, each with a Try it cell: parameters and return, tables, edge cases, functions as values.
Mechanics
Every part of a function: scope and LEGB, arguments, defaults, *args / **kwargs, annotations, lambda, closures.
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
04-Functions-Decomposition/Examples_of_Python_Functions_orig.ipynb
Reference while you work: Decomposition deck · Decomposition reading

