Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Activity 4.1

This page is an ipython notebook, meaning you can download it and run the executable cells. It directly follows Activity 4.1. Reference answers appear after the questions, challenges, predictions, and reflections. They show one reasonable solution or interpretation, but other correct approaches may exist.

Modular Programming & Functions

Estimated time: 2–3 hours
Tools: Python, pandas, Jupyter/VS Code

Learning objectives

By the end of this tutorial, you should be able to:

  • define reusable Python functions;

  • distinguish parameters from arguments;

  • return values rather than relying on printing;

  • use defaults, keyword arguments, type hints, and docstrings;

  • recognize pure functions and side effects;

  • understand local and global scope;

  • reduce dependence on global variables;

  • compose small functions into workflows;

  • use map(), filter(), and reduce();

  • recognize when comprehensions are more readable;

  • apply functions to pandas data;

  • move reusable functions into modules.


Part 0 — Set up

Create:

notebooks/week07_functions.ipynb

and:

src/week07_utils.py

Import:


Part 1 — Repeated code

Calculate:

Question

What is duplicated here?

What happens if this calculation occurs in 20 places?


Part 2 — Write a function

Run:

What has been abstracted away?


Reference answer

The mean calculation is duplicated. A function abstracts the repeated operation so changes are made in one place rather than many.

Part 3 — Parameters and arguments

For:

identify:

function name:
parameter:
argument in mean_score(scores_a):

Part 4 — Return vs. print

Compare:

with:

Run:

Inspect:

Why is the returned result more reusable?

Reference answer

return gives a reusable value back to the caller; print only displays text. Returned values can be stored, tested, combined, or passed to other functions.


Part 5 — Multiple outputs

Then:

Challenge

Also return the number of observations.

Reference answer

Returning multiple outputs lets the caller reuse each result programmatically instead of parsing printed output.


Part 6 — Default arguments

Try:

What behavior does the default encode?

Reference answer

A default argument specifies the function’s standard behavior when the caller omits that argument, while still allowing an override.


Part 7 — Keyword arguments

Compare:

with:

Which is easier to interpret?

Reference answer

Keyword arguments improve readability because each supplied value is explicitly associated with a parameter name.


Part 8 — Type hints

Inspect:

What information do the annotations provide?

Reference answer

Type hints document expected input/output types and help editors/static checkers catch likely mistakes. They do not usually enforce types at runtime.


Part 9 — Docstrings

Inspect:

Challenge

Expand the docstring to describe:

  • score;

  • threshold;

  • return value.

Reference answer

A useful docstring should describe the function purpose, the score and threshold parameters, the return value, and important assumptions/errors.


Part 10 — Pure function

Run:

Does anything outside the function change?

Reference answer

A pure function depends only on its arguments and avoids hidden side effects. The same inputs therefore produce the same output, which simplifies testing.


Part 11 — Side effects

Run:

What external state changed?


Part 12 — Mutation of inputs

Then:

Why could this surprise a caller?

Reference answer

The function changed external state or mutated an input object. That can surprise a caller because later code sees a changed object even if the caller did not explicitly request mutation.


Part 13 — Non-mutating alternative

Then:

Inspect:

Which is easier to reason about?

Reference answer

A non-mutating alternative creates and returns a new object rather than silently altering the caller’s object, making data flow easier to reason about.


Part 14 — Local scope

Run:

Then try:

Why is the name unavailable outside the function?


Part 15 — Global scope

Run:

Change:

Run again:

Why did the same function call change behavior?

Reference answer

A local variable exists only inside the function call that created it, so its name is unavailable outside that local scope.


Part 16 — Remove hidden global dependence

Rewrite:

Now:

Why is this interface clearer?

Reference answer

The same function call changed because the function depended on hidden global state. Passing the value explicitly as an argument removes that hidden dependency.


Part 17 — LEGB exploration

Run:

Remove the local x from inner() and repeat.

Then remove the enclosing x.

Complete:

L:
E:
G:
B:

Explain the lookup order.

Reference answer

LEGB means Python searches Local, Enclosing, Global, then Built-in scopes and uses the first matching name.


Part 18 — Refactor a large function

Start:

Split it into:


Reference answer

Breaking a large function into smaller functions separates responsibilities so each component can be tested and reused independently.

Part 19 — Compose the functions

Use:

Why might this be easier to test than one large function?


Part 20 — Create a DataFrame

Use your functions from the previous parts.

Reference answer

Small functions with DataFrame inputs/outputs create clear interfaces: each transformation can be tested using a small synthetic table.


Part 21 — Functions are objects

Then:

What does it mean to say functions are first-class objects?


Part 22 — Higher-order function

Run:

Challenge

Predict:

before running it.

Reference answer

Functions are first-class objects because they can be assigned, stored, passed as arguments, and returned. A higher-order function accepts or returns another function.


Part 23 — map()

Inspect:

Why does map() not immediately return a list?


Part 24 — map() vs. comprehension

Compare:

with:

Which is clearer?

Reference answer

map() returns a lazy iterator in Python 3, while a list comprehension constructs the result immediately. For simple transformations, the comprehension is often clearer.


Part 25 — filter()

Now write the equivalent comprehension.


Part 26 — Statistical predicate

Use:

Then:

Compare with a comprehension.

Reference answer

A predicate returns True/False. The equivalent comprehension keeps the values for which that Boolean condition is true.


Part 27 — reduce()

Run:

Write the sequence of reductions manually.

Reference answer

reduce() repeatedly combines an accumulator with the next item until one result remains. Built-ins are usually clearer when they already express the desired reduction.


Part 28 — Reduction and statistics

Calculate:

Why is built-in sum() preferable here?

The goal is to understand reduction rather than replace clearer built-ins.

Reference answer

A manual reduction starts with an accumulator and repeatedly combines it with each subsequent value. This is the general pattern behind reductions.


Part 29 — Lambda expressions

Compare:

with:

Use:

Challenge

Use a lambda with filter() to retain values above 3.

Reference answer

sum() is preferable because it clearly communicates intent and is well tested/optimized. A custom reduction is mainly useful for understanding the pattern.


Part 30 — When not to use lambda

Would this be clear?

Use lambdas for short, local behavior—not to hide complicated logic.

Reference answer

Example: list(filter(lambda x: x > 3, values)). Use a lambda only when the operation is short and obvious; otherwise a named function is clearer.


Part 31 — Apply a function to pandas

Apply:

Inspect df.


Part 32 — Reusable DataFrame function

Test:


Part 33 — Add a defensive check

Test on a DataFrame without group.

Why is an informative error useful?


Part 34 — Build an analysis pipeline

Use:


Reference answer

A clean pipeline is a sequence of small functions with explicit inputs/outputs, for example load → validate → transform → summarize. Avoid hidden globals and accidental mutation.

Part 35 — One-expression composition

Try:

Is this clearer or less clear than named intermediate objects?

Readable code matters more than compact code.


Part 36 — Move functions into a module

Open:

src/week07_utils.py

Move these functions into it:

categorize_score
select_group
remove_missing_scores
select_adults
summarize_scores

Part 37 — Import your module

From the notebook:

Use one function.

How has the notebook changed now that reusable logic lives elsewhere?


Part 38 — Inspect the module boundary

Your project now resembles:

project/
├── notebooks/
│   └── week07_functions.ipynb
├── src/
│   └── week07_utils.py
├── data/
├── figures/
└── tests/

Which code belongs in the notebook?

Which belongs in src/?


Part 39 — Mini refactoring challenge

Start with:

Refactor into a workflow resembling:


Reference answer

A strong refactor removes duplicated logic, extracts conceptually distinct work into named functions, makes dependencies explicit, and gives each function one clear responsibility.

Part 40 — Function design critique

For each function, ask:

Interface

  • Are inputs explicit?

  • Is the return value clear?

Purity

  • Does it unexpectedly mutate data?

  • Does it rely on globals?

Scope

  • Are internal variables local?

Responsibility

  • Does it do one coherent job?

Documentation

  • Does it need a docstring?

  • Would type hints improve clarity?

Revise at least two functions.


Part 41 — Git checkpoint

Run:

git status

Stage:

git add .

Commit:

git commit -m "Complete Week 7 modular programming exercises"

Push:

git push

Part 42 — Final reflection

1. Functions

Why are functions useful in statistical workflows?

2. Return values

Why is returning generally more reusable than printing?

3. Purity

What makes a function pure?

4. Side effects

Give two examples.

5. Scope

What is the difference between local and global scope?

6. Global state

Why can global variables make functions harder to test?

7. Higher-order functions

What does it mean for a function to accept another function?

8. Functional patterns

Explain map, filter, and reduce conceptually.

9. Python style

Why might a comprehension be preferable to map() or filter()?

10. Modularity

Why move functions from notebooks into modules?


Completion checklist

  • Created Week 7 notebook

  • Created a Week 7 Python module

  • Refactored duplicated code into functions

  • Distinguished parameters and arguments

  • Used return values

  • Returned multiple values

  • Used default arguments

  • Used keyword arguments

  • Added type hints

  • Added docstrings

  • Identified a pure function

  • Identified side effects

  • Compared mutating and non-mutating functions

  • Explored local and global scope

  • Demonstrated the LEGB rule

  • Removed hidden global dependencies

  • Split one large function into smaller ones

  • Composed functions

  • Treated a function as an object

  • Wrote a higher-order function

  • Used map()

  • Used filter()

  • Used reduce()

  • Compared functional tools with comprehensions

  • Used lambda expressions

  • Applied a function to a pandas Series

  • Wrote a reusable DataFrame function

  • Added a defensive validation check

  • Built a modular analysis pipeline

  • Imported functions from src/

  • Refactored an analysis into reusable components

  • Committed Week 7 work to Git

  • Pushed work to GitHub


What you should now understand

Repeated code
      ↓
Functions
      ↓
Explicit inputs + outputs
      ↓
Pure transformations
      ↓
Small reusable components
      ↓
Composition
      ↓
Modules
      ↓
Maintainable statistical software

Next week we will use these modular components inside reproducible reports and literate programming workflows.

Reference answer

Functions improve reproducibility by making operations explicit and reusable. Prefer reusable return values, avoid hidden state/mutation, document interfaces, and use higher-order tools only when they improve clarity.