How-to
Automating Reporting WorkflowsDeep dive

Test Excel Code Against Several Library Versions with nox

Find out before your users do whether your Excel automation still works with the next pandas or openpyxl release, and still works with the oldest versions you claim to support: a nox session matrix over Python and pandas versions, uv for fast environments, warnings turned into errors, and a lowest-versions session that checks the minimums in pyproject.toml.

Excel automation code depends heavily on a few libraries — pandas, openpyxl, perhaps XlsxWriter or Polars — and those libraries change. pandas 3 switched text columns to a new string type, which quietly changed what select_dtypes("object") returns; openpyxl releases occasionally change how styles or charts are written. Code that is installed on other people's machines, or deployed to servers that are upgraded on their own schedule, runs against versions you did not test. nox runs your test suite in a fresh environment for each combination you care about — two Python versions times two pandas versions, plus the oldest versions your package claims to support — with one command. This page sets that up with uv for speed, turns deprecation warnings into failures so upcoming breakages show up early, finds a real pandas 3 deprecation in a small report module, and uses the lowest-versions session to discover that a declared minimum version was wrong. It belongs to Testing and Packaging Excel Automation Scripts.

Prerequisites

Bash
pip install nox        # uv must also be installed: https://docs.astral.sh/uv/

The project is a small installable package, excelreport, with one function that reads product codes from a workbook and trims the text columns, and one pytest test for it; Set Up an Excel Automation Project with uv covers that layout. uv has to be able to fetch the Python versions and packages named below, so the first run needs network access; later runs come from its cache.

Step 1 — The project and its version claims

The package declares the versions it supports in pyproject.toml. Those claims are exactly what the test matrix will check:

Toml
[project]
name = "excelreport"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["pandas>=2.2", "openpyxl>=3.1"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
filterwarnings = ["error"]
Python
import pandas as pd

def load_codes(path):
    df = pd.read_excel(path, dtype={"code": str})
    text = df.select_dtypes(include="object").columns
    df[text] = df[text].apply(lambda s: s.str.strip())
    return df

filterwarnings = ["error"] makes pytest treat every warning as a test failure. That is deliberate: libraries announce breaking changes with DeprecationWarning, FutureWarning and their own warning classes one or more releases ahead, and a warning in the test output is easy to scroll past, whereas a failing session is not. If a third-party warning you cannot fix gets in the way, ignore that one specifically with an ignore:: entry rather than relaxing the rule.

Step 2 — A nox session matrix

The noxfile.py defines sessions. One decorator multiplies the session over Python versions, another over pandas versions:

Python
import nox

nox.options.default_venv_backend = "uv"
nox.options.sessions = ["tests"]

@nox.session(python=["3.11", "3.13"])
@nox.parametrize("pandas", ["2.2.3", "3.0.6"])
def tests(session, pandas):
    session.install(".", "pytest", f"pandas=={pandas}")
    session.run("pytest", "-q")

@nox.session(python="3.11")
def lowest(session):
    session.install("--resolution", "lowest-direct", ".")
    session.install("pytest")
    session.run("python", "-c", "import pandas, openpyxl; print('pandas', pandas.__version__, 'openpyxl', openpyxl.__version__)")
    session.run("pytest", "-q")

nox alone now runs four sessions — Python 3.11 and 3.13, each with pandas 2.2.3 and 3.0.6 — each in its own virtual environment. With default_venv_backend = "uv", environments are created and filled by uv, which takes a second or two per session instead of the best part of a minute with pip. session.install(".", …) installs the package itself, so the tests run against the installed code, exactly as users get it, rather than against the source folder. The lowest session is explained in Step 4.

The test matrix nox runs Four sessions combine Python 3.11 and 3.13 with pandas 2.2.3 and 3.0.6. A fifth session installs the lowest versions allowed by pyproject.toml on Python 3.11. Before the fix both pandas 3 sessions fail on a deprecation warning. pandas 2.2.3 pandas 3.0.6 Python 3.11 pass fail — warning Python 3.13 pass fail — warning lowest (3.11) minimums from pyproject

Step 3 — Run it and read the failures

Run nox in the project folder. Here the same command is run from Python so the summary can be shown:

Python
import subprocess, sys

def nox(*args):
    run = subprocess.run([sys.executable, "-m", "nox", "--no-color", *args], capture_output=True, text=True)
    lines = (run.stdout + run.stderr).splitlines()
    errors = {" ".join(l[1:].split()) + " " + " ".join(lines[i + 1][1:].split()) for i, l in enumerate(lines[:-1])
              if l.startswith("E ") and l.rstrip().endswith(("Warning:", "Warning: ")) or l.startswith("E ") and "Warning:" in l}
    for e in sorted(errors):
        print("E", e[:105] + " …")
    summary = [l for l in lines if l.startswith("nox > *")] or [l for l in lines if l.startswith("nox > Session")]
    print("\n".join([l for l in lines if l.startswith("pandas ")] + summary))

nox()

Both pandas 2.2.3 sessions pass and both pandas 3 sessions fail with a Pandas4Warning: in pandas 3, text columns read from Excel have the new str dtype, and select_dtypes(include="object") only still includes them for backward compatibility — "this behavior is deprecated and will be removed". The code works today but will stop trimming text columns in a future release, silently, which is the worst kind of breakage for a cleaning function. Without the matrix, and without warnings as errors, nobody would notice until it happened. The fix is to name both kinds of text column:

Python
from pathlib import Path

src = Path("src/excelreport/__init__.py")
src.write_text(src.read_text().replace('include="object"', 'include=["object", "string"]'))
nox()

All four sessions now pass: "string" selects the pandas 3 text dtype and the older string dtype, and "object" still covers pandas 2's object columns. Many pandas 3 changes are of this kind — code that runs but means something slightly different — which is why running the suite against both major versions during the transition is worth the few seconds it costs. pandas' own migration guide lists the changes; Fix pandas FutureWarnings in Excel Pipelines works through the common ones.

Step 4 — Check the minimum versions you claim

pandas>=2.2 in pyproject.toml is a promise: the package works with pandas 2.2.0. Nothing so far tests that, because every session installed a recent release. The lowest session installs the lowest version of each direct dependency that satisfies the declared ranges — uv's --resolution lowest-direct — and runs the tests:

Python
nox("-s", "lowest")

It fails — not on the package's code but on importing pandas 2.2.0, which emits a DeprecationWarning about pyarrow becoming a required dependency; pandas removed that warning again in 2.2.2. With warnings as errors, the oldest version the package claims to support breaks its own test suite, and users pinned to it would see the warning on every run. The honest fix is to raise the floor to the oldest version that actually works:

Python
toml = Path("pyproject.toml")
toml.write_text(toml.read_text().replace('"pandas>=2.2"', '"pandas>=2.2.2"'))
nox("-s", "lowest")

With the minimum raised, the lowest session installs pandas 2.2.2 and openpyxl 3.1.0 and the test passes. pytest is installed in a separate step so that it is not resolved to its own lowest version — only the package's direct dependencies are of interest here. Lower bounds are easy to write and easy to get wrong; this session keeps them true. The same idea applies to requires-python: the oldest Python in the matrix should be the one the package claims, here 3.11, so the claim and the test cannot drift apart.

What the lowest-versions session found The package claimed to work with pandas 2.2. The lowest session installed pandas 2.2.0, whose import warning failed the tests under warnings-as-errors. Raising the minimum to 2.2.2 made the lowest session pass with pandas 2.2.2 and openpyxl 3.1.0. Claim pandas>=2.2 lowest-direct installs pandas 2.2.0 Import warning pyarrow notice, test fails Fix the claim pandas>=2.2.2, passes Which versions belong in the matrix Test the oldest and newest Python versions you support, the previous and current major versions of the libraries the code depends on most, and the lowest versions allowed by pyproject.toml. Pre-release versions belong in a separate nightly session rather than the main matrix. Which versions to test? Python oldest and newest 3.11, 3.13 key libraries previous and current major pandas 2 and 3 lower bounds lowest-direct session from pyproject.toml

Running it in CI

Locally, run the matrix before a release or after upgrading dependencies; nox -s tests runs just the matrix and nox -s "tests-3.13(pandas='3.0.6')" a single combination. In continuous integration, install uv and nox and run nox as the test step; nox's --report option writes a JSON summary of every session, and its exit code fails the build if any session failed. Keep the matrix small and meaningful — the oldest and newest Python you support, the previous and current major version of the libraries you depend on most, and the lowest-versions session — rather than every combination. A nightly session that installs the newest pre-release versions (session.install("--prerelease", "allow", "pandas")) gives even earlier warning of what is coming. Generating test workbooks inside the tests themselves, so the matrix never depends on files someone forgot to commit, is covered in Create Excel Test Fixtures in Memory.

Common pitfalls

  • Testing only the versions on your machine. Users and servers run others.
  • Warnings left as warnings. Make them errors so deprecations fail a session.
  • Untested lower bounds. Add a --resolution lowest-direct session.
  • Testing the source folder. Install the package in each session.
  • A huge matrix. Test the edges that matter, not every combination.

Conclusion

Use nox to run your Excel code's tests in a clean environment for each combination of Python and library versions you support, with uv making each session take seconds. Turn warnings into errors so that deprecations — such as pandas 3's change to text column dtypes — fail a session long before they break real reports, and add a lowest-versions session so the minimums in pyproject.toml are tested rather than assumed.

Frequently asked questions

How do I test my code with two versions of pandas? Write a nox session decorated with nox.parametrize over the pandas versions and install the version passed in; nox creates a separate environment for each.

How do I make nox faster? Set nox.options.default_venv_backend to uv, so environments are created and populated by uv instead of virtualenv and pip.

How do I test the minimum versions in pyproject.toml? Install the package with uv's --resolution lowest-direct option in a dedicated session, then run the tests.

Why turn warnings into errors in tests? Libraries announce breaking changes with deprecation warnings; failing on them reveals upcoming breakages while there is still time to fix them.

More on logging, testing & packaging

More on logging, testing & packaging →