There are two kinds of Python files floating around analytics teams. One is a notebook that saved a hero analysis at 11:47 p.m., full of plots, half-run cells, and a markdown confession that says “final??”. The other is a short .py script nobody loves to open, but it runs the same way every Monday and nobody argues about which version is real. Both are useful. Confusing them is expensive.
This is Part 12 of Python for analytics, the close of the stretch path after plotting. You can load data, clean it, join it, and chart it. The last skill is packaging: when to stay in a notebook, when to graduate to a script, and how to share work so a teammate can re-run it without calling you.
What you’ll learn
Example:

- What notebooks are good at (and bad at)
- What scripts are good at (and bad at)
- A simple graduation path from exploration to repeatable job
- How to structure a small analytics project folder
- Sharing rules that prevent “works on my machine” theater
Two tools, two jobs
A notebook (Jupyter, VS Code notebooks, similar) mixes prose, code, and outputs in one document. You run cells. Order can get weird. Memory holds leftover variables. That is perfect for thinking out loud with data.
A script is a plain .py file that runs top to bottom (or from a clear main). No hidden cell state. Great for jobs you will run again, schedule, or hand to someone who does not want a tour of your rabbit holes.

| Need | Prefer notebook | Prefer script |
|---|---|---|
| First look at a new extract | Yes | Later |
| Teaching a teammate the logic | Yes | Maybe with comments |
| Weekly job every Monday | Risky | Yes |
| Scheduled or automated run | Usually no | Yes |
| Lots of charts while exploring | Yes | Export later |
| Code review in git | Messy diffs | Cleaner |
| One-off stakeholder question | Often fine | Overkill |
Rule of thumb: If you have run it more than twice and someone else depends on the output, start a script.
Why notebooks go wrong (even good ones)
Notebooks do not fail because they are evil. They fail because they hide process.
- Out-of-order cells. You fix cell 12, forget to re-run cell 3, and the chart is lying with old filters.
- Hidden state. A variable renamed in one place still exists under the old name in memory.
- Unclear inputs. The CSV path only works on your laptop Desktop.
- Giant output blobs. Hard to review in git; easy to ship stale plots.
- No single entry point. “Which cell do I run?” is not a runbook.
None of this means “never notebook.” It means “do not confuse a thinking document with a production path.” That is the same lesson as spreadsheets becoming liabilities in From spreadsheets to real data: tools that start personal eventually need multiplayer habits.
Why scripts go wrong (even good ones)
Scripts can be hostile in the other direction:
- No narrative. Six months later you forget why the filter exists.
- Over-engineering early. Classes and configs for a 40-line job.
- Silent failures. No prints, no checks, a zero-row CSV that looks “successful.”
- Hard-coded secrets. Passwords in the file (never do this).
A good analytics script is boring: clear inputs, clear outputs, a few validation prints, and comments where the business rule is non-obvious. You already practiced that shape in Part 8’s cleaning pipeline.
A graduation path that works at work
Six steps from explore to handoff:

Use stages. Do not jump from first curiosity to a platform project.
- Explore in a notebook. Load, profile, plot, take notes.
- Stabilize the logic. Collapse dead ends. Keep only the cells that matter.
- Restart and run all. If it fails, the notebook is not ready to share as truth.
- Extract functions for load, clean, summarize, export.
- Move functions into a
.pymodule or a single script withif __name__ == "__main__":. - Keep a thin notebook only if you still need a teaching demo that imports the module.
That last step is underrated. You can still teach with a notebook while the real job lives in a script. Teaching and running are different products.
Minimal project shape
What the folder looks like in practice:

You do not need a monorepo. You need a folder a teammate can open without spelunking.
weekly_sales/
README.md
requirements.txt
data/
raw/ # inputs you do not edit by hand
clean/ # outputs of the pipeline
notebooks/
explore_week.ipynb
src/
run_weekly_sales.py
outputs/
charts/README.md should answer four questions in under a screen:
- What decision or report is this for?
- What file do I run?
- What inputs does it expect?
- What outputs should I see?
That is a lightweight version of the table contract idea from the spreadsheet series: document grain, owner, and outputs so the next person is not reverse engineering your career.
Script pattern you can copy
Example:

Example console output when the job runs cleanly:

Here is a small script shape that matches Parts 8 through 11: load, clean, summarize, chart optional, export, validate.
"""Weekly sales summary.
Run:
python src/run_weekly_sales.py
Inputs:
data/raw/orders.csv
Outputs:
data/clean/sales_by_region.csv
outputs/charts/sales_by_region.png
"""
from pathlib import Path
import pandas as pd
import matplotlib.pyplot as plt
ROOT = Path(__file__).resolve().parents[1]
RAW = ROOT / "data" / "raw" / "orders.csv"
OUT_CSV = ROOT / "data" / "clean" / "sales_by_region.csv"
OUT_CHART = ROOT / "outputs" / "charts" / "sales_by_region.png"
def load_orders(path: Path) -> pd.DataFrame:
df = pd.read_csv(path)
expected = {"region", "amount", "order_date"}
missing = expected - set(df.columns)
if missing:
raise ValueError(f"Missing columns: {sorted(missing)}")
return df
def clean(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out["amount"] = pd.to_numeric(out["amount"], errors="coerce")
out["order_date"] = pd.to_datetime(out["order_date"], errors="coerce")
out["region"] = out["region"].astype(str).str.strip()
before = len(out)
out = out.dropna(subset=["amount", "order_date", "region"])
print(f"clean: kept {len(out)} of {before} rows")
return out
def summarize(df: pd.DataFrame) -> pd.DataFrame:
return (
df.groupby("region", as_index=False)["amount"]
.sum()
.sort_values("amount", ascending=False)
)
def save_chart(summary: pd.DataFrame, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
ax = summary.plot(
kind="bar",
x="region",
y="amount",
legend=False,
color="#c2410c",
figsize=(8, 4.5),
)
ax.set_title("Sales by region (weekly run)")
ax.set_xlabel("Region")
ax.set_ylabel("Sales amount (USD)")
ax.tick_params(axis="x", rotation=0)
plt.tight_layout()
plt.savefig(path, dpi=160)
plt.close()
print(f"wrote chart {path}")
def main() -> None:
OUT_CSV.parent.mkdir(parents=True, exist_ok=True)
orders = clean(load_orders(RAW))
summary = summarize(orders)
if summary.empty:
raise SystemExit("No rows after clean; refusing to write empty success")
summary.to_csv(OUT_CSV, index=False)
print(f"wrote {OUT_CSV} ({len(summary)} regions)")
save_chart(summary, OUT_CHART)
print(summary)
if __name__ == "__main__":
main()Features worth copying even if your domain differs:
- Docstring with run command and I/O
- Paths relative to the project root, not your home folder
- Column check on load
- Prints for row survival
- Hard fail on empty result masquerading as success
plt.close()so batch runs do not leak figure memory
Notebook etiquette when you still need one
If the deliverable is a teaching notebook or a research log, raise the bar:
- Top cell: purpose, inputs, outputs, owner name or team
- Use a virtual environment pinned in
requirements.txt - Prefer Restart kernel and run all before sharing
- Clear giant unused outputs if the file must live in git
- Import shared logic from
src/instead of pasting the same cleaners in five notebooks
# notebooks/explore_week.ipynb (conceptually)
# Cell 1
# Purpose: explore anomalies before the Monday scripted run
# Input: data/raw/orders.csv
# Owner: analytics team
import sys
from pathlib import Path
ROOT = Path("..").resolve()
sys.path.append(str(ROOT / "src"))
# If you later move clean() into a module, import it:
# from run_weekly_sales import load_orders, cleanSharing with teammates without drama
| Share method | Works when | Watch out |
|---|---|---|
| Repo + README + requirements | Team uses git | Secrets, huge data files |
| Script + sample CSV | Small handoff | Sample not matching prod grain |
| Exported HTML notebook | Read-only story | Not re-runnable easily |
| Scheduled job + output folder | Recurring report | Who owns failures? |
| Screenshot only | Never, almost | No grain, no refresh path |
Pair the share with the handoff habits from Part 9: what file, what grain, what timestamp, what known caveats. Python does not remove the need for that note. It just makes the path repeatable when you write it down.
Where SQL still fits
From Part 10, heavy filters and governed aggregates often stay in SQL. Your script can still be the conductor:
- SQL (or warehouse job) produces a clean extract
- Python script loads extract, light polish, chart, export
- Notebook only for investigations when the extract looks weird
That division keeps expensive compute where it belongs and keeps your Python layer small enough to reason about.
Common mistakes
- Shipping a notebook as the production job because “it already works.”
- Never restarting the kernel before you trust outputs.
- Absolute paths to
/Users/you/Downloads. - No requirements file, so teammates install mystery package versions.
- Copy-paste cleaners across six notebooks until one drifts.
- Empty success: writing outputs even when filters wipe all rows.
- Treating scripts as unreadable. Comments for business rules are kindness.
How to practice Monday morning
- Pick one analysis you repeated this month in a notebook.
- Restart and run all. Fix breaks.
- Extract load/clean/summarize into functions.
- Move them into a
.pyscript with a project-relative path. - Write a five-line README: purpose, run command, inputs, outputs.
- Have a teammate run it once without you in the room. Note every stuck point.
Where the Python series leaves you
Across Parts 1 through 12 you built a full beginner-to-work path:
- When Python is worth it (and when Sheets or SQL win)
- Calm setup and DataFrames as tables
- Select, filter, sort, group, join
- Missing data, cleaning pipelines, exports
- Python with SQL, then plots, then packaging
You do not need to become a software engineer to be dangerous (in the good way) with data. You need habits: clear grain, checked joins, honest charts, and a run path someone else can follow. For more paths on the site, start at the Python series landing and the Learn hub. Next calendar arc in the master plan is data quality if you want the “why do numbers fight” sequel.
Quick recap
- Notebooks explore and teach; scripts repeat and hand off.
- Graduate with restart-and-run-all, then extract functions.
- Small project folders beat mystery Desktops.
- Document purpose, run command, inputs, outputs.
- Fail loudly on empty or invalid results.
Sources
Research and further reading used for this article:
- Jupyter Project documentation (notebooks as computational documents)
- Python modules tutorial (scripts, imports,
__main__) - pip requirements files (pinning dependencies for teammates)
- matplotlib: save and close figures (batch-friendly plotting)
- Analytics Made Simple: Python for analytics
- Analytics Made Simple: Learn
