Nox automates development tasks in isolated environments through noxfile.py. Each session installs dependencies and runs commands, reducing drift between developer machines and CI.

Define sessions

python -m pip install nox
import nox


@nox.session(python=["3.12", "3.13"])
def tests(session: nox.Session) -> None:
    session.install(".", "pytest")
    session.run("pytest", "-q", *session.posargs)


@nox.session
def lint(session: nox.Session) -> None:
    session.install("ruff")
    session.run("ruff", "check", ".")

Run all selected sessions with nox, list them with nox -l, or choose nox -s lint. session.posargs forwards filters without editing configuration.

Reproducible dependencies

Do not install unrelated versions locally and in CI. Use the project's lock mechanism and make sessions consume the same source. Reusing environments speeds local runs, but a clean CI job should still prove installation from scratch.

Compare tox for multiple Python versions and use uv when it owns dependency resolution. Avoid divergent commands across scripts and CI; call a common workflow.

The official Nox documentation, accessed July 22, 2026, covers sessions, parametrization, and environment backends. Pin critical automation versions and review changes before upgrading.

Understand session isolation

Each session creates a virtual environment. session.install() installs tools there; session.run() invokes a program and fails on a nonzero exit code. Nox does not replace pytest, Ruff, or Sphinx: it coordinates them. Set python= when the interpreter is part of the test. Install every matrix version in CI and never count a skipped session as a pass.

Install the project like a user

Test the installed package, not only files visible from the working directory:

import nox


@nox.session(python=["3.11", "3.12", "3.13"])
def tests(session: nox.Session) -> None:
    session.install(".[test]")
    session.run("pytest", "-q", *session.posargs)


@nox.session
def typecheck(session: nox.Session) -> None:
    session.install(".[typing]")
    session.run("mypy", "src")

Declare extras such as test and typing in pyproject.toml, so sessions use the team's dependency source. For libraries, a normal installation also reveals files missing from the wheel that a PYTHONPATH adjustment would hide.

Forward arguments and measure coverage

Everything after -- becomes session.posargs. nox -s tests -- tests/test_api.py -k login selects a subset without editing configuration:

@nox.session
def coverage(session: nox.Session) -> None:
    session.install(".[test]")
    args = session.posargs or ["tests"]
    session.run("coverage", "run", "-m", "pytest", *args)
    session.run("coverage", "report", "--fail-under=90")

Do not construct a shell string from forwarded input. Separate arguments avoid platform-specific escaping and unnecessary shell interpretation.

Parametrize with a purpose

@nox.session(python=["3.12", "3.13"])
@nox.parametrize("django", ["4.2", "5.1"])
def compatibility(session: nox.Session, django: str) -> None:
    session.install(".", f"django~={django}.0", "pytest")
    session.run("pytest", "tests/compat")

Avoid an accidental Cartesian product. Each combination costs installation and runtime. Select minimum and maximum supported versions, document that policy, and reserve a larger matrix for scheduled jobs.

Reuse, documentation, and builds

nox -r reuses environments and speeds up local feedback, but it does not prove that removed dependencies disappeared or that a clean install works. CI should create fresh environments regularly. Caching downloads differs from restoring an entire virtual environment.

@nox.session
def docs(session: nox.Session) -> None:
    session.install(".[docs]")
    session.run("sphinx-build", "-W", "docs", "build/docs")


@nox.session
def build(session: nox.Session) -> None:
    session.install("build")
    session.run("python", "-m", "build")

-W turns documentation warnings into failures. Keep publishing outside default sessions: uploading changes external state and requires credentials, review, and a trusted source.

Use one workflow in CI

The pipeline should call nox -s tests instead of copying installation and test commands into YAML. CI can choose the interpreter matrix, while logic stays in noxfile.py. Use nox -l to inspect names and descriptions.

Pin versions according to project policy and keep sessions small, with clear inputs and outputs. This lets a developer reproduce locally the same failure seen in continuous integration. When results are inconsistent, recreate the environment before blaming application code.

External commands and variables

Prefer programs installed inside the session. When a tool must come from the host, declare that dependency visibly instead of silencing warnings. This prevents a pass caused by an unexpected executable on one developer's machine.

Forward only required variables. Tokens and passwords do not belong in noxfile.py; CI should expose them only to an authorized session. Put publishing or destructive work in a separate, non-default session and verify a release condition.

Keep sessions observable

Use descriptive names and reliable exit codes. Do not catch a failure merely to continue and report success. If an optional step may fail, explain why in the log. Automation should show the command, Python version, and non-sensitive inputs.

Review noxfile.py like production code: format it, lint it, and test complex helpers. Extract a small function when repetition appears, but retain a workflow readers can follow directly.

Give public sessions descriptions so nox -l works as a concise guide. Remove obsolete sessions instead of preserving aliases forever, and verify commands from a clean clone without personal configuration. Record required system tools in contributor documentation so failures remain actionable.