Test Excel Code Against Several Library Versions with nox
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
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:
[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"]
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:
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.
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:
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:
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:
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:
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.
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-directsession. - 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.
Related
- Up one level: Testing and Packaging Excel Automation Scripts — reliable, shippable code.
- Test Excel Output with pytest — the tests the matrix runs.
- Set Up an Excel Automation Project with uv — the project layout.
- Lint and Type-Check an Excel Automation Project — static checks to add.
More on logging, testing & packaging
- GuideError Handling and Logging in Excel Automation
- How-toDetect a Scheduled Report That Did Not Run
- How-toIgnore Excel Lock Files When Processing a Folder
- How-toLog Python Excel Script Output to a File
- How-toMake an Excel Report Job Safe to Rerun
- How-toPrevent Formula Injection in Excel Exports from Python