This template should be used for every Python project in the lab. It uses:
uvfor dependency management.rufffor code formatting.tyfor type checking.pre-commithooks for automated validation.pytestfor testing.- GitHub Actions to run the hooks and the tests on every push.
We use uv for dependency management. It is just as full-featured as
poetry, but much faster. Follow the instructions below to create a new project:
-
Update the name of the project in
pyproject.toml. This is the distribution name, and it usually matches the name of the repository (hyphens are fine here). -
Rename the folder
src/package_nameto the name of your package, and update the import intests/test_hello.pyto match. This is a Python module name, so it must use underscores rather than hyphens (my_project, notmy-project) — otherwise it cannot be imported. -
Run
uv syncfrom the root of the repo. This will create a virtual environment and install needed development dependencies. -
Add the dependencies you need (and run this same command every time you need a new package):
uv add polars lightgbm
-
Take a look at the
uv's Getting started guide.
Install the pre-commit hooks:
uvx pre-commit installThis will create a .git/hooks/pre-commit file that will run the pre-commit hooks every time you
commit. Upon the first commit, the hooks will be installed.
Some hooks output error message that require a manual change (e.g., linting errors). Other hooks perform automated fixes. Either way, you need to re-run the commit command:
git commit -m "My message"Among the pre-commit hooks, you will find one that runs ruff on
every Python file. It is also warmly recommended that you set up ruff in your IDE (e.g., Visual
Studio Code, PyCharm).
We recommend the use of type hints of your code.
One of the pre-commit hooks is ty, which will perform type checking
when hints are available. This reduces greatly the risk of bugs and the maintainability of the code.
Tests live in tests/ and are run with pytest:
uv run pytestUnlike the linters, pytest is a dev dependency in pyproject.toml rather than a uvx tool: it
has to import your package, so it needs the project environment. It is installed by uv sync.
tests/test_hello.py is a stub covering the example hello function. Populate tests/ as follows:
-
Delete
src/package_name/hello.pyandtests/test_hello.pyonce your own code replaces the example. -
Name test files
test_*.pyand test functionstest_*, mirroring the layout of your package (src/package_name/foo.pyis tested bytests/test_foo.py). -
Write one test per behaviour you want to keep working, and name it after that behaviour (
test_hello_returns_greeting, nottest_1). A test that would still pass if the function were broken is not worth having. -
Cover the cases you are tempted to check by hand in a notebook: the empty input, the boundary, the error path. Add a test reproducing any bug you fix, so it cannot come back.
-
Keep tests fast and independent of the lab storage. If a test needs data, generate a small fixture in the test itself rather than reading from a data path.
The same style rules apply to test code as to the rest: type hints and a short numpy-style docstring on every test function.
.github/workflows/ci.yml defines two jobs, which run on every push to main and on every pull
request:
pre-commitruns every hook over all files, so CI fails on anything you did not run locally.pytestrunsuv sync --frozenand the test suite.--frozenfails ifuv.lockis out of date withpyproject.toml, so commit the lockfile whenever you add a dependency.
Both jobs pin the Python version through the PYTHON_VERSION variable at the top of the workflow.
It should match the version in containers/apptainer.def; change it in both places. To reproduce a
CI failure locally:
uvx pre-commit run --all-files
uv run pytest