The Nox Cookbook¶
The What?¶
A lot of people and a lot of projects use Nox for their python automation powers.
Some of these sessions are the classic “run pytest and linting”, some are more unique and more interesting!
The Nox cookbook is a collection of these sessions.
Nox is super easy to get started with, and super powerful right out of the box. But when things get complex or you want to chain together some more powerful tasks, often the only examples can be found hunting around GitHub for novel sessions.
The kind of sessions that make you think “I didn’t know you could do that!”
This cookbook is intended to be a centralized, community-driven repository of awesome Nox sessions to act as a source of inspiration and a reference guide for Nox’s users. If you’re doing something cool with Nox, why not add your session here?
Contributing a Session¶
Anyone can contribute sessions to the cookbook. However, there are a few guiding principles you should keep in mind:
Your session should be interesting or unique, it should do something out of the ordinary or otherwise interesting.
You should explain briefly what it does and why it’s interesting.
For general advice on how to contribute to Nox see our Contributing guide
Recipes¶
Instant Dev Environment¶
A common sticking point in contributing to python projects (especially for beginners) is the problem of wrangling virtual environments and installing dependencies.
Enter the dev nox session:
import nox
# It's a good idea to keep your dev session out of the default list
# so it's not run twice accidentally
@nox.session(default=False)
def dev(session: nox.Session) -> None:
"""
Set up a python development environment for the project at ".venv".
"""
session.install("virtualenv")
session.run("virtualenv", ".venv", silent=True)
# Use the venv's interpreter to install the project along with
# all its dev dependencies, this ensures it's installed in the right way
session.run(".venv/bin/pip", "install", "-e", ".[dev]", external=True)
With this, a user can simply run nox -s dev and have their entire environment set up automatically!
The Auto-Release¶
Releasing a new version of an open source project can be a real pain, with lots of intricate steps. Tools like Bump My Version really help here.
Even more so with a sprinkling of Nox:
import argparse
import nox
@nox.session
def release(session: nox.Session) -> None:
"""
Kicks off an automated release process by creating and pushing a new tag.
Invokes bump-my-version with the posarg selecting the version part.
Usage:
$ nox -s release -- [major|minor|patch]
"""
parser = argparse.ArgumentParser(description="Release a semver version.")
parser.add_argument(
"version",
type=str,
nargs=1,
help="The type of semver release to make.",
choices={"major", "minor", "patch"},
)
args: argparse.Namespace = parser.parse_args(args=session.posargs)
version: str = args.version.pop()
# If we get here, we should be good to go
# Let's do a final check for safety
confirm = input(
f"You are about to bump the {version!r} version. Are you sure? [y/n]: "
)
# Abort on anything other than 'y'
if confirm.lower().strip() != "y":
session.error(f"You said no when prompted to bump the {version!r} version.")
session.install("bump-my-version")
session.log(f"Bumping the {version!r} version")
session.run("bump-my-version", "bump", version)
session.log("Pushing the new tag")
session.run("git", "push", external=True)
session.run("git", "push", "--tags", external=True)
Now a simple nox -s release -- patch will automate your release (provided you have Bump My Version set up to change your files). This is especially powerful if you have a CI/CD pipeline set up!
Using a lockfile¶
If you use a tool like uv to lock your dependencies, you can use that inside a nox session. Here’s an example:
@nox.session(venv_backend="uv")
def tests(session: nox.Session) -> None:
"""
Run the unit and regular tests.
"""
session.run_install(
"uv",
"sync",
"--extra=test",
"--no-default-extras",
f"--python={session.virtualenv.location}",
env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
)
session.run("pytest", *session.posargs)
Here we run uv sync on the nox virtual environment. Other useful flags might include --locked (validate lockfile is up-to-date) and --inexact (will allow you to install other packages as well).
The nox-uv package can be used to reduce the boilerplate needed to uv sync specific dependency groups or extras into the nox virtual environment.
By default, nox-uv also validates that the lockfile is up-to-date.
#!/usr/bin/env -S uv run --script --quiet
# /// script
# dependencies = ["nox", "nox-uv"]
# ///
import nox
import nox_uv
nox.options.default_venv_backend = "uv"
@nox_uv.session(
python=["3.10", "3.11", "3.12", "3.13"],
uv_groups=["test"],
)
def test(s: nox.Session) -> None:
"""`uv sync` main dependencies and the `test` dependency group."""
s.run("python", "-m", "pytest")
@nox_uv.session(uv_groups=["type_check"])
def type_check(s: nox.Session) -> None:
"""`uv sync` main dependencies and the `type_check` dependency group."""
s.run("mypy", "src")
@nox_uv.session(uv_only_groups=["lint"])
def lint(s: nox.Session) -> None:
"""`uv sync` only the `lint` dependency group."""
s.run("ruff", "check", ".")
s.run("ruff", "format", "--check", ".")
if __name__ == "__main__":
nox.main()
Testing on every supported Python¶
The Programming Language :: Python :: 3.X trove classifiers in
pyproject.toml already list every Python your project supports.
nox.project.python_versions() reads them, so the session matrix follows
the classifiers without a second copy of the list. If the project also has a
Programming Language :: Python :: Free Threading classifier, the
non-experimental free-threaded builds (3.14t and newer) are added too:
[project]
classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Programming Language :: Python :: Free Threading :: 3 - Stable",
]
import nox
PYPROJECT = nox.project.load_toml("pyproject.toml")
# ["3.12", "3.13", "3.14", "3.14t, "3.15", "3.15t"]
ALL_PYTHON = nox.project.python_versions(PYPROJECT)
@nox.session(python=ALL_PYTHON)
def tests(session: nox.Session) -> None:
session.install("-e.", "pytest")
session.run("pytest")
Pass free_threaded=False to leave the free-threaded builds out, or
free_threaded=True to add them without the classifier. 3.13t is never
added automatically, as free threading was experimental in 3.13.
Combining coverage across Python versions¶
Code paths can vary between Python versions, so measuring each test run separately and then combining the data gives a more accurate coverage report. The important parts are to remove stale data before the matrix starts, give every run a unique data file, and combine the files only after every test session finishes:
import nox
PYPROJECT = nox.project.load_toml("pyproject.toml")
PYTHON_VERSIONS = nox.project.python_versions(PYPROJECT)
@nox.session(default=False)
def coverage_erase(session: nox.Session) -> None:
"""Remove data left by an earlier coverage run."""
session.install("coverage[toml]")
session.run("coverage", "erase")
@nox.session(
python=PYTHON_VERSIONS,
default=False,
requires=["coverage_erase"],
)
def tests(session: nox.Session) -> None:
"""Measure tests independently for each supported Python version."""
session.install("coverage[toml]", "--group=test", ".")
session.run(
"coverage",
"run",
"-m",
"pytest",
*session.posargs,
env={"COVERAGE_FILE": f".coverage.{session.python}"},
)
@nox.session(requires=["tests"])
def coverage_report(session: nox.Session) -> None:
"""Combine the per-version data and display one report."""
session.install("coverage[toml]")
session.run("coverage", "combine")
session.run("coverage", "report", "--show-missing")
Running nox selects coverage_report by default; its requirements run
cleanup first and then every parametrized test session. coverage combine
writes the combined .coverage data file and removes the per-version files it
consumed.
Keep the phases sequential so cleanup and reporting cannot race with the test
sessions. Projects that use subprocesses or pytest-xdist should also enable
Coverage.py’s parallel mode.
Generating a matrix with GitHub Actions¶
Nox knows what sessions it needs to run. Why not tell GitHub Actions what jobs to run dynamically? Using the --json flag and a bit of json processing, it’s easy:
jobs:
generate-jobs:
runs-on: ubuntu-latest
outputs:
session: ${{ steps.set-matrix.outputs.session }}
steps:
- uses: actions/checkout@v3
- uses: wntrblm/nox@main
- id: set-matrix
shell: bash
run: echo session=$(nox --json -l | jq -c '[.[].session]') | tee --append $GITHUB_OUTPUT
checks:
name: Session ${{ matrix.session }}
needs: [generate-jobs]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
session: ${{ fromJson(needs.generate-jobs.outputs.session) }}
steps:
- uses: actions/checkout@v3
- uses: wntrblm/nox@main
- run: nox -s "${{ matrix.session }}"