Skip to main content

Module 3 — Functions, modules and code organization

Analysis code always starts as a linear script — and that is fine. But as soon as a treatment repeats or a project outlives the afternoon, functions and modules become the difference between reusable work and an 800-line file even its author dreads opening.

The anatomy of a clean function

def missing_rate(column, alert_threshold=0.2):
"""Compute the share of missing values in a column.

Returns a (rate, alert) tuple where alert is True if the
rate exceeds alert_threshold.
"""
rate = column.isna().mean()
return rate, rate > alert_threshold

Four design choices in these eight lines, all generalizable.

A name that says what it does — a precise verb or noun, not process_data. If the name requires an "and" (clean_and_save), the function does two things: split it.

A default value for the secondary parameter: the common call stays simple, the special case stays possible.

A docstring on the first line: one sentence on the role, one on the return value. It is what help() displays, and it is the documentation that never drifts from the code because it lives inside it.

An explicit return. A function that prints instead of returning is unusable downstream: you can neither test its result nor pass it to the next pipeline stage.

Positional and keyword arguments

Python lets you call by position, by name, or mixed:

read_csv("sales.csv", ";", "utf-8", True)                       # unreadable
read_csv("sales.csv", sep=";", encoding="utf-8", header=True) # clear

The practical rule: beyond two arguments, name them at the call site. Data libraries practically enforce it: a real call to pd.read_csv easily lines up five keyword arguments, and that is precisely what keeps it readable.

The *args and **kwargs signatures (variable numbers of positional / keyword arguments) are read more than written day to day: they explain why Matplotlib's plotting functions accept dozens of options without declaring them one by one.

Variable scope: local first

Variables created inside a function are local: they are born at the call and die at the return. A function can read a global variable, but modifying one requires the global keyword — and it is almost always a bad idea.

Functions that depend on the outside

A function that reads global variables (df, config…) works in the notebook where it was born and nowhere else. The discipline that changes everything: everything the function needs comes in through its parameters; everything it produces leaves through its return value. This principle makes code testable and movable — and it defuses half of the notebook problems of module 10.

Modules and imports: reuse without copy-paste

Every .py file is an importable module. A typical data project is structured like this:

project/
├── cleaning.py # preparation functions
├── visualization.py # plotting functions
├── analysis.ipynb # the notebook that uses them
└── requirements.txt # the dependencies (module 9)
# inside analysis.ipynb
from cleaning import missing_rate, normalize_columns
import visualization as viz

The ecosystem's import conventions are almost ritual — following them makes your code instantly familiar to any reader:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

Two practices to avoid: from module import * (nobody knows where names come from anymore) and imports in the middle of a file (all at the top: standard library first, third-party next, local last).

The if __name__ == "__main__" block

This guard, present in every serious script, separates two uses of the same file:

# cleaning.py
def normalize_columns(df):
...

if __name__ == "__main__":
# runs only via: python cleaning.py
# NOT when imported from the notebook
df = pd.read_csv("raw.csv")
print(normalize_columns(df).head())

Without it, importing the module would execute the test code — file loading included. With it, the file is both an importable library and a runnable script.

Handling errors without hiding them

try:
df = pd.read_csv(path)
except FileNotFoundError:
print(f"Missing file: {path} — check the data mount")
raise

The two rules that avoid the classic traps: catch the precise exception (never a bare except:, which also swallows real errors and keyboard interrupts), and never silence an error — handle the case, or re-raise with raise. A pipeline that keeps going on half-loaded data produces wrong results while looking perfectly healthy.

Key takeaways

  • One function: one role, inputs through parameters, output through return, a one-or-two-sentence docstring.
  • Keyword arguments as soon as a call exceeds two parameters; default values for options.
  • A project = .py modules imported by the notebook; np, pd, plt, sns conventions; imports at the top.
  • if __name__ == "__main__" separates library use from script use.
  • Catch precise exceptions; never swallow an error silently.

Next module, the heart of scientific computing: NumPy, its arrays, and the vectorization that replaces loops.