Project — Python for ML Engineers
Build a clean, importable and runnable Python package for the Adult / Census Income dataset (predict whether a person earns >50K), in your own GitHub repo. It is the skeleton the rest of Phase 1 builds on — you carry this same repo through the M2–M4 projects — so the bar is maintainability, not cleverness.
| Dataset | Adult / Census Income — different from the lessons’ Lending Club scorer, so finishing it proves the technique transferred, not that you replayed the lesson |
| Start from | Fork dutchengineer-org/phase1-starter — an empty package shell you fill in (do not start from a blank directory); the data comes from ml_pipeline.datasets.load_adult, not a bundled file |
| Aim for | The reference package ml-pipeline-starter — read it to see what good looks like; do not fork it (it is built on Lending Club, yours is the same structure on Census) |
| Done when | It passes every line of the rubric below, then you push to GitHub |
Start here
- Fork the starter →
dutchengineer-org/phase1-starter, clone it,pip install -e . - Work the 5 tasks below in order — each maps to a lesson you just finished
- Check yourself against the rubric, then push and submit
The finished package should let another engineer clone your repo, install it once, and both
import your code (to reuse a function) and run it (python -m census_pipeline, to
score a record) — without anything surprising happening.
The tasks
1. Stand up the package (from Lessons 1–2)
- The forked starter already ships the standard layout as empty files:
pyproject.toml,src/census_pipeline/with__init__.py, the modules (data.py,features.py,model.py), and__main__.py. Fill them in; do not start a second layout beside it. data.pyis your data-loading seam: get the Adult / Census Income frame from the source withfrom ml_pipeline.datasets import load_adult(the “go get the data” step — a stand-in for the database query a real system would run), and hand it back. You build the module; the package provides the data.__main__.pyruns a baseline score end to end.pip install -e .works, andpython -m census_pipelineruns your scorer.
2. Make the imports robust (from Lesson 4)
- No module does real work at import time: importing any module must not load data,
train, or write files. The “do the run” code lives behind
if __name__ == "__main__":. - The package imports cleanly from any directory after the editable install (no
sys.pathhacks).
3. Make the data contracts solid (from Lesson 5)
- Add typed data contracts at the boundary: a
dataclassor pydantic model for a record, with correct type hints (dict[str, float],str | None, etc.). - The serving-style entry point validates input through that contract: a malformed field is rejected with a clear error, not passed silently into the model.
4. Make it reproducible (from Lesson 6)
- Pin dependencies with a lock file (
uvlock) so a fresh clone resolves the same environment. - A teammate cloning the repo and following your README gets the same result you do.
5. Configure it through one typed settings object (from Lesson 5, applied to config)
-
Add a
settings.pynext to your modules with aSettingsclass built onpydantic-settings(pip install pydantic-settings), the same pydantic that validates your records in Task 3, now pointed at your runtime config. Two fields are enough: the data path as a required field (no default), and the split seed as an optional one:from pydantic_settings import BaseSettings class Settings(BaseSettings): census_data_path: str # required: missing -> refuses to start, names the field random_state: int = 42 # optional: the default lives here, once -
__main__.pyconstructsSettings()once and passes the values into your functions as arguments. No other module readsos.environ; the library functions stay pure and testable, and the environment is read in exactly one reviewable place. -
Prove the fail-fast: run
python -m census_pipelinewithCENSUS_DATA_PATHunset and confirm it exits with a validation error naming the field, instead of running against a fallback.
Hints
- Start from the
src/layout: it forces you to install the package to import it, which is exactly what makes “works from any directory” true. - The fastest way to catch an import side effect: open a Python REPL and
import census_pipeline.model. If anything prints, loads, or writes, you have a side effect to move behind the__main__guard. - A mutable default argument (
def f(x, acc=[])) is the classic trap: check every default in your signatures.
Rubric — your project is done when
This is the standard the module holds you to (each bar maps to the lesson that taught it):
- Imports without side effects —
import-ing any module runs no job, loads no data, writes no files. (Lessons 1, 4) - Runs as a module —
python -m census_pipelinescores a record end to end. (Lessons 1, 4) - Typed data contracts at the boundary — a dataclass/pydantic model validates records; malformed input is rejected with a clear error. (Lesson 5)
- Locked dependencies — a lock file makes a fresh clone reproducible. (Lesson 6)
- Runtime config is one typed object — a
Settings(BaseSettings)insettings.pyis the only place the environment is read; a missing required value fails at startup naming the field, and library functions take arguments instead of readingos.environ. (Lesson 5, applied to config) - No mutable-default or global-state traps — behavior comes from arguments, not hidden module state. (Lesson 4)
Run the self-audit from the Make It Reproducible lesson against your own repo before you submit: for each rubric line, confirm it holds, and if one fails, name which lesson’s failure mode you reintroduced.
Submit
Use the branch workflow from the git lesson, not commits straight to main. Your first
commit establishes main as the working package; do the project work on a branch
(git checkout -b m1-package-setup), and open a pull request to merge it back once it meets
the rubric. main staying green from the start is what keeps M2 onward building on
solid ground.
When your repo meets every rubric line, merge your branch to main, push it to GitHub,
and submit the repository URL here. (Submission coming soon.)