This repository demonstrates how to structure a production-ready, end-to-end machine learning project.
The focus is not on models, dataset and training code per say, but on the infrastructure and engineering practices that make ML projects maintainable, testable, and scalable over time.
Follow the installation instructions here:
https://astral.sh/uv
git clone <repo-url>
cd ml-project-foundationsuv sync --all-groupsNotice that uv sync:
- Creates (or reuses) the project virtual environment
- Ensures a compatible Python version is available (based on
.python-version), installing it if necessary - Installs all dependencies from
pyproject.toml/uv.lock - Installs the current project in editable mode, meaning:
- Changes to the source code are immediately reflected
- The package can be imported and run as an installed module from the environment
We can now run eg.
uv run pytest
uv run mypy src
uv run python -m ml_project_foundations.runner.main experiment.name=<NAME>
uv run python -m ml_project_foundations.some_fileThe responsibility of this repo is to run machine learning models in various states: training, evaluation, testing, and prediction. The main entry point is:
uv run python -m ml_project_foundations.runner.main experiment.name=<NAME>Remark: setting experiment name is enforced.
Most of the control is handled through configuration files. We use Hydra, which allows us to merge configs, override them via the CLI, and run multiruns seamlessly.
In some cases we want to continue a run, for that we do:
uv run python -m ml_project_foundations.runner.main experiment.name=<NAME> run.ckpt=<run_id> run.which=<last or epoch>for run_id see mlflow UI and which see artifact folder for the correct naming
The dataset handling layer is implemented directly in this repo (for example, data_module.py). The reason for implementing it here is the tight coupling between how data is handled and how the model is trained.
The actual data resolution and access are handled via the ML-project-foundations-data-quarry repo. More specifically, data is accessed using:
from data_quarry.tools import get_file_pathsFor experiment tracking and full model lifecycle management, we use MLflow. MLflow provides:
- Lineage (which experiment and run produced a model)
- Versioning and aliasing
- Metadata tagging and annotations
This ensures full visibility from development through production deployment.
Remark: MLflow now uses aliases for model lineage in the registry; the old stage-based approach is deprecated.
In a collaborative environment, MLflow is normally deployed as a shared service
in the cloud rather than started on each developer's machine. The deployment
should use a durable database for tracking metadata and shared object storage for
artifacts. An administrator configures the service, access control, and storage;
team members only need to point MLFLOW_TRACKING_URI at its URL:
$env:MLFLOW_TRACKING_URI = "https://mlflow.example.com"This gives the team a common view of experiments, runs, metrics, artifacts, and registered models. The local server below is intended for development and testing when a shared server is unavailable.
Training expects an MLflow tracking server at the URI in MLFLOW_TRACKING_URI.
For local development, start the server from the repository root in its own
terminal:
New-Item -ItemType Directory -Force .local\mlflow\artifacts | Out-Null
uv run mlflow server `
--host 127.0.0.1 `
--port 5000 `
--workers 1 `
--backend-store-uri "sqlite:///./.local/mlflow/mlflow.db" `
--artifacts-destination "./.local/mlflow/artifacts"Keep that terminal running. The MLflow UI is available at
http://localhost:5000, and all local state is stored
under the Git-ignored .local/mlflow/ directory.
In a second terminal, configure the client and start a run:
$env:MLFLOW_TRACKING_URI = "http://localhost:5000"
uv run python -m ml_project_foundations.runner.main `
experiment.name=my_experiment `
trainer.fast_dev_run=trueIf the server is not running, training fails with a connection-refused error for
localhost:5000. Stop the server with Ctrl+C when it is no longer needed.
We do not use MLflow for dataset versioning, for two main reasons:
- The UI does not clearly showcase dataset lineage separately from models.
- MLflow does not track data in a Git-like way.
To address this, we use DVC together with a separate repo This setup provides a clear UI for dataset lineage, including metadata, data contracts, and tracking, which helps us avoid duplicate data.
This project aims to:
- Showcase a typical ML project layout, including project structure, tooling, and best practices
- Explain the technologies and packages used, what problems they solve, and how they fit together
- Serve as a long-term reference for learning, experimentation, and future projects
Every serious ML project needs solid infrastructure. This includes:
-
Clear project and folder structure
-
Predictable Python imports and packaging
-
Reproducible Python environments and dependency management
-
Consistent code formatting and linting
-
Continuous Integration (CI) via GitHub Actions, i.e. automated building and testing
-
CD if used(might add for later)
This repository is designed to demonstrate how all of these pieces work together in a realistic setup.
The project follows a src/-based layout:
Using a src/ directory ensures:
- Predictable and explicit imports
- Tests import the package the same way it will be used in production
- Fewer issues with accidental relative imports (e.g. when running
pytest)
The structure will evolve as the project grows, with each major folder introduced and explained when needed.
This project uses uv for:
- Python version management
- Virtual environment creation
- Dependency installation
- Packaging the project as a Python module
uv is a modern, fast alternative to traditional setups like venv + pip, keeping environment and dependency management under one umbrella.
To enforce consistent and high-quality code, the project uses:
- Black for code formatting
- Ruff for linting
- mypy for static type checking
- pre-commit hooks to enforce formatting, linting, and type checks on every commit
This ensures that code style, correctness, and type safety are handled automatically.
Static type checking is done using mypy.
- Helps catch bugs early by validating type correctness
- Improves code readability and self-documentation
- Makes refactoring safer as the project grows
Type checking configuration is managed via pyproject.toml and enforced both locally and in CI.
Testing is done using pytest.
- All tests must pass before a pull request can be merged
- Tests are executed in an isolated environment
- Failures block merging to the main branch
This project uses GitHub Actions for Continuous Integration.
On every pull request, the CI pipeline:
- Sets up a clean Python environment
- Installs dependencies using
uv - Runs:
- Code formatting checks
- Linting
- Static type checking with
mypy - Tests with
pytest
This ensures that all code merged into the repository meets the same quality and correctness standards. When using astral-sh/setup-uv in GitHub Actions, uv automatically persists its internal cache between workflow runs.
Most project configuration lives in pyproject.toml, including:
- Project packaging configuration
- Dependency definitions
- Tooling configuration (uv, black, ruff, mypy, pytest, etc.)
Centralizing configuration keeps the project easier to reason about and maintain.
Hydra configs that contain _target_ use flat constructor arguments. Each key
beside _target_ is passed directly to the target class:
_target_: ml_project_foundations.models.model1.Model1
input_dim: 5
target_dim: 1
lr: 1e-3This is equivalent to Model1(input_dim=5, target_dim=1, lr=1e-3) and keeps
application classes independent of DictConfig. Select and override a model with:
uv run python -m ml_project_foundations.runner.main experiment.name=my_run model=model1 model.lr=5e-4This project prioritizes consistency and reproducibility over editor-specific behavior.
All formatting, linting, and type checking are enforced at commit time and in CI, ensuring identical results regardless of editor or local configuration.
Installing dependencies and running Python code produces various generated artifacts. These files are expected, but they should not be committed to the repository.
The following entries are included in .gitignore to prevent repository bloat:
__pycache__/
*.pyc
*.egg-info/VSCode is my current choice of editor. A typical setup for this kind of projects.
Useful extensions include:
- Python (official Microsoft extension)
- GitLens (optional, for Git insights)
Formatting, linting, and type checking are intentionally not handled by editor extensions. Instead, they are enforced via:
pre-commithooks- GitHub Actions (CI)
This avoids editor-specific discrepancies and ensures a single source of truth.
When installing dependencies or running Python code, various generated artifacts
are created (e.g. __pycache__, .egg-info directories and *.pyc files).
These files are expected but should not be interacted with directly.
To reduce noise in the editor, the following exclusions are configured in
.vscode/settings.json:
{
"files.exclude": {
"**/.git": true,
"**/__pycache__": true,
"**/*.pyc": true,
"**/*.egg-info": true,
"**/.pytest_cache": true,
"**/.mypy_cache": true,
"**/.ruff_cache": true
}
}This repository follows a simple and opinionated Git workflow to keep the history
clean and the main branch stable.
Workflows and conventions may vary between teams and projects. The choices here reflect a reasonable default rather than a strict prescription.
Repository rules and branch protections are configured directly via the GitHub web interface.
The main branch is protected and represents the current stable state of the
project.
Direct pushes to main are disabled. All changes must go through a pull request
(PR).
Branch protection rules enforce:
- All CI checks passing (tests, linting, type checking)
- No force-pushes to
main - No direct commits to
main
This ensures that every change merged into main meets the same quality standards.
All development is done on feature branches.
Pull requests are used to:
- Run the full CI pipeline in a clean environment
- Review changes before merging
- Provide a clear audit trail of why changes were made
Once a pull request is merged, the feature branch is deleted to keep the repository tidy and avoid stale branches.
Repository rulesets are used to centrally define and enforce:
- Branch protection rules
- Required status checks(eg. passing pytest)
- Merge requirements
Using rulesets ensures consistent enforcement across branches and avoids relying on individual developer discipline.
