[{"data":1,"prerenderedAt":1779},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox":1745,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox":1753,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox":1758,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox":1759},{"id":4,"title":5,"body":6,"dateModified":1713,"datePublished":1713,"description":1714,"extension":1715,"faq":1716,"keyword":1725,"kind":1726,"meta":1727,"navigation":149,"path":1736,"seo":1737,"slug":1739,"stem":1740,"topics":1741,"type":1743,"__hash__":1744},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox\u002Findex.md","Test Excel Code Against Several Library Versions with nox",{"type":7,"value":8,"toc":1701},"minimark",[9,24,29,58,70,74,81,217,333,350,354,361,604,622,734,738,744,1101,1115,1172,1187,1191,1211,1231,1237,1283,1294,1425,1552,1556,1586,1590,1628,1632,1638,1642,1648,1654,1660,1666,1670,1697],[10,11,12,13,17,18,23],"p",{},"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 ",[14,15,16],"code",{},"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 ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install nox        # uv must also be installed: https:\u002F\u002Fdocs.astral.sh\u002Fuv\u002F\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," nox",[39,55,57],{"class":56},"s-wDw","        # uv must also be installed: https:\u002F\u002Fdocs.astral.sh\u002Fuv\u002F\n",[10,59,60,61,64,65,69],{},"The project is a small installable package, ",[14,62,63],{},"excelreport",", with one function that reads product codes from a workbook and trims the text columns, and one pytest test for it; ",[19,66,68],{"href":67},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fset-up-an-excel-automation-project-with-uv\u002F","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.",[25,71,73],{"id":72},"step-1-the-project-and-its-version-claims","Step 1 — The project and its version claims",[10,75,76,77,80],{},"The package declares the versions it supports in ",[14,78,79],{},"pyproject.toml",". Those claims are exactly what the test matrix will check:",[30,82,86],{"className":83,"code":84,"language":85,"meta":35,"style":35},"language-toml shiki shiki-themes github-light github-dark-high-contrast","[project]\nname = \"excelreport\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\"pandas>=2.2\", \"openpyxl>=3.1\"]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.pytest.ini_options]\nfilterwarnings = [\"error\"]\n","toml",[14,87,88,100,109,118,127,144,151,161,172,181,186,206],{"__ignoreMap":35},[39,89,90,94,97],{"class":41,"line":42},[39,91,93],{"class":92},"skGVy","[",[39,95,96],{"class":45},"project",[39,98,99],{"class":92},"]\n",[39,101,103,106],{"class":41,"line":102},2,[39,104,105],{"class":92},"name = ",[39,107,108],{"class":49},"\"excelreport\"\n",[39,110,112,115],{"class":41,"line":111},3,[39,113,114],{"class":92},"version = ",[39,116,117],{"class":49},"\"0.1.0\"\n",[39,119,121,124],{"class":41,"line":120},4,[39,122,123],{"class":92},"requires-python = ",[39,125,126],{"class":49},"\">=3.11\"\n",[39,128,130,133,136,139,142],{"class":41,"line":129},5,[39,131,132],{"class":92},"dependencies = [",[39,134,135],{"class":49},"\"pandas>=2.2\"",[39,137,138],{"class":92},", ",[39,140,141],{"class":49},"\"openpyxl>=3.1\"",[39,143,99],{"class":92},[39,145,147],{"class":41,"line":146},6,[39,148,150],{"emptyLinePlaceholder":149},true,"\n",[39,152,154,156,159],{"class":41,"line":153},7,[39,155,93],{"class":92},[39,157,158],{"class":45},"build-system",[39,160,99],{"class":92},[39,162,164,167,170],{"class":41,"line":163},8,[39,165,166],{"class":92},"requires = [",[39,168,169],{"class":49},"\"hatchling\"",[39,171,99],{"class":92},[39,173,175,178],{"class":41,"line":174},9,[39,176,177],{"class":92},"build-backend = ",[39,179,180],{"class":49},"\"hatchling.build\"\n",[39,182,184],{"class":41,"line":183},10,[39,185,150],{"emptyLinePlaceholder":149},[39,187,189,191,194,196,199,201,204],{"class":41,"line":188},11,[39,190,93],{"class":92},[39,192,193],{"class":45},"tool",[39,195,23],{"class":92},[39,197,198],{"class":45},"pytest",[39,200,23],{"class":92},[39,202,203],{"class":45},"ini_options",[39,205,99],{"class":92},[39,207,209,212,215],{"class":41,"line":208},12,[39,210,211],{"class":92},"filterwarnings = [",[39,213,214],{"class":49},"\"error\"",[39,216,99],{"class":92},[30,218,222],{"className":219,"code":220,"language":221,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndef load_codes(path):\n    df = pd.read_excel(path, dtype={\"code\": str})\n    text = df.select_dtypes(include=\"object\").columns\n    df[text] = df[text].apply(lambda s: s.str.strip())\n    return df\n","python",[14,223,224,239,243,255,288,309,325],{"__ignoreMap":35},[39,225,226,230,233,236],{"class":41,"line":42},[39,227,229],{"class":228},"s-kum","import",[39,231,232],{"class":92}," pandas ",[39,234,235],{"class":228},"as",[39,237,238],{"class":92}," pd\n",[39,240,241],{"class":41,"line":102},[39,242,150],{"emptyLinePlaceholder":149},[39,244,245,248,252],{"class":41,"line":111},[39,246,247],{"class":228},"def",[39,249,251],{"class":250},"s_Opv"," load_codes",[39,253,254],{"class":92},"(path):\n",[39,256,257,260,263,266,270,272,275,278,281,285],{"class":41,"line":120},[39,258,259],{"class":92},"    df ",[39,261,262],{"class":228},"=",[39,264,265],{"class":92}," pd.read_excel(path, ",[39,267,269],{"class":268},"sa561","dtype",[39,271,262],{"class":228},[39,273,274],{"class":92},"{",[39,276,277],{"class":49},"\"code\"",[39,279,280],{"class":92},": ",[39,282,284],{"class":283},"sP0c6","str",[39,286,287],{"class":92},"})\n",[39,289,290,293,295,298,301,303,306],{"class":41,"line":129},[39,291,292],{"class":92},"    text ",[39,294,262],{"class":228},[39,296,297],{"class":92}," df.select_dtypes(",[39,299,300],{"class":268},"include",[39,302,262],{"class":228},[39,304,305],{"class":49},"\"object\"",[39,307,308],{"class":92},").columns\n",[39,310,311,314,316,319,322],{"class":41,"line":146},[39,312,313],{"class":92},"    df[text] ",[39,315,262],{"class":228},[39,317,318],{"class":92}," df[text].apply(",[39,320,321],{"class":228},"lambda",[39,323,324],{"class":92}," s: s.str.strip())\n",[39,326,327,330],{"class":41,"line":153},[39,328,329],{"class":228},"    return",[39,331,332],{"class":92}," df\n",[10,334,335,338,339,138,342,345,346,349],{},[14,336,337],{},"filterwarnings = [\"error\"]"," makes pytest treat every warning as a test failure. That is deliberate: libraries announce breaking changes with ",[14,340,341],{},"DeprecationWarning",[14,343,344],{},"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 ",[14,347,348],{},"ignore::"," entry rather than relaxing the rule.",[25,351,353],{"id":352},"step-2-a-nox-session-matrix","Step 2 — A nox session matrix",[10,355,356,357,360],{},"The ",[14,358,359],{},"noxfile.py"," defines sessions. One decorator multiplies the session over Python versions, another over pandas versions:",[30,362,364],{"className":219,"code":363,"language":221,"meta":35,"style":35},"import nox\n\nnox.options.default_venv_backend = \"uv\"\nnox.options.sessions = [\"tests\"]\n\n@nox.session(python=[\"3.11\", \"3.13\"])\n@nox.parametrize(\"pandas\", [\"2.2.3\", \"3.0.6\"])\ndef tests(session, pandas):\n    session.install(\".\", \"pytest\", f\"pandas=={pandas}\")\n    session.run(\"pytest\", \"-q\")\n\n@nox.session(python=\"3.11\")\ndef lowest(session):\n    session.install(\"--resolution\", \"lowest-direct\", \".\")\n    session.install(\"pytest\")\n    session.run(\"python\", \"-c\", \"import pandas, openpyxl; print('pandas', pandas.__version__, 'openpyxl', openpyxl.__version__)\")\n    session.run(\"pytest\", \"-q\")\n",[14,365,366,373,377,387,402,406,431,454,464,500,514,518,532,543,562,571,591],{"__ignoreMap":35},[39,367,368,370],{"class":41,"line":42},[39,369,229],{"class":228},[39,371,372],{"class":92}," nox\n",[39,374,375],{"class":41,"line":102},[39,376,150],{"emptyLinePlaceholder":149},[39,378,379,382,384],{"class":41,"line":111},[39,380,381],{"class":92},"nox.options.default_venv_backend ",[39,383,262],{"class":228},[39,385,386],{"class":49}," \"uv\"\n",[39,388,389,392,394,397,400],{"class":41,"line":120},[39,390,391],{"class":92},"nox.options.sessions ",[39,393,262],{"class":228},[39,395,396],{"class":92}," [",[39,398,399],{"class":49},"\"tests\"",[39,401,99],{"class":92},[39,403,404],{"class":41,"line":129},[39,405,150],{"emptyLinePlaceholder":149},[39,407,408,411,414,416,418,420,423,425,428],{"class":41,"line":146},[39,409,410],{"class":250},"@nox.session",[39,412,413],{"class":92},"(",[39,415,221],{"class":268},[39,417,262],{"class":228},[39,419,93],{"class":92},[39,421,422],{"class":49},"\"3.11\"",[39,424,138],{"class":92},[39,426,427],{"class":49},"\"3.13\"",[39,429,430],{"class":92},"])\n",[39,432,433,436,438,441,444,447,449,452],{"class":41,"line":153},[39,434,435],{"class":250},"@nox.parametrize",[39,437,413],{"class":92},[39,439,440],{"class":49},"\"pandas\"",[39,442,443],{"class":92},", [",[39,445,446],{"class":49},"\"2.2.3\"",[39,448,138],{"class":92},[39,450,451],{"class":49},"\"3.0.6\"",[39,453,430],{"class":92},[39,455,456,458,461],{"class":41,"line":163},[39,457,247],{"class":228},[39,459,460],{"class":250}," tests",[39,462,463],{"class":92},"(session, pandas):\n",[39,465,466,469,472,474,477,479,482,485,488,491,494,497],{"class":41,"line":174},[39,467,468],{"class":92},"    session.install(",[39,470,471],{"class":49},"\".\"",[39,473,138],{"class":92},[39,475,476],{"class":49},"\"pytest\"",[39,478,138],{"class":92},[39,480,481],{"class":228},"f",[39,483,484],{"class":49},"\"pandas==",[39,486,274],{"class":487},"sSjpA",[39,489,490],{"class":92},"pandas",[39,492,493],{"class":487},"}",[39,495,496],{"class":49},"\"",[39,498,499],{"class":92},")\n",[39,501,502,505,507,509,512],{"class":41,"line":183},[39,503,504],{"class":92},"    session.run(",[39,506,476],{"class":49},[39,508,138],{"class":92},[39,510,511],{"class":49},"\"-q\"",[39,513,499],{"class":92},[39,515,516],{"class":41,"line":188},[39,517,150],{"emptyLinePlaceholder":149},[39,519,520,522,524,526,528,530],{"class":41,"line":208},[39,521,410],{"class":250},[39,523,413],{"class":92},[39,525,221],{"class":268},[39,527,262],{"class":228},[39,529,422],{"class":49},[39,531,499],{"class":92},[39,533,535,537,540],{"class":41,"line":534},13,[39,536,247],{"class":228},[39,538,539],{"class":250}," lowest",[39,541,542],{"class":92},"(session):\n",[39,544,546,548,551,553,556,558,560],{"class":41,"line":545},14,[39,547,468],{"class":92},[39,549,550],{"class":49},"\"--resolution\"",[39,552,138],{"class":92},[39,554,555],{"class":49},"\"lowest-direct\"",[39,557,138],{"class":92},[39,559,471],{"class":49},[39,561,499],{"class":92},[39,563,565,567,569],{"class":41,"line":564},15,[39,566,468],{"class":92},[39,568,476],{"class":49},[39,570,499],{"class":92},[39,572,574,576,579,581,584,586,589],{"class":41,"line":573},16,[39,575,504],{"class":92},[39,577,578],{"class":49},"\"python\"",[39,580,138],{"class":92},[39,582,583],{"class":49},"\"-c\"",[39,585,138],{"class":92},[39,587,588],{"class":49},"\"import pandas, openpyxl; print('pandas', pandas.__version__, 'openpyxl', openpyxl.__version__)\"",[39,590,499],{"class":92},[39,592,594,596,598,600,602],{"class":41,"line":593},17,[39,595,504],{"class":92},[39,597,476],{"class":49},[39,599,138],{"class":92},[39,601,511],{"class":49},[39,603,499],{"class":92},[10,605,606,609,610,613,614,617,618,621],{},[14,607,608],{},"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 ",[14,611,612],{},"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. ",[14,615,616],{},"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 ",[14,619,620],{},"lowest"," session is explained in Step 4.",[623,624,632,633,632,637,632,641,632,649,632,657,632,664,632,671,632,674,632,678,632,683,632,689,632,691,632,696,632,698,632,701,632,704,632,708,632,710,632,713,632,715,632,717,632,720,632,724,632,726,632,730,632,732],"svg",{"viewBox":625,"role":626,"ariaLabelledBy":627,"xmlns":630,"style":631},"4 0 752 174","img",[628,629],"nx-matrix-t","nx-matrix-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[634,635,636],"title",{"id":628},"The test matrix nox runs",[638,639,640],"desc",{"id":629},"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.",[642,643],"rect",{"x":644,"y":645,"width":646,"height":647,"fill":648},"4","0","752","174","#ffffff",[642,650],{"x":651,"y":652,"width":653,"height":654,"rx":645,"fill":648,"stroke":655,"style":656},"20.0","16.0","107.9","40.0","var(--line,#cdd5e6)","stroke-width:1px",[642,658],{"x":659,"y":652,"width":660,"height":654,"rx":645,"fill":661,"stroke":662,"style":663},"127.9","306.0","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[665,666,670],"text",{"x":667,"y":668,"style":669},"280.9","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas 2.2.3",[642,672],{"x":673,"y":652,"width":660,"height":654,"rx":645,"fill":661,"stroke":662,"style":663},"434.0",[665,675,677],{"x":676,"y":668,"style":669},"587.0","pandas 3.0.6",[642,679],{"x":651,"y":680,"width":653,"height":681,"rx":645,"fill":682,"stroke":655,"style":656},"56.0","34.0","#e7ebef",[665,684,688],{"x":685,"y":686,"style":687},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Python 3.11",[642,690],{"x":659,"y":680,"width":660,"height":681,"rx":645,"fill":648,"stroke":655,"style":656},[665,692,695],{"x":667,"y":693,"style":694},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","pass",[642,697],{"x":673,"y":680,"width":660,"height":681,"rx":645,"fill":648,"stroke":655,"style":656},[665,699,700],{"x":676,"y":693,"style":694},"fail — warning",[642,702],{"x":651,"y":703,"width":653,"height":681,"rx":645,"fill":682,"stroke":655,"style":656},"90.0",[665,705,707],{"x":685,"y":706,"style":687},"112.0","Python 3.13",[642,709],{"x":659,"y":703,"width":660,"height":681,"rx":645,"fill":648,"stroke":655,"style":656},[665,711,695],{"x":667,"y":712,"style":694},"111.0",[642,714],{"x":673,"y":703,"width":660,"height":681,"rx":645,"fill":648,"stroke":655,"style":656},[665,716,700],{"x":676,"y":712,"style":694},[642,718],{"x":651,"y":719,"width":653,"height":681,"rx":645,"fill":682,"stroke":655,"style":656},"124.0",[665,721,723],{"x":685,"y":722,"style":687},"146.0","lowest (3.11)",[642,725],{"x":659,"y":719,"width":660,"height":681,"rx":645,"fill":648,"stroke":655,"style":656},[665,727,729],{"x":667,"y":728,"style":694},"145.0","minimums from pyproject",[642,731],{"x":673,"y":719,"width":660,"height":681,"rx":645,"fill":648,"stroke":655,"style":656},[665,733],{"x":676,"y":728,"style":694},[25,735,737],{"id":736},"step-3-run-it-and-read-the-failures","Step 3 — Run it and read the failures",[10,739,740,741,743],{},"Run ",[14,742,608],{}," in the project folder. Here the same command is run from Python so the summary can be shown:",[30,745,747],{"className":219,"code":746,"language":221,"meta":35,"style":35},"import subprocess, sys\n\ndef nox(*args):\n    run = subprocess.run([sys.executable, \"-m\", \"nox\", \"--no-color\", *args], capture_output=True, text=True)\n    lines = (run.stdout + run.stderr).splitlines()\n    errors = {\" \".join(l[1:].split()) + \" \" + \" \".join(lines[i + 1][1:].split()) for i, l in enumerate(lines[:-1])\n              if l.startswith(\"E \") and l.rstrip().endswith((\"Warning:\", \"Warning: \")) or l.startswith(\"E \") and \"Warning:\" in l}\n    for e in sorted(errors):\n        print(\"E\", e[:105] + \" …\")\n    summary = [l for l in lines if l.startswith(\"nox > *\")] or [l for l in lines if l.startswith(\"nox > Session\")]\n    print(\"\\n\".join([l for l in lines if l.startswith(\"pandas \")] + summary))\n\nnox()\n",[14,748,749,756,760,774,822,838,907,958,974,1000,1053,1092,1096],{"__ignoreMap":35},[39,750,751,753],{"class":41,"line":42},[39,752,229],{"class":228},[39,754,755],{"class":92}," subprocess, sys\n",[39,757,758],{"class":41,"line":102},[39,759,150],{"emptyLinePlaceholder":149},[39,761,762,764,766,768,771],{"class":41,"line":111},[39,763,247],{"class":228},[39,765,53],{"class":250},[39,767,413],{"class":92},[39,769,770],{"class":228},"*",[39,772,773],{"class":92},"args):\n",[39,775,776,779,781,784,787,789,792,794,797,799,801,804,807,809,812,814,816,818,820],{"class":41,"line":120},[39,777,778],{"class":92},"    run ",[39,780,262],{"class":228},[39,782,783],{"class":92}," subprocess.run([sys.executable, ",[39,785,786],{"class":49},"\"-m\"",[39,788,138],{"class":92},[39,790,791],{"class":49},"\"nox\"",[39,793,138],{"class":92},[39,795,796],{"class":49},"\"--no-color\"",[39,798,138],{"class":92},[39,800,770],{"class":228},[39,802,803],{"class":92},"args], ",[39,805,806],{"class":268},"capture_output",[39,808,262],{"class":228},[39,810,811],{"class":283},"True",[39,813,138],{"class":92},[39,815,665],{"class":268},[39,817,262],{"class":228},[39,819,811],{"class":283},[39,821,499],{"class":92},[39,823,824,827,829,832,835],{"class":41,"line":129},[39,825,826],{"class":92},"    lines ",[39,828,262],{"class":228},[39,830,831],{"class":92}," (run.stdout ",[39,833,834],{"class":228},"+",[39,836,837],{"class":92}," run.stderr).splitlines()\n",[39,839,840,843,845,848,851,854,857,860,862,865,868,870,873,875,878,881,883,885,888,891,894,897,900,903,905],{"class":41,"line":146},[39,841,842],{"class":92},"    errors ",[39,844,262],{"class":228},[39,846,847],{"class":92}," {",[39,849,850],{"class":49},"\" \"",[39,852,853],{"class":92},".join(l[",[39,855,856],{"class":283},"1",[39,858,859],{"class":92},":].split()) ",[39,861,834],{"class":228},[39,863,864],{"class":49}," \" \"",[39,866,867],{"class":228}," +",[39,869,864],{"class":49},[39,871,872],{"class":92},".join(lines[i ",[39,874,834],{"class":228},[39,876,877],{"class":283}," 1",[39,879,880],{"class":92},"][",[39,882,856],{"class":283},[39,884,859],{"class":92},[39,886,887],{"class":228},"for",[39,889,890],{"class":92}," i, l ",[39,892,893],{"class":228},"in",[39,895,896],{"class":283}," enumerate",[39,898,899],{"class":92},"(lines[:",[39,901,902],{"class":228},"-",[39,904,856],{"class":283},[39,906,430],{"class":92},[39,908,909,912,915,918,921,924,927,930,932,935,938,941,943,945,947,949,952,955],{"class":41,"line":153},[39,910,911],{"class":228},"              if",[39,913,914],{"class":92}," l.startswith(",[39,916,917],{"class":49},"\"E \"",[39,919,920],{"class":92},") ",[39,922,923],{"class":228},"and",[39,925,926],{"class":92}," l.rstrip().endswith((",[39,928,929],{"class":49},"\"Warning:\"",[39,931,138],{"class":92},[39,933,934],{"class":49},"\"Warning: \"",[39,936,937],{"class":92},")) ",[39,939,940],{"class":228},"or",[39,942,914],{"class":92},[39,944,917],{"class":49},[39,946,920],{"class":92},[39,948,923],{"class":228},[39,950,951],{"class":49}," \"Warning:\"",[39,953,954],{"class":228}," in",[39,956,957],{"class":92}," l}\n",[39,959,960,963,966,968,971],{"class":41,"line":163},[39,961,962],{"class":228},"    for",[39,964,965],{"class":92}," e ",[39,967,893],{"class":228},[39,969,970],{"class":283}," sorted",[39,972,973],{"class":92},"(errors):\n",[39,975,976,979,981,984,987,990,993,995,998],{"class":41,"line":174},[39,977,978],{"class":283},"        print",[39,980,413],{"class":92},[39,982,983],{"class":49},"\"E\"",[39,985,986],{"class":92},", e[:",[39,988,989],{"class":283},"105",[39,991,992],{"class":92},"] ",[39,994,834],{"class":228},[39,996,997],{"class":49}," \" …\"",[39,999,499],{"class":92},[39,1001,1002,1005,1007,1010,1012,1015,1017,1020,1023,1025,1028,1031,1033,1035,1037,1039,1041,1043,1045,1047,1050],{"class":41,"line":183},[39,1003,1004],{"class":92},"    summary ",[39,1006,262],{"class":228},[39,1008,1009],{"class":92}," [l ",[39,1011,887],{"class":228},[39,1013,1014],{"class":92}," l ",[39,1016,893],{"class":228},[39,1018,1019],{"class":92}," lines ",[39,1021,1022],{"class":228},"if",[39,1024,914],{"class":92},[39,1026,1027],{"class":49},"\"nox > *\"",[39,1029,1030],{"class":92},")] ",[39,1032,940],{"class":228},[39,1034,1009],{"class":92},[39,1036,887],{"class":228},[39,1038,1014],{"class":92},[39,1040,893],{"class":228},[39,1042,1019],{"class":92},[39,1044,1022],{"class":228},[39,1046,914],{"class":92},[39,1048,1049],{"class":49},"\"nox > Session\"",[39,1051,1052],{"class":92},")]\n",[39,1054,1055,1058,1060,1062,1065,1067,1070,1072,1074,1076,1078,1080,1082,1085,1087,1089],{"class":41,"line":188},[39,1056,1057],{"class":283},"    print",[39,1059,413],{"class":92},[39,1061,496],{"class":49},[39,1063,1064],{"class":487},"\\n",[39,1066,496],{"class":49},[39,1068,1069],{"class":92},".join([l ",[39,1071,887],{"class":228},[39,1073,1014],{"class":92},[39,1075,893],{"class":228},[39,1077,1019],{"class":92},[39,1079,1022],{"class":228},[39,1081,914],{"class":92},[39,1083,1084],{"class":49},"\"pandas \"",[39,1086,1030],{"class":92},[39,1088,834],{"class":228},[39,1090,1091],{"class":92}," summary))\n",[39,1093,1094],{"class":41,"line":208},[39,1095,150],{"emptyLinePlaceholder":149},[39,1097,1098],{"class":41,"line":534},[39,1099,1100],{"class":92},"nox()\n",[10,1102,1103,1104,1107,1108,1110,1111,1114],{},"Both pandas 2.2.3 sessions pass and both pandas 3 sessions fail with a ",[14,1105,1106],{},"Pandas4Warning",": in pandas 3, text columns read from Excel have the new ",[14,1109,284],{}," dtype, and ",[14,1112,1113],{},"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:",[30,1116,1118],{"className":219,"code":1117,"language":221,"meta":35,"style":35},"from pathlib import Path\n\nsrc = Path(\"src\u002Fexcelreport\u002F__init__.py\")\nsrc.write_text(src.read_text().replace('include=\"object\"', 'include=[\"object\", \"string\"]'))\nnox()\n",[14,1119,1120,1133,1137,1152,1168],{"__ignoreMap":35},[39,1121,1122,1125,1128,1130],{"class":41,"line":42},[39,1123,1124],{"class":228},"from",[39,1126,1127],{"class":92}," pathlib ",[39,1129,229],{"class":228},[39,1131,1132],{"class":92}," Path\n",[39,1134,1135],{"class":41,"line":102},[39,1136,150],{"emptyLinePlaceholder":149},[39,1138,1139,1142,1144,1147,1150],{"class":41,"line":111},[39,1140,1141],{"class":92},"src ",[39,1143,262],{"class":228},[39,1145,1146],{"class":92}," Path(",[39,1148,1149],{"class":49},"\"src\u002Fexcelreport\u002F__init__.py\"",[39,1151,499],{"class":92},[39,1153,1154,1157,1160,1162,1165],{"class":41,"line":120},[39,1155,1156],{"class":92},"src.write_text(src.read_text().replace(",[39,1158,1159],{"class":49},"'include=\"object\"'",[39,1161,138],{"class":92},[39,1163,1164],{"class":49},"'include=[\"object\", \"string\"]'",[39,1166,1167],{"class":92},"))\n",[39,1169,1170],{"class":41,"line":129},[39,1171,1100],{"class":92},[10,1173,1174,1175,1178,1179,1181,1182,1186],{},"All four sessions now pass: ",[14,1176,1177],{},"\"string\""," selects the pandas 3 text dtype and the older string dtype, and ",[14,1180,305],{}," 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; ",[19,1183,1185],{"href":1184},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-pandas-futurewarnings-in-excel-pipelines\u002F","Fix pandas FutureWarnings in Excel Pipelines"," works through the common ones.",[25,1188,1190],{"id":1189},"step-4-check-the-minimum-versions-you-claim","Step 4 — Check the minimum versions you claim",[10,1192,1193,1196,1197,1199,1200,1202,1203,1206,1207,1210],{},[14,1194,1195],{},"pandas>=2.2"," in ",[14,1198,79],{}," is a promise: the package works with pandas 2.2.0. Nothing so far tests that, because every session installed a recent release. The ",[14,1201,620],{}," session installs the ",[1204,1205,620],"em",{}," version of each direct dependency that satisfies the declared ranges — uv's ",[14,1208,1209],{},"--resolution lowest-direct"," — and runs the tests:",[30,1212,1214],{"className":219,"code":1213,"language":221,"meta":35,"style":35},"nox(\"-s\", \"lowest\")\n",[14,1215,1216],{"__ignoreMap":35},[39,1217,1218,1221,1224,1226,1229],{"class":41,"line":42},[39,1219,1220],{"class":92},"nox(",[39,1222,1223],{"class":49},"\"-s\"",[39,1225,138],{"class":92},[39,1227,1228],{"class":49},"\"lowest\"",[39,1230,499],{"class":92},[10,1232,1233,1234,1236],{},"It fails — not on the package's code but on importing pandas 2.2.0, which emits a ",[14,1235,341],{}," 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:",[30,1238,1240],{"className":219,"code":1239,"language":221,"meta":35,"style":35},"toml = Path(\"pyproject.toml\")\ntoml.write_text(toml.read_text().replace('\"pandas>=2.2\"', '\"pandas>=2.2.2\"'))\nnox(\"-s\", \"lowest\")\n",[14,1241,1242,1256,1271],{"__ignoreMap":35},[39,1243,1244,1247,1249,1251,1254],{"class":41,"line":42},[39,1245,1246],{"class":92},"toml ",[39,1248,262],{"class":228},[39,1250,1146],{"class":92},[39,1252,1253],{"class":49},"\"pyproject.toml\"",[39,1255,499],{"class":92},[39,1257,1258,1261,1264,1266,1269],{"class":41,"line":102},[39,1259,1260],{"class":92},"toml.write_text(toml.read_text().replace(",[39,1262,1263],{"class":49},"'\"pandas>=2.2\"'",[39,1265,138],{"class":92},[39,1267,1268],{"class":49},"'\"pandas>=2.2.2\"'",[39,1270,1167],{"class":92},[39,1272,1273,1275,1277,1279,1281],{"class":41,"line":111},[39,1274,1220],{"class":92},[39,1276,1223],{"class":49},[39,1278,138],{"class":92},[39,1280,1228],{"class":49},[39,1282,499],{"class":92},[10,1284,1285,1286,1289,1290,1293],{},"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 ",[1204,1287,1288],{},"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 ",[14,1291,1292],{},"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.",[623,1295,632,1300,632,1303,632,1306,632,1309,632,1315,632,1326,632,1332,632,1335,632,1340,632,1345,632,1348,632,1355,632,1360,632,1363,632,1367,632,1370,632,1375,632,1382,632,1388,632,1392,632,1396,632,1400,632,1403,632,1407,632,1414,632,1419,632,1422],{"viewBox":1296,"role":626,"ariaLabelledBy":1297,"xmlns":630,"style":631},"4 0 752 142",[1298,1299],"nx-flow-t","nx-flow-d",[634,1301,1302],{"id":1298},"What the lowest-versions session found",[638,1304,1305],{"id":1299},"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.",[642,1307],{"x":644,"y":645,"width":646,"height":1308,"fill":648},"142",[642,1310],{"x":651,"y":652,"width":1311,"height":1312,"rx":1313,"fill":682,"stroke":655,"style":1314},"153.0","110.5","12","stroke-width:2px",[1316,1317,1319],"g",{"transform":1318},"translate(84.5,37.8) scale(1.000)",[1320,1321],"path",{"d":1322,"fill":1323,"stroke":1324,"style":1325},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[665,1327,1331],{"x":1328,"y":1329,"style":1330},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Claim",[665,1333,1195],{"x":1328,"y":1334,"style":694},"103.8",[41,1336],{"x1":1337,"y1":1338,"x2":1339,"y2":1338,"stroke":662,"style":1314},"178.0","71.2","200.0",[1341,1342],"polygon",{"points":1343,"fill":1344},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[642,1346],{"x":1347,"y":652,"width":1311,"height":1312,"rx":1313,"fill":661,"stroke":662,"style":1314},"209.0",[1316,1349,1351],{"transform":1350},"translate(273.5,37.8) scale(1.000)",[1320,1352],{"d":1353,"fill":1323,"stroke":1354,"style":1325},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","var(--brand-strong,#4338ca)",[665,1356,1359],{"x":1357,"y":1329,"style":1358},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","lowest-direct",[665,1361,1362],{"x":1357,"y":1334,"style":694},"installs pandas 2.2.0",[41,1364],{"x1":1365,"y1":1338,"x2":1366,"y2":1338,"stroke":662,"style":1314},"367.0","389.0",[1341,1368],{"points":1369,"fill":1344},"393.0,71.2 384.0,76.2 384.0,66.2",[642,1371],{"x":1372,"y":652,"width":1311,"height":1312,"rx":1313,"fill":1373,"stroke":1374,"style":1314},"398.0","#fdefd8","var(--gold,#b4740a)",[1316,1376,1378],{"transform":1377},"translate(462.5,29.0) scale(1.000)",[1320,1379],{"d":1380,"fill":1323,"stroke":1381,"style":1325},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[665,1383,1387],{"x":1384,"y":1385,"style":1386},"474.5","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Import warning",[665,1389,1391],{"x":1384,"y":1390,"style":694},"95.0","pyarrow notice, test",[665,1393,1395],{"x":1384,"y":1394,"style":694},"112.5","fails",[41,1397],{"x1":1398,"y1":1338,"x2":1399,"y2":1338,"stroke":662,"style":1314},"556.0","578.0",[1341,1401],{"points":1402,"fill":1344},"582.0,71.2 573.0,76.2 573.0,66.2",[642,1404],{"x":676,"y":652,"width":1311,"height":1312,"rx":1313,"fill":1405,"stroke":1406,"style":1314},"#d9f4f1","var(--teal,#0f9488)",[1316,1408,1410],{"transform":1409},"translate(651.5,29.0) scale(1.000)",[1320,1411],{"d":1412,"fill":1323,"stroke":1413,"style":1325},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[665,1415,1418],{"x":1416,"y":1385,"style":1417},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Fix the claim",[665,1420,1421],{"x":1416,"y":1390,"style":694},"pandas>=2.2.2,",[665,1423,1424],{"x":1416,"y":1394,"style":694},"passes",[623,1426,632,1431,632,1434,632,1437,632,1440,632,1444,632,1450,632,1454,632,1458,632,1462,632,1465,632,1468,632,1475,632,1480,632,1485,632,1491,632,1495,632,1499,632,1501,632,1504,632,1508,632,1511,632,1514,632,1519,632,1522,632,1525,632,1527,632,1530,632,1534,632,1537,632,1540,632,1546,632,1549],{"viewBox":1427,"role":626,"ariaLabelledBy":1428,"xmlns":630,"style":631},"4 0 752 272",[1429,1430],"nx-which-t","nx-which-d",[634,1432,1433],{"id":1429},"Which versions belong in the matrix",[638,1435,1436],{"id":1430},"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.",[642,1438],{"x":644,"y":645,"width":646,"height":1439,"fill":648},"272",[642,1441],{"x":1442,"y":652,"width":1443,"height":1385,"rx":1313,"fill":661,"stroke":662,"style":1314},"240.0","280.0",[1316,1445,1447],{"transform":1446},"translate(368.0,29.0) scale(1.000)",[1320,1448],{"d":1449,"fill":1323,"stroke":1354,"style":1325},"M9 3h6 M10 3v6l-5 9a2 2 0 0 0 1.8 3h10.4a2 2 0 0 0 1.8-3l-5-9V3",[665,1451,1453],{"x":1452,"y":1385,"style":1358},"380.0","Which versions to test?",[41,1455],{"x1":1452,"y1":1456,"x2":1452,"y2":1457,"stroke":662,"style":1314},"91.5","111.5",[41,1459],{"x1":1460,"y1":1457,"x2":1461,"y2":1457,"stroke":662,"style":1314},"134.7","625.3",[41,1463],{"x1":1460,"y1":1457,"x2":1460,"y2":1464,"stroke":662,"style":1314},"157.5",[1341,1466],{"points":1467,"fill":1344},"134.7,161.5 129.7,152.5 139.7,152.5",[642,1469],{"x":1470,"y":1471,"width":1472,"height":1473,"rx":1474,"fill":648,"stroke":655,"style":663},"104.7","121.5","60.0","21.0","10",[665,1476,1479],{"x":1460,"y":1477,"style":1478},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Python",[642,1481],{"x":651,"y":1482,"width":1483,"height":1484,"rx":1313,"fill":661,"stroke":662,"style":1314},"163.5","229.3","93.0",[1316,1486,1488],{"transform":1487},"translate(122.7,176.5) scale(1.000)",[1320,1489],{"d":1490,"fill":1323,"stroke":1354,"style":1325},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01",[665,1492,1494],{"x":1460,"y":1493,"style":1358},"223.0","oldest and newest",[665,1496,1498],{"x":1460,"y":1497,"style":694},"242.5","3.11, 3.13",[41,1500],{"x1":1452,"y1":1457,"x2":1452,"y2":1464,"stroke":662,"style":1314},[1341,1502],{"points":1503,"fill":1344},"380.0,161.5 375.0,152.5 385.0,152.5",[642,1505],{"x":1506,"y":1471,"width":1507,"height":1473,"rx":1474,"fill":648,"stroke":655,"style":663},"334.7","90.6",[665,1509,1510],{"x":1452,"y":1477,"style":1478},"key libraries",[642,1512],{"x":1513,"y":1482,"width":1483,"height":1484,"rx":1313,"fill":1405,"stroke":1406,"style":1314},"265.3",[1316,1515,1517],{"transform":1516},"translate(368.0,176.5) scale(1.000)",[1320,1518],{"d":1353,"fill":1323,"stroke":1413,"style":1325},[665,1520,1521],{"x":1452,"y":1493,"style":1417},"previous and current major",[665,1523,1524],{"x":1452,"y":1497,"style":694},"pandas 2 and 3",[41,1526],{"x1":1461,"y1":1457,"x2":1461,"y2":1464,"stroke":662,"style":1314},[1341,1528],{"points":1529,"fill":1344},"625.3,161.5 620.3,152.5 630.3,152.5",[642,1531],{"x":1532,"y":1471,"width":1533,"height":1473,"rx":1474,"fill":648,"stroke":655,"style":663},"576.2","98.3",[665,1535,1536],{"x":1461,"y":1477,"style":1478},"lower bounds",[642,1538],{"x":1539,"y":1482,"width":1483,"height":1484,"rx":1313,"fill":1373,"stroke":1374,"style":1314},"510.7",[1316,1541,1543],{"transform":1542},"translate(613.3,176.5) scale(1.000)",[1320,1544],{"d":1545,"fill":1323,"stroke":1381,"style":1325},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[665,1547,1548],{"x":1461,"y":1493,"style":1386},"lowest-direct session",[665,1550,1551],{"x":1461,"y":1497,"style":694},"from pyproject.toml",[25,1553,1555],{"id":1554},"running-it-in-ci","Running it in CI",[10,1557,1558,1559,1562,1563,1566,1567,1569,1570,1573,1574,1577,1578,1581,1582,23],{},"Locally, run the matrix before a release or after upgrading dependencies; ",[14,1560,1561],{},"nox -s tests"," runs just the matrix and ",[14,1564,1565],{},"nox -s \"tests-3.13(pandas='3.0.6')\""," a single combination. In continuous integration, install uv and nox and run ",[14,1568,608],{}," as the test step; nox's ",[14,1571,1572],{},"--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 ",[1204,1575,1576],{},"pre-release"," versions (",[14,1579,1580],{},"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 ",[19,1583,1585],{"href":1584},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory\u002F","Create Excel Test Fixtures in Memory",[25,1587,1589],{"id":1588},"common-pitfalls","Common pitfalls",[1591,1592,1593,1601,1607,1616,1622],"ul",{},[1594,1595,1596,1600],"li",{},[1597,1598,1599],"strong",{},"Testing only the versions on your machine."," Users and servers run others.",[1594,1602,1603,1606],{},[1597,1604,1605],{},"Warnings left as warnings."," Make them errors so deprecations fail a session.",[1594,1608,1609,1612,1613,1615],{},[1597,1610,1611],{},"Untested lower bounds."," Add a ",[14,1614,1209],{}," session.",[1594,1617,1618,1621],{},[1597,1619,1620],{},"Testing the source folder."," Install the package in each session.",[1594,1623,1624,1627],{},[1597,1625,1626],{},"A huge matrix."," Test the edges that matter, not every combination.",[25,1629,1631],{"id":1630},"conclusion","Conclusion",[10,1633,1634,1635,1637],{},"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 ",[14,1636,79],{}," are tested rather than assumed.",[25,1639,1641],{"id":1640},"frequently-asked-questions","Frequently asked questions",[10,1643,1644,1647],{},[1597,1645,1646],{},"How do I test my code with two versions of pandas?","\nWrite a nox session decorated with nox.parametrize over the pandas versions and install the version passed in; nox creates a separate environment for each.",[10,1649,1650,1653],{},[1597,1651,1652],{},"How do I make nox faster?","\nSet nox.options.default_venv_backend to uv, so environments are created and populated by uv instead of virtualenv and pip.",[10,1655,1656,1659],{},[1597,1657,1658],{},"How do I test the minimum versions in pyproject.toml?","\nInstall the package with uv's --resolution lowest-direct option in a dedicated session, then run the tests.",[10,1661,1662,1665],{},[1597,1663,1664],{},"Why turn warnings into errors in tests?","\nLibraries announce breaking changes with deprecation warnings; failing on them reveals upcoming breakages while there is still time to fix them.",[25,1667,1669],{"id":1668},"related","Related",[1591,1671,1672,1678,1685,1690],{},[1594,1673,1674,1675,1677],{},"Up one level: ",[19,1676,22],{"href":21}," — reliable, shippable code.",[1594,1679,1680,1684],{},[19,1681,1683],{"href":1682},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest"," — the tests the matrix runs.",[1594,1686,1687,1689],{},[19,1688,68],{"href":67}," — the project layout.",[1594,1691,1692,1696],{},[19,1693,1695],{"href":1694},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project\u002F","Lint and Type-Check an Excel Automation Project"," — static checks to add.",[1698,1699,1700],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":102,"depth":102,"links":1702},[1703,1704,1705,1706,1707,1708,1709,1710,1711,1712],{"id":27,"depth":102,"text":28},{"id":72,"depth":102,"text":73},{"id":352,"depth":102,"text":353},{"id":736,"depth":102,"text":737},{"id":1189,"depth":102,"text":1190},{"id":1554,"depth":102,"text":1555},{"id":1588,"depth":102,"text":1589},{"id":1630,"depth":102,"text":1631},{"id":1640,"depth":102,"text":1641},{"id":1668,"depth":102,"text":1669},"2026-10-02","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.","md",[1717,1719,1721,1723],{"q":1646,"a":1718},"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.",{"q":1652,"a":1720},"Set nox.options.default_venv_backend to uv, so environments are created and populated by uv instead of virtualenv and pip.",{"q":1658,"a":1722},"Install the package with uv's --resolution lowest-direct option in a dedicated session, then run the tests.",{"q":1664,"a":1724},"Libraries announce breaking changes with deprecation warnings; failing on them reveals upcoming breakages while there is still time to fix them.","test excel code against several library versions with nox",null,{"breadcrumb":1728},[1729,1732,1735],{"name":1730,"item":1731},"Home","\u002F",{"name":1733,"item":1734},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox",{"title":5,"description":1738},"A nox matrix for pandas and openpyxl code: parametrized sessions over Python and pandas versions, the uv backend, filterwarnings = error to catch deprecations, and a lowest-direct session to verify minimum versions.","test-excel-code-against-several-library-versions-with-nox","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox\u002Findex",[1742,490],"reliable-jobs","how-to","-qCCtJF8hZnySRJaiNV4iEi8QLjvx74rh022s4r_UIA",[1746,1750],{"title":1747,"path":1748,"stem":1749,"children":-1},"Test Excel Cleaning Code with Hypothesis","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-cleaning-code-with-hypothesis\u002Findex",{"title":1683,"path":1751,"stem":1752,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002Findex",[1754,1756],{"path":1755,"title":1733},"\u002Fautomating-reporting-workflows",{"path":1757,"title":22},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[1760,1764,1767,1770,1773,1776],{"path":1761,"title":1762,"kind":1763},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",{"path":1765,"title":1766,"kind":1743},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",{"path":1768,"title":1769,"kind":1743},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder","Ignore Excel Lock Files When Processing a Folder",{"path":1771,"title":1772,"kind":1743},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file","Log Python Excel Script Output to a File",{"path":1774,"title":1775,"kind":1743},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun","Make an Excel Report Job Safe to Rerun",{"path":1777,"title":1778,"kind":1743},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","Prevent Formula Injection in Excel Exports from Python",1790972142916]