Skip to content

Fix: Nox Not Working — Session Errors, Virtualenv Backends, and Reuse Logic

FixDevs ·

Quick Answer

How to fix Nox errors — no noxfile.py found, session not detected, virtualenv backend uv not installed, session.install fails outside virtualenv, parametrize matrix exploding, and reuse_venv confusion.

The Error

You install Nox and the first run can’t find your file:

$ nox
nox > Failed to load Noxfile noxfile.py: No such file or directory

Or sessions you defined aren’t detected:

# noxfile.py
def test():
    ...
$ nox -l
# No sessions listed — but you defined one!

Or session.install fails inside a CI runner:

nox > InvalidUsageError: Cannot use session.install outside of a virtualenv

Or you parametrize a session and the matrix explodes to 100+ runs:

@nox.session
@nox.parametrize("python", ["3.10", "3.11", "3.12"])
@nox.parametrize("django", ["4.2", "5.0", "5.1"])
@nox.parametrize("db", ["sqlite", "postgres", "mysql"])
def tests(session, django, db):
    ...
# 3 * 3 * 3 = 27 sessions, each spawning its own venv

Or the uv backend isn’t recognized:

nox > Failed to create virtualenv: uv venv command not found

Nox is the Python-configured cousin of Tox — instead of tox.ini declarative config, you write noxfile.py with imperative session functions. This gives you full Python control over what each session does (conditionals, loops, dynamic deps) at the cost of more verbose configuration. The model is powerful for complex test matrices and dynamic resource provisioning but trips up newcomers expecting Tox-like behavior. This guide covers each common failure.

Why This Happens

Nox requires explicit decoration with @nox.session to mark a function as a session — plain functions are ignored. Sessions create their own virtual environments by default; calls to session.install(), session.run(), etc. operate within that env. Without the decorator, your functions never get registered.

The default venv backend is virtualenv, but Nox 2024+ supports uv (much faster), conda, mamba, and venv. Switching backends requires both installing the tool and configuring Nox to use it.

Fix 1: Basic noxfile.py

# noxfile.py
import nox

@nox.session
def tests(session):
    session.install("pytest", "pytest-cov")
    session.install(".")
    session.run("pytest", *session.posargs)

@nox.session
def lint(session):
    session.install("ruff>=0.5")
    session.run("ruff", "check", ".")
    session.run("ruff", "format", "--check", ".")

@nox.session
def type_check(session):
    session.install("mypy", "pydantic")
    session.install(".")
    session.run("mypy", "src/")

Run sessions:

nox                       # Run all sessions
nox -l                    # List sessions
nox -s tests              # Run a specific session
nox -s tests lint         # Multiple sessions
nox -s tests -- -v -k test_login   # Pass args via -- to pytest

session.posargs receives arguments after -- on the command line — pass them to pytest, mypy, etc.

Common Mistake: Defining a function without the @nox.session decorator. Nox silently ignores it — nox -l doesn’t show it, and nox -s function_name says “session not found.” The decorator is what registers the function as a runnable session.

Multiple decorators stack:

@nox.session(python=["3.10", "3.11", "3.12"])
def tests(session):
    session.install("pytest")
    session.run("pytest")

This creates three sessions: tests-3.10, tests-3.11, tests-3.12. Each gets its own venv with the specified Python.

Fix 2: Switching to the uv Backend (Major Speedup)

# noxfile.py
import nox

nox.options.default_venv_backend = "uv"   # Global setting

@nox.session(venv_backend="uv")           # Per-session override
def tests(session):
    session.install("pytest")
    session.run("pytest")

Install uv:

pipx install uv
# Or
uv tool install nox

The uv backend is 10–100x faster than the default virtualenv + pip combo. For a 4-session noxfile that takes 60 seconds with the default backend, switching to uv typically takes < 5 seconds.

Pro Tip: Switch to the uv backend the moment you adopt Nox. The speedup compounds across every session and every CI run — there’s no good reason to stick with the default virtualenv + pip unless you’re on an unusual platform where uv isn’t available. For uv setup details, see uv not working.

Available backends:

BackendNotes
virtualenvDefault; uses virtualenv + pip
venvStdlib venv + pip (no virtualenv dep)
uvFastest; requires uv installed
condaFor conda environments
mambaFaster conda
micromambaLighter than mamba
noneNo venv; runs in current Python (rare)

Fix 3: Session Reuse

By default, Nox creates a fresh venv for every run — slow but reproducible. Reuse envs across runs for fast iteration:

@nox.session(reuse_venv=True)
def tests(session):
    session.install("pytest")
    session.run("pytest")

Or globally:

nox.options.reuse_existing_virtualenvs = True

CLI override:

nox -r                    # Reuse all venvs
nox -R                    # Force recreate all (--no-reuse)

reuse_venv=True reinstall behavior:

@nox.session(reuse_venv=True)
def tests(session):
    session.install("pytest")   # Skipped if pytest already installed (matching version)
    session.run("pytest")

session.install() is idempotent — it doesn’t reinstall if the right version is already present. Combined with uv, this makes subsequent runs nearly instant.

Common Mistake: Reusing venvs across CI runs of different commits. CI environments are usually ephemeral, so caching the venv via GitHub Actions cache is the right pattern — Nox’s reuse_venv only helps if the venv directory persists.

Fix 4: Parametrize for Matrix Testing

import nox

@nox.session(python=["3.10", "3.11", "3.12"])
@nox.parametrize("django", ["4.2", "5.0", "5.1"])
def tests(session, django):
    session.install(f"django=={django}")
    session.install("pytest", "pytest-django")
    session.install(".")
    session.run("pytest")

This creates 9 sessions: tests-3.10(django='4.2'), tests-3.10(django='5.0'), …, tests-3.12(django='5.1').

Run a specific combination:

nox -s "tests-3.11(django='5.0')"

Quotes matter for parametrize names — shell may need escaping for parentheses.

Multiple parametrize decorators multiply:

@nox.session
@nox.parametrize("python", ["3.10", "3.11"])
@nox.parametrize("db", ["sqlite", "postgres"])
def tests(session, python, db):
    ...
# 2 * 2 = 4 sessions

Combined parametrize (when not all combos are valid):

@nox.session
@nox.parametrize(
    "python,django",
    [
        ("3.10", "4.2"),
        ("3.11", "5.0"),
        ("3.12", "5.1"),
    ],
    ids=["py310-d42", "py311-d50", "py312-d51"],
)
def tests(session, python, django):
    ...
# Only 3 sessions — one per tuple

This is better than separate @nox.parametrize when only specific combinations matter.

Common Mistake: Stacking multiple @nox.parametrize decorators that produce a Cartesian product when you actually wanted a 1-1 mapping. A 4-axis matrix can balloon to 256 sessions, taking hours. Always use the tuple form when axes are correlated (e.g., “Python 3.10 supports Django 4.2”, not “every Python supports every Django”).

Fix 5: session.install Outside a Virtualenv

nox > InvalidUsageError: Cannot use session.install outside of a virtualenv

This happens when a session has venv_backend="none" (uses the current interpreter). session.install would mutate the user’s global Python — Nox refuses.

# WRONG — venv_backend="none" + install
@nox.session(venv_backend="none")
def tests(session):
    session.install("pytest")   # Error
    session.run("pytest")

# CORRECT — venv_backend="none" + just run
@nox.session(venv_backend="none")
def tests(session):
    session.run("pytest")   # Uses already-installed pytest

# OR — use a real venv
@nox.session
def tests(session):
    session.install("pytest")
    session.run("pytest")

venv_backend="none" is useful for sessions that don’t need their own env (e.g., running an external CLI tool, formatting that doesn’t depend on project deps).

Fix 6: Conditional Logic in Sessions

This is where Nox shines — noxfile.py is real Python:

import nox
import os
import sys

@nox.session
def tests(session):
    if sys.platform == "win32":
        session.install("pytest", "pytest-xdist")
        session.run("pytest", "-n", "auto")
    else:
        session.install("pytest")
        session.run("pytest")

@nox.session
def deploy(session):
    if os.getenv("CI") != "true":
        session.error("deploy can only run in CI")

    session.install("twine")
    session.run("twine", "upload", "dist/*")

@nox.session
def benchmark(session):
    session.install("pytest", "pytest-benchmark")

    # Only run benchmarks on main branch
    if os.getenv("GITHUB_REF") != "refs/heads/main":
        session.skip("Benchmarks only on main")

    session.run("pytest", "benchmarks/")

session.skip() vs session.error():

  • session.skip(reason) — marks session as skipped (yellow in output); other sessions continue
  • session.error(reason) — marks session as errored (red); fails the run

Pro Tip: Use Nox over Tox specifically when you need conditional logic, dynamic dependency resolution, or shell-script-like flexibility. For pure declarative matrices, Tox is simpler. For “test on Linux but skip on Windows, install postgres deps only when DATABASE_URL contains postgres” — Nox’s Python-based config is the right tool. For Tox comparison, see Tox not working.

Fix 7: Default Sessions

Without args, nox runs all sessions. Customize the default:

import nox

# Only these sessions run by default; -s overrides
nox.options.sessions = ["lint", "type_check", "tests"]

@nox.session
def lint(session): ...

@nox.session
def type_check(session): ...

@nox.session
def tests(session): ...

@nox.session
def benchmark(session): ...   # Not in default — must be requested explicitly
nox                           # Runs lint, type_check, tests (not benchmark)
nox -s benchmark              # Runs benchmark only
nox -s lint benchmark         # Runs both

Tag-based selection:

@nox.session(tags=["ci"])
def tests(session): ...

@nox.session(tags=["ci", "fast"])
def lint(session): ...

@nox.session(tags=["slow"])
def benchmark(session): ...
nox -t ci                     # All sessions tagged "ci"
nox -t fast                   # All sessions tagged "fast"

Fix 8: Passing Environment Variables

Nox sessions run in a clean environment by default. To forward vars:

@nox.session
def tests(session):
    session.install("pytest")
    session.run("pytest", env={
        "DATABASE_URL": "sqlite:///test.db",
        "PYTHONDONTWRITEBYTECODE": "1",
    })

Or globally for the session:

@nox.session
def tests(session):
    session.env["DATABASE_URL"] = "sqlite:///test.db"
    session.install("pytest")
    session.run("pytest")   # Inherits the env

Pass through from CI:

import os

@nox.session
def tests(session):
    session.install("pytest")
    env = {
        "CI": os.getenv("CI", ""),
        "GITHUB_TOKEN": os.getenv("GITHUB_TOKEN", ""),
        "CODECOV_TOKEN": os.getenv("CODECOV_TOKEN", ""),
    }
    session.run("pytest", env=env)

Common Mistake: Assuming os.environ is forwarded automatically. Nox isolates the session’s environment to make runs reproducible — you must explicitly pass through anything from outside. Without this isolation, tests that pass locally fail in CI because some env var was set on your laptop.

Still Not Working?

Nox vs Tox

  • Nox — Python-configured, conditional logic, dynamic deps. Best for complex projects with non-declarative needs.
  • Tox — INI/TOML-configured, simpler for standard matrices. See Tox not working.

Use Tox for libraries with simple matrices (Python versions × dependency versions). Use Nox when sessions need to make decisions, manipulate files, or call external services.

Debugging Failed Sessions

nox -v                        # Verbose output
nox -s tests --no-stop-on-first-error
nox --report report.json      # Machine-readable report

For interactive debugging inside a session:

@nox.session
def debug(session):
    session.install("pytest", "ipdb")
    session.install(".")
    session.run("python", "-c", "import ipdb; ipdb.set_trace()")

session.run() spawns subprocesses — to enter a Python REPL with project installed, nox -s debug is faster than recreating the env by hand.

CI Integration

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
      - run: pip install nox
      - run: nox -s "tests-${{ matrix.python }}"

Cache the nox envs for faster CI:

- uses: actions/cache@v4
  with:
    path: .nox
    key: nox-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('noxfile.py', 'pyproject.toml') }}

The cache key includes hashes of noxfile.py and pyproject.toml — invalidates when sessions or deps change.

Integration with pytest

For pytest fixture patterns that run under Nox sessions, see pytest fixture not found. For Ruff and mypy linters commonly wired into Nox sessions, see Ruff not working and Python mypy type error.

Use with Hatch / Poetry

Nox doesn’t manage your project — it runs commands. Combine with Hatch or Poetry for project management:

@nox.session
def tests(session):
    # Use Hatch's env
    session.run("hatch", "run", "test", external=True)

@nox.session(venv_backend="none")
def docs(session):
    session.run("hatch", "run", "docs:build", external=True)

external=True skips Nox’s path validation — useful when calling tools outside the venv.

For Hatch-specific patterns, see Hatch not working. For Poetry, see Poetry dependency conflict.

Session Aliases and Composition

Nox lets a session call another:

@nox.session
def lint(session):
    session.install("ruff")
    session.run("ruff", "check", ".")

@nox.session
def type_check(session):
    session.install("mypy")
    session.run("mypy", "src/")

@nox.session
def all_checks(session):
    """Run lint and type_check in this session's env."""
    session.notify("lint")
    session.notify("type_check")

session.notify(name) queues another session to run after the current one — they share a CLI invocation but each has its own venv. Useful for “run this group of related checks together.”

File Discovery

For sessions that operate on files in the project (formatters, validators), use the nox.options.envdir pattern with proper exclusions:

import nox

nox.options.envdir = ".nox"
nox.options.error_on_external_run = True   # Fail if a command isn't in the venv

@nox.session
def format(session):
    session.install("ruff")
    session.run("ruff", "format", ".", external=False)
    session.run("ruff", "check", "--fix", ".", external=False)

error_on_external_run catches bugs where you accidentally call a globally-installed tool instead of the env’s version.

Conditional Skips for Local-Only Tests

import nox
import shutil

@nox.session
def integration(session):
    if shutil.which("docker") is None:
        session.skip("docker not installed; skipping integration tests")

    session.install("pytest")
    session.install(".")
    session.run("pytest", "tests/integration/")

This pattern is the killer feature of Nox over Tox — express “skip if X” cleanly in Python.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles