Functions and Modules
Structure that scales with the dataset
Functions package logic into testable units; modules and packages organize them into libraries. Returning early, keyword args and docstrings separate clean data code from notebook spaghetti.
What you will learn
- Write functions with clear inputs, outputs and defaults
- Explain why functions without side effects are safer
- Import and organize code into modules
What is this?
Functions are named, reusable blocks of logic: `def clean(df): ... return df`. Modules are `.py` files holding related functions; packages are folders of modules. Data science code that stays in notebooks turns into untestable scripts as it grows; extracting pure, documented functions into modules is what makes analyses maintainable and reusable.
Why do you need it?
- Notebook linear flow becomes unmaintainable past a few hundred cells.
- Pure functions (same input → same output, no global mutation) are testable and cacheable.
- Well-named modules let teams share transformations instead of re-implementing them.
- Functions are where unit tests, docstrings and type hints live.
Mental model
A chef's mise en place
Prepared functions before the rush
Gather the ingredients and compose recipes before service. Clean, small, tested functions prepared in advance make the final pipeline (the meal service) fast and repeatable.
How it works
- def name(params, default=...): body; return ...
- Positional vs keyword args: callers can pass f(x, y=2) to be explicit.
- A function with no side effects only maps inputs to outputs — safe to call anywhere, any time.
- import module runs the module once and exposes names; `from x import y` imports specific names.
- if __name__ == '__main__': guards module-level code so imports don't execute it.
From notebook to modular code.
Examples
A pure, documented transformer
def flag_outliers(s, k=3): """Return a boolean Series marking values > k MAD from the median.""" med = s.median() mad = (s - med).abs().median() return (s - med).abs() > k * (1.4826 * mad) import pandas as pdcol = pd.Series([1, 2, 3, 2, 100])print(flag_outliers(col).tolist()) # [False, False, False, False, True]Pure function: same input → same output, no globals mutated, documented parameters. The pandas median/MAD logic inside is robust to the outlier that would corrupt a mean/std approach.
In production
- A team shares one features.py with curated transformers, imported by all notebooks and the serving API.
- CI runs pytest against each pipeline function before data refresh deploys.
- A modeling group wraps training in train(params) so experiments differ only in parameters.
Best practices
- Make functions pure where possible; isolate and document mutations.
- Give defaults for tunable constants (k=3) and document what they mean.
- Write one docstring line on purpose and parameters.
- Keep functions small enough to read top-to-bottom.
Common mistakes
- Mutating an input DataFrame in place without a copy, surprising the caller.
- Module-level import-time heavy work (loading data at import).
- Massive 200-line functions with hidden global state.
Debugging tips
- Test each function in isolation with a tiny fixture before composing the pipeline.
- Name functions by action (clean_, compute_, flag_) for greppable codebases.
- If two callers get different results, suspect shared mutable state inside the function.
Interview checkpoint
Quick quiz
A function that only maps its inputs to outputs, with no external effects, is called:
Functional mutations of the input argue for a local .copy() inside the function.