Fix: Nox Not Working — Session Errors, Virtualenv Backends, and Reuse Logic
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 directoryOr 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 virtualenvOr 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 venvOr the uv backend isn’t recognized:
nox > Failed to create virtualenv: uv venv command not foundNox 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 pytestsession.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 noxThe 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:
| Backend | Notes |
|---|---|
virtualenv | Default; uses virtualenv + pip |
venv | Stdlib venv + pip (no virtualenv dep) |
uv | Fastest; requires uv installed |
conda | For conda environments |
mamba | Faster conda |
micromamba | Lighter than mamba |
none | No 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 = TrueCLI 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 sessionsCombined 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 tupleThis 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 virtualenvThis 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 continuesession.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 explicitlynox # Runs lint, type_check, tests (not benchmark)
nox -s benchmark # Runs benchmark only
nox -s lint benchmark # Runs bothTag-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 envPass 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 reportFor 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Tox Not Working — Environment Creation, Config Errors, and Multi-Python Testing
How to fix Tox errors — ERROR cannot find Python interpreter, tox.ini config parsing error, allowlist_externals required, recreating environments slow, pyproject.toml integration, and matrix env selection.
Fix: freezegun Not Working — Datetime Not Frozen, Timezone Issues, and Async Tests
How to fix freezegun errors — freeze_time decorator not affecting datetime.now, timezone-aware datetime mismatch, time.time not frozen, async test time leak, third-party library still using real time, and tick parameter behavior.
Fix: Moto Not Working — Mock Decorator, Real AWS Calls Leaking, and v4 to v5 Migration
How to fix Moto errors — mock not activating, real AWS credentials used in tests, ImportError mock_s3 removed in v5, fixtures with multiple services, NoCredentialsError despite mock, and standalone server mode.
Fix: Hypothesis Not Working — Strategy Errors, Flaky Tests, and Shrinking Issues
How to fix Hypothesis errors — Unsatisfied assumption, Flaky test detected, HealthCheck data_too_large, strategy composition failing, example database stale, settings profile not found, and stateful testing errors.