Notebook Refactor
Exploratory notebooks grow by appending cells. That's fine for discovery and terrible for review, reuse, and reproduction. This skill restructures a notebook into functions a reviewer can read and a machine can re-run top-to-bottom.
Where this sits (boundary)
- vs. [[data-modeling]] (python-data-patterns): that skill fixes performance and correctness (memory, vectorization, Polars/Spark). This skill fixes structure and reviewability. Refactor structure here; if a function is also slow or buggy, hand the internals to python-data-patterns.
The smells to fix
- One giant cell doing five things
- Hidden order dependency — cell 12 only works if cell 7 ran, with no signal
- Globals mutated across cells; variables reused for different meanings
- Magic numbers and paths hard-coded mid-logic
- Copy-pasted blocks that differ by one value
- Output/plots interleaved so logic can't be read straight through
- No way to test anything without running the whole notebook
Refactor procedure
- Make it re-runnable first. Restart kernel, run all. Fix anything that breaks on a clean top-to-bottom pass. You can't refactor what you can't reproduce.
- Separate the four kinds of cell. Sort content into: config/params (paths, dates, constants) → top; pure logic (transformations) → functions; side effects (I/O, writes); presentation (plots, displays) → bottom.
- Extract subfunctions. Turn each coherent block into a named function with explicit args and a return value — no reliance on globals. One function does one thing; name it after that thing.
- Parameterize. Lift hard-coded values into a config cell (or
papermill-style parameter cell) so the notebook runs for different dates/inputs without edits.
- De-duplicate. Fold copy-pasted variants into one parameterized function.
- Document lightly. One-line docstring per function: what it takes, what it returns. Keep markdown cells for why, not what.
- Add smoke tests. For the core functions, a couple of
asserts on a tiny fixture — enough that a reviewer trusts them without rerunning everything.
- Consider extraction to a module. If functions are reused or production-bound, move them to a
.py beside the notebook and import them — the notebook becomes a thin driver.
Before / after
# BEFORE — one cell, globals, hard-coded, not reusable
df = pd.read_csv("/data/2024-03-15/orders.csv")
df = df[df.status == "complete"]
df["total"] = df.qty * df.price
df.groupby("customer_id")["total"].sum().to_csv("/out/rev.csv")
# AFTER — params + pure functions + thin driver
# --- params cell ---
INPUT_PATH = "/data/2024-03-15/orders.csv"
OUTPUT_PATH = "/out/rev.csv"
# --- functions cell ---
def load_orders(path: str) -> pd.DataFrame:
"""Read raw orders CSV."""
return pd.read_csv(path)
def revenue_by_customer(orders: pd.DataFrame) -> pd.DataFrame:
"""Sum completed-order revenue per customer."""
completed = orders[orders.status == "complete"].copy()
completed["total"] = completed.qty * completed.price
return completed.groupby("customer_id", as_index=False)["total"].sum()
# --- smoke test ---
_fixture = pd.DataFrame({"status":["complete"],"qty":[2],"price":[3.0],"customer_id":[1]})
assert revenue_by_customer(_fixture)["total"].iloc[0] == 6.0
# --- driver cell ---
orders = load_orders(INPUT_PATH)
revenue_by_customer(orders).to_csv(OUTPUT_PATH, index=False)
Done when
Hand-off
Slow or memory-heavy function internals → [[data-modeling]] (python-data-patterns). Lifecycle overview: [[data-lifecycle]].
1---2name: notebook-refactor3description: Refactor a Jupyter/.ipynb notebook from top-to-bottom exploratory sprawl into small, named, reviewable functions with clear inputs and outputs. Extract subfunctions, separate config/params from logic, remove hidden cell-order dependencies and global-state leakage, add docstrings and light tests, and make the notebook re-runnable top-to-bottom and diff-friendly. Use this skill whenever the user has a messy notebook they want cleaned up, wants notebook code turned into functions or a module, is preparing a notebook for review/handoff/production, or asks to make an .ipynb readable or testable. This is about code STRUCTURE and reviewability; performance and correctness of data code is python-data-patterns' job.4---56# Notebook Refactor78Exploratory notebooks grow by appending cells. That's fine for discovery and terrible for review, reuse, and reproduction. This skill restructures a notebook into functions a reviewer can read and a machine can re-run top-to-bottom.910## Where this sits (boundary)1112- **vs. [[data-modeling]] (python-data-patterns):** that skill fixes *performance and correctness* (memory, vectorization, Polars/Spark). This skill fixes *structure and reviewability*. Refactor structure here; if a function is also slow or buggy, hand the internals to python-data-patterns.1314## The smells to fix1516- One giant cell doing five things17- Hidden order dependency — cell 12 only works if cell 7 ran, with no signal18- Globals mutated across cells; variables reused for different meanings19- Magic numbers and paths hard-coded mid-logic20- Copy-pasted blocks that differ by one value21- Output/plots interleaved so logic can't be read straight through22- No way to test anything without running the whole notebook2324## Refactor procedure25261. **Make it re-runnable first.** Restart kernel, run all. Fix anything that breaks on a clean top-to-bottom pass. You can't refactor what you can't reproduce.272. **Separate the four kinds of cell.** Sort content into: **config/params** (paths, dates, constants) → top; **pure logic** (transformations) → functions; **side effects** (I/O, writes); **presentation** (plots, displays) → bottom. 283. **Extract subfunctions.** Turn each coherent block into a named function with explicit args and a return value — no reliance on globals. One function does one thing; name it after that thing.294. **Parameterize.** Lift hard-coded values into a config cell (or `papermill`-style parameter cell) so the notebook runs for different dates/inputs without edits.305. **De-duplicate.** Fold copy-pasted variants into one parameterized function.316. **Document lightly.** One-line docstring per function: what it takes, what it returns. Keep markdown cells for *why*, not *what*.327. **Add smoke tests.** For the core functions, a couple of `assert`s on a tiny fixture — enough that a reviewer trusts them without rerunning everything.338. **Consider extraction to a module.** If functions are reused or production-bound, move them to a `.py` beside the notebook and `import` them — the notebook becomes a thin driver.3435## Before / after3637```python38# BEFORE — one cell, globals, hard-coded, not reusable39df = pd.read_csv("/data/2024-03-15/orders.csv")40df = df[df.status == "complete"]41df["total"] = df.qty * df.price42df.groupby("customer_id")["total"].sum().to_csv("/out/rev.csv")43```4445```python46# AFTER — params + pure functions + thin driver4748# --- params cell ---49INPUT_PATH = "/data/2024-03-15/orders.csv"50OUTPUT_PATH = "/out/rev.csv"5152# --- functions cell ---53def load_orders(path: str) -> pd.DataFrame:54 """Read raw orders CSV."""55 return pd.read_csv(path)5657def revenue_by_customer(orders: pd.DataFrame) -> pd.DataFrame:58 """Sum completed-order revenue per customer."""59 completed = orders[orders.status == "complete"].copy()60 completed["total"] = completed.qty * completed.price61 return completed.groupby("customer_id", as_index=False)["total"].sum()6263# --- smoke test ---64_fixture = pd.DataFrame({"status":["complete"],"qty":[2],"price":[3.0],"customer_id":[1]})65assert revenue_by_customer(_fixture)["total"].iloc[0] == 6.06667# --- driver cell ---68orders = load_orders(INPUT_PATH)69revenue_by_customer(orders).to_csv(OUTPUT_PATH, index=False)70```7172## Done when7374- [ ] Restart-and-run-all passes clean, top to bottom75- [ ] Params/config live in one place, not scattered in logic76- [ ] Each function is named, single-purpose, no global reliance, has a docstring77- [ ] No copy-pasted logic blocks remain78- [ ] Core functions have at least a smoke assert79- [ ] Reusable/production code extracted to a `.py` module (if applicable)80- [ ] Notebook diffs cleanly (cleared bulky outputs before commit)8182## Hand-off8384Slow or memory-heavy function internals → [[data-modeling]] (python-data-patterns). Lifecycle overview: [[data-lifecycle]].