How to Take Your ML Model From Notebook to Production
For Data scientists moving from notebooks to production · Based on Egor Howell Production-Grade ML Deploy Framework
// TL;DR
If you're a data scientist who lives in notebooks, this framework shows you how professional ML engineers structure and deploy production systems. You'll refactor exploratory code into single-responsibility files grouped by action, add a Makefile and Poetry-managed dependencies, write pytest suites that outnumber your source lines, and enforce quality with CircleCI on a protected main branch. Then you'll automate retraining with GitHub Actions cron jobs, store predictions in Supabase, and deploy a Streamlit dashboard on a VPS behind Nginx. The guiding principle: do it first, then do it right, then do it better.
Why won't your notebook survive contact with production?
A notebook is exploratory by design — global state, top-to-bottom execution, no tests, hardcoded values scattered throughout. Production ML systems need the opposite: modular code, isolated dependencies, automated quality checks, and a way to run unattended on a schedule. The Production-Grade ML Deploy Framework bridges this gap without asking you to learn heavyweight cloud platforms.
The mental shift is from 'code that runs once for me' to 'code that runs every day for a system.' That means predictions land in a database, a front-end reads them, and CI/CD ensures broken code never reaches production.
How do you restructure notebook code for production?
Group code by action — one primary callable per file. Take the cells that pull data and make `extractor.py`. Cells that clean and engineer features become `processor.py`. Training and inference go in `model.py`. Your decision logic becomes `optimizer.py`. Database writes go in `database.py`, and the dashboard lives in `streamlit_app.py`. Move every constant — date ranges, hyperparameters, thresholds — into `settings.py`.
Crucially, `main.py` becomes orchestration only. It calls each module in sequence and contains no business logic itself. This is exactly how production ML teams organise codebases, and it makes every piece independently testable.
How much testing is enough?
More test lines than source lines. Every function gets at least two or three pytest tests covering the happy path, an edge case, and a failure mode. Don't just assert that a function runs — assert on output columns, types, shapes, and values. If your `tests/` directory is smaller than your `src/` directory, you're under-testing, and reviewers will notice immediately.
Add a Makefile so quality is a single command: `make check` runs formatting, linting (Ruff + Black), type-checking (mypy), and pytest in sequence. Type annotations plus mypy are standard practice — skipping them is a named pitfall.
How do you automate and deploy the pipeline?
Manage your Python version with Pyenv and dependencies with Poetry, documenting everything in `pyproject.toml`. Wire CircleCI to run lint, type-check, and test jobs on every push, and protect main so nothing merges without passing.
For automation, create a GitHub Actions cron workflow (e.g. `0 9 *`) that installs the project and runs `poetry run python src/main.py` daily, injecting Supabase credentials from repository secrets. A second deploy workflow triggers on merge to main, SSHes into your VPS, and runs `scripts/deploy.sh` to git pull and restart the Streamlit systemd service. Provision the VPS with Nginx as a reverse proxy and Certbot for HTTPS.
Why choose Supabase and Streamlit over building a backend?
Because the goal is to demonstrate deployment competence, not to reinvent infrastructure. Supabase gives you a real-time Postgres database as a service — define a table whose columns match your prediction dictionary keys, and your pipeline writes straight to it. Streamlit lets your dashboard API-call Supabase on load and re-render automatically. This keeps you focused on the ML system rather than boilerplate server code. Ship Streamlit first; swap in a JavaScript front-end only if production complexity genuinely demands it.
Next step: Take your existing notebook, split it into the canonical `src/` files grouped by action, and get `main.py` writing predictions to a Supabase table. Then add the Makefile, write your first pytest suite, and run `make check` until it's green.
// FREQUENTLY ASKED QUESTIONS
Do I have to rewrite my whole notebook at once?
No — follow 'do it first, then do it right, then do it better.' Get an end-to-end pipeline working first, even roughly, then refactor into single-responsibility files, add tests, and layer in CI/CD. Iterating on a live baseline beats waiting for a perfect rewrite before anything is deployed.
Is Poetry really necessary if pip already works?
Poetry is the framework's preferred dependency manager because it resolves transitive dependencies into poetry.lock and pins versions cleanly in pyproject.toml. It's used in production at multiple top tech companies. Combined with Pyenv for version isolation, it prevents the cross-repo conflicts that plague pip-only setups and signals professional practice to reviewers.
How do I keep my model retraining automatically?
Use a GitHub Actions cron workflow. Set a schedule like `0 9 * * *`, add workflow_dispatch for manual runs, then checkout code, set up Python 3.12, install Poetry and dependencies, and run `poetry run python src/main.py`. Inject Supabase credentials from GitHub repository secrets so the pipeline never depends on your local machine.