This repository is a clean, reusable template for building Python packages with:
- Modern
src/layout - A lightweight training profiler with W&B integration
- Reproducible testing via tox
- Automated CI via GitHub Actions
- Consistent formatting and static analysis
- Easy local installation (
pip install -e .)
It is intended for developers or teams maintaining multiple internal Python packages who want consistent quality gates and minimal setup friction. In this version, the template is tuned for personal ML projects that need lightweight experiment tracking without pulling in a larger framework.
pyproject.tomlusing setuptools- Versioning via setuptools_scm
- Dependencies loaded from
requirements.txt - Editable installs supported
src/package_name/profiler.py— per-phase wall-clock profiler with optional CUDA synchronization- CSV logging of per-batch and per-epoch timing breakdowns
- W&B (Weights & Biases) logger for experiment metrics and phase timings
Preconfigured environments:
py312--- run unit tests with coveragelint--- ruff lintingtype--- mypy static typingdocs--- interrogate doc coverageformat_check--- ruff formatting + lint verificationformat--- auto-format convenience
GitHub Actions workflow (ci-quality.yml) provides:
- Formatting checks
- Linting
- Type checking
- Unit tests
- Coverage artifacts
- JUnit test reporting
The workflow is intentionally simple and suitable for private or internal packages.
.
├── .github/workflows/ci-quality.yml
├── pyproject.toml
├── requirements.txt
├── src/package_name/
│ ├── profiler.py
│ └── package_code_file.py
├── tests/
├── tox.ini
└── README.md
Use this repository as a template (GitHub "Use this template") or copy it.
Replace occurrences of:
package_name→ your import namepackage-name→ your distribution name
Locations to update:
pyproject.tomltox.inisrc/package_name/- workflow env paths (if present)
Edit:
requirements.txt
(no -r, no --extra-index-url, no editable installs).
If you do not want the profiler in a given project, remove torch,
numpy, and wandb from requirements.txt and delete
src/package_name/profiler.py.
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e .tox
tox -e lint
tox -e type
tox -e format_checkPush to GitHub --- CI runs automatically on:
- push
- pull request
- manual trigger
src/package_name/profiler.py provides three composable classes for
profiling training loops and logging results:
| Class | Role |
|---|---|
PhaseTimer |
Times named phases within each batch; optionally syncs CUDA |
ProfileLogger |
Writes per-batch and per-epoch CSV logs to disk |
WandbLogger |
Logs epoch metrics and phase timings to Weights & Biases |
Log in to W&B before running your script:
wandb loginOr set the API key via environment variable:
export WANDB_API_KEY="your-key-here"import torch
from package_name.profiler import PhaseTimer, ProfileLogger, WandbLogger
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = ...
optimizer = ...
dataloader = ...
timer = PhaseTimer(device=device)
csv_logger = ProfileLogger(log_dir="logs/profiler")
wb = WandbLogger(
config={"lr": 1e-4, "batch_size": 32},
project="my-project",
run_name="baseline",
)
for epoch in range(num_epochs):
timer.batch_records.clear()
timer.records.clear()
for batch_idx, batch in enumerate(dataloader):
# Annotate the batch with any metadata you want in the CSV
timer.start_batch(batch_idx=batch_idx, batch_size=len(batch))
with timer.time("forward"):
loss = model(batch)
with timer.time("backward"):
optimizer.zero_grad()
loss.backward()
optimizer.step()
timer.end_batch()
# Print a timing summary to stdout
timer.summary()
# Write CSVs
csv_logger.log_epoch(epoch, timer)
# Log to W&B
wb.log_epoch({"epoch": epoch, "loss": loss.item()})
wb.log_phase_timings(timer)
wb.finish()| File | Contents |
|---|---|
logs/profiler/profiler_batches.csv |
One row per batch — metadata + per-phase durations + GPU memory |
logs/profiler/profiler_epochs.csv |
One row per phase per epoch — mean/std/min/max/total |
PhaseTimer and ProfileLogger have no wandb dependency and can be
used standalone:
from package_name.profiler import PhaseTimer, ProfileLogger
timer = PhaseTimer()
logger = ProfileLogger(log_dir="logs/profiler")
for epoch in range(num_epochs):
timer.batch_records.clear()
timer.records.clear()
for batch_idx, batch in enumerate(dataloader):
timer.start_batch(batch_idx=batch_idx)
with timer.time("forward"):
...
with timer.time("backward"):
...
timer.end_batch()
logger.log_epoch(epoch, timer)
timer.summary()Typical loop:
- Write code
- Run
tox -e format - Run
tox - Commit & push
- CI verifies everything
Depending on your needs, you may want to add:
pre-commithooks- Notebook testing or linting
- Coverage upload (Codecov)
- Multi‑Python test matrix
- Wheel/sdist build job
- Private PyPI publishing workflow
This template is intentionally:
- Minimal but production‑ready
- Fast in CI
- Friendly for internal packages
- Easy to reason about
- Easy to extend
It avoids heavy frameworks (Poetry, Hatch, etc.) unless explicitly needed.
MIT License © 2026 Jan Mikelson