Functions and Modules
Shape your code so it ships, not just runs
Well-shaped functions, type hints, dataclasses and module boundaries are what make a pipeline readable, testable and importable — the difference between a script and a system.
What you will learn
- Design small, testable functions with clear boundaries
- Type-hint signatures and model records with dataclasses
- Structure a pipeline as importable modules instead of one script
What is this?
Functions convert a script into reusable logic; modules give it import boundaries; dataclasses give records explicit shape. In production data code these three turn a 'run once' script into something a team can test, review and operate for years.
Why do you need it?
- Small functions each cover one behavior, which is exactly what pytest checks.
- Type hints and dataclasses make contracts visible — a schema for your code, not just your data.
- Module boundaries let you import one transform in a notebook or test instead of re-running an entire script.
Mental model
A workshop's labeled tool drawers
Modules and functions
You don't keep every tool in one pile; each drawer holds a labelled set. Modules group related functions so the right drawer can be pulled out and the right tool run in isolation.
How it works
- A function isolates a unit of behavior with explicit inputs/outputs; purity (no hidden global state) makes it testable.
- Type hints (`def transform(df: pd.DataFrame) -> pd.DataFrame`) document and (via mypy) check contracts.
- `@dataclass` defines record types with fields and equality, replacing ad-hoc dicts.
- Modules are `.py` files; packages are directories with `__init__.py`, imported by dotted path.
- A pipeline package exposes entry points (a `main()`) that a DAG or CLI calls.
From script to importable package.
Examples
A typed, testable extraction function
from dataclasses import dataclassfrom typing import Iterator @dataclass(frozen=True)class Event: event_id: str amount: float ts: str def parse_events(lines: Iterator[str]) -> Iterator[Event]: for line in lines: eid, amount, ts = line.strip().split(",") yield Event(eid, float(amount), ts)The parser is a pure function over an iterator — trivially unit tested without files or I/O.
In production
- An extraction module exposes `extract_orders(source) -> DataFrame` and is imported by both the DAG and a notebook.
- Dataclasses model config and record shapes, so mypy catches a mis-typed field at CI time instead of 3am.
- A transforms package releases semantic versions; dags pin the version they were written against.
Best practices
- Keep functions pure (inputs in, return out) so tests don't need global state or mocks of the real world.
- Return small dataclasses or DataFrames rather than throwing ragged dicts around.
- Name modules by responsibility (extract/transform/load/quality), not by tool.
- Add a `main()` guarded by `if __name__ == "__main__":` so the module is both importable and runnable.
Common mistakes
- A 400-line function that 'does the pipeline' — impossible to unit test and to blame.
- Global mutable state (a shared list of processed ids) that makes runs non-reproducible.
- Modules that import each other in a cycle, breaking imports at 3am.
- Type hints that lie (`Any` everywhere) — worse than no hints because tooling trusts them.
Common issues
ImportError: cannot import nameCircular imports or a renamed symbol not picked up
Break the cycle by importing inside functions or reordering module-level imports.
Two team members get different bundles of the same data from one pipelineShared mutable state accumulated across runs in the same process
Make the pipeline pure per run: fresh containers per invocation, no module-level caches.
Debugging tips
- When a pipeline misbehaves, run the single suspected function in isolation with real inputs.
- Enable mypy in CI and treat it as a gate — moving contracts earlier catches field bugs.
- Put the smallest repro in a pytest file; if it passes, the bug is in composition, not the unit.
Interview checkpoint
Quick quiz
Which property makes a function easy to unit test?
Type hints are enforced at runtime by default in Python.