[{"data":1,"prerenderedAt":2089},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox\u002F":3,"content-directory":1542},{"id":4,"title":5,"body":6,"date":1526,"description":1527,"difficulty":1528,"draft":1529,"extension":1530,"meta":1531,"navigation":126,"path":1532,"seo":1533,"stem":1534,"tags":1535,"updated":1526,"__hash__":1541},"content\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox\u002Findex.md","Supporting Multiple Python Versions with nox",{"type":7,"value":8,"toc":1504},"minimark",[9,37,42,67,71,78,82,86,860,865,868,880,908,933,950,963,967,1066,1069,1075,1079,1082,1136,1147,1151,1154,1303,1325,1329,1332,1381,1385,1406,1410,1413,1417,1421,1431,1438,1444,1448,1451,1455,1465,1469,1500],[10,11,12,13,17,18,21,22,25,26,30,31,36],"p",{},"Your CLI supports Python 3.10 through 3.14, but you develop on one of them, and your ",[14,15,16],"code",{},".venv"," has accumulated packages from months of experiments. Every so often CI reports a failure on 3.10 — a ",[14,19,20],{},"match"," statement edge case, a stdlib function that does not exist yet, a dependency that dropped support — and you push speculative fixes until it goes green. nox moves that matrix onto your machine: a ",[14,23,24],{},"noxfile.py"," in plain Python defines sessions such as \"tests\" or \"lint\", each runs in a ",[27,28,29],"strong",{},"fresh virtual environment"," for each Python version you list, and the same file drives CI. With uv as the backend, creating those environments takes a second or two. This guide builds a noxfile for a CLI with locked test sessions across versions, a lowest-dependency session, lint, and a smoke test of the built wheel. It belongs to the ",[32,33,35],"a",{"href":34},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002F","virtual environments and isolation topic",".",[38,39,41],"h2",{"id":40},"prerequisites","Prerequisites",[43,44,45,57,64],"ul",{},[46,47,48,49,52,53,56],"li",{},"A CLI project managed with uv, with ",[14,50,51],{},"uv.lock"," committed and a ",[14,54,55],{},"dev"," dependency group containing pytest.",[46,58,59,60,63],{},"nox, run on demand with ",[14,61,62],{},"uvx nox"," (no installation needed), version 2024.3 or newer for the uv backend.",[46,65,66],{},"uv installs any Python versions nox asks for, so you do not need them installed beforehand.",[38,68,70],{"id":69},"what-nox-adds","What nox adds",[10,72,73,74,77],{},"A virtual environment isolates one project from another; nox isolates ",[27,75,76],{},"one run"," from another. Each session gets a newly created environment containing exactly what the session installs, so results never depend on something left over from last week — the same property CI has, available locally.",[79,80],"inline-diagram",{"name":81},"nox-flow",[38,83,85],{"id":84},"the-recipe","The recipe",[87,88,93],"pre",{"className":89,"code":90,"language":91,"meta":92,"style":92},"language-python shiki shiki-themes github-light github-dark","# noxfile.py\nfrom pathlib import Path\n\nimport nox\n\nnox.options.default_venv_backend = \"uv\"\nnox.options.sessions = [\"tests\", \"lint\"]        # what plain `nox` runs\n\nPYTHONS = [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n\ndef sync(session: nox.Session, *groups: str) -> None:\n    \"\"\"Install the project and groups from uv.lock into this session's environment.\"\"\"\n    session.run_install(\n        \"uv\", \"sync\", \"--locked\",\n        *(f\"--group={g}\" for g in groups),\n        f\"--python={session.virtualenv.location}\",\n        env={\"UV_PROJECT_ENVIRONMENT\": session.virtualenv.location},\n    )\n\n\n@nox.session(python=PYTHONS)\ndef tests(session: nox.Session) -> None:\n    \"\"\"Run the test suite with locked dependencies.\"\"\"\n    sync(session, \"dev\")\n    session.run(\"pytest\", \"-q\", *session.posargs)\n\n\n@nox.session(python=PYTHONS[0])\ndef tests_lowest_deps(session: nox.Session) -> None:\n    \"\"\"Oldest Python with the lowest versions our constraints allow.\"\"\"\n    session.install(\"--resolution=lowest-direct\", \"-e\", \".\", \"pytest>=8\")\n    session.run(\"pytest\", \"-q\", *session.posargs)\n\n\n@nox.session(python=PYTHONS[-1])\ndef lint(session: nox.Session) -> None:\n    \"\"\"Ruff and mypy, once, on the newest Python.\"\"\"\n    sync(session, \"dev\")\n    session.run(\"ruff\", \"check\", \".\")\n    session.run(\"ruff\", \"format\", \"--check\", \".\")\n    session.run(\"mypy\", \"src\")\n\n\n@nox.session(python=[PYTHONS[0], PYTHONS[-1]])\ndef smoke(session: nox.Session) -> None:\n    \"\"\"Build the wheel, install it alone, and run the command from elsewhere.\"\"\"\n    dist = Path(session.create_tmp()) \u002F \"dist\"\n    session.run(\"uv\", \"build\", \"--wheel\", \"--out-dir\", str(dist), external=True)\n    session.install(str(next(dist.glob(\"*.whl\"))))\n    with session.chdir(session.create_tmp()):\n        session.run(\"mytool\", \"--version\")\n        session.run(\"mytool\", \"--help\", silent=True)\n","python","",[14,94,95,104,121,128,136,141,154,180,185,223,228,233,264,270,276,295,334,354,371,377,382,387,404,419,425,436,457,462,467,489,503,509,535,552,557,562,585,599,605,614,633,656,671,676,681,714,728,734,751,791,812,821,837],{"__ignoreMap":92},[96,97,100],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"sJ8bj","# noxfile.py\n",[96,105,107,111,115,118],{"class":98,"line":106},2,[96,108,110],{"class":109},"szBVR","from",[96,112,114],{"class":113},"sVt8B"," pathlib ",[96,116,117],{"class":109},"import",[96,119,120],{"class":113}," Path\n",[96,122,124],{"class":98,"line":123},3,[96,125,127],{"emptyLinePlaceholder":126},true,"\n",[96,129,131,133],{"class":98,"line":130},4,[96,132,117],{"class":109},[96,134,135],{"class":113}," nox\n",[96,137,139],{"class":98,"line":138},5,[96,140,127],{"emptyLinePlaceholder":126},[96,142,144,147,150],{"class":98,"line":143},6,[96,145,146],{"class":113},"nox.options.default_venv_backend ",[96,148,149],{"class":109},"=",[96,151,153],{"class":152},"sZZnC"," \"uv\"\n",[96,155,157,160,162,165,168,171,174,177],{"class":98,"line":156},7,[96,158,159],{"class":113},"nox.options.sessions ",[96,161,149],{"class":109},[96,163,164],{"class":113}," [",[96,166,167],{"class":152},"\"tests\"",[96,169,170],{"class":113},", ",[96,172,173],{"class":152},"\"lint\"",[96,175,176],{"class":113},"]        ",[96,178,179],{"class":102},"# what plain `nox` runs\n",[96,181,183],{"class":98,"line":182},8,[96,184,127],{"emptyLinePlaceholder":126},[96,186,188,192,195,197,200,202,205,207,210,212,215,217,220],{"class":98,"line":187},9,[96,189,191],{"class":190},"sj4cs","PYTHONS",[96,193,194],{"class":109}," =",[96,196,164],{"class":113},[96,198,199],{"class":152},"\"3.10\"",[96,201,170],{"class":113},[96,203,204],{"class":152},"\"3.11\"",[96,206,170],{"class":113},[96,208,209],{"class":152},"\"3.12\"",[96,211,170],{"class":113},[96,213,214],{"class":152},"\"3.13\"",[96,216,170],{"class":113},[96,218,219],{"class":152},"\"3.14\"",[96,221,222],{"class":113},"]\n",[96,224,226],{"class":98,"line":225},10,[96,227,127],{"emptyLinePlaceholder":126},[96,229,231],{"class":98,"line":230},11,[96,232,127],{"emptyLinePlaceholder":126},[96,234,236,239,243,246,249,252,255,258,261],{"class":98,"line":235},12,[96,237,238],{"class":109},"def",[96,240,242],{"class":241},"sScJk"," sync",[96,244,245],{"class":113},"(session: nox.Session, ",[96,247,248],{"class":109},"*",[96,250,251],{"class":113},"groups: ",[96,253,254],{"class":190},"str",[96,256,257],{"class":113},") -> ",[96,259,260],{"class":190},"None",[96,262,263],{"class":113},":\n",[96,265,267],{"class":98,"line":266},13,[96,268,269],{"class":152},"    \"\"\"Install the project and groups from uv.lock into this session's environment.\"\"\"\n",[96,271,273],{"class":98,"line":272},14,[96,274,275],{"class":113},"    session.run_install(\n",[96,277,279,282,284,287,289,292],{"class":98,"line":278},15,[96,280,281],{"class":152},"        \"uv\"",[96,283,170],{"class":113},[96,285,286],{"class":152},"\"sync\"",[96,288,170],{"class":113},[96,290,291],{"class":152},"\"--locked\"",[96,293,294],{"class":113},",\n",[96,296,298,301,304,307,310,313,316,319,322,325,328,331],{"class":98,"line":297},16,[96,299,300],{"class":109},"        *",[96,302,303],{"class":113},"(",[96,305,306],{"class":109},"f",[96,308,309],{"class":152},"\"--group=",[96,311,312],{"class":190},"{",[96,314,315],{"class":113},"g",[96,317,318],{"class":190},"}",[96,320,321],{"class":152},"\"",[96,323,324],{"class":109}," for",[96,326,327],{"class":113}," g ",[96,329,330],{"class":109},"in",[96,332,333],{"class":113}," groups),\n",[96,335,337,340,343,345,348,350,352],{"class":98,"line":336},17,[96,338,339],{"class":109},"        f",[96,341,342],{"class":152},"\"--python=",[96,344,312],{"class":190},[96,346,347],{"class":113},"session.virtualenv.location",[96,349,318],{"class":190},[96,351,321],{"class":152},[96,353,294],{"class":113},[96,355,357,361,363,365,368],{"class":98,"line":356},18,[96,358,360],{"class":359},"s4XuR","        env",[96,362,149],{"class":109},[96,364,312],{"class":113},[96,366,367],{"class":152},"\"UV_PROJECT_ENVIRONMENT\"",[96,369,370],{"class":113},": session.virtualenv.location},\n",[96,372,374],{"class":98,"line":373},19,[96,375,376],{"class":113},"    )\n",[96,378,380],{"class":98,"line":379},20,[96,381,127],{"emptyLinePlaceholder":126},[96,383,385],{"class":98,"line":384},21,[96,386,127],{"emptyLinePlaceholder":126},[96,388,390,393,395,397,399,401],{"class":98,"line":389},22,[96,391,392],{"class":241},"@nox.session",[96,394,303],{"class":113},[96,396,91],{"class":359},[96,398,149],{"class":109},[96,400,191],{"class":190},[96,402,403],{"class":113},")\n",[96,405,407,409,412,415,417],{"class":98,"line":406},23,[96,408,238],{"class":109},[96,410,411],{"class":241}," tests",[96,413,414],{"class":113},"(session: nox.Session) -> ",[96,416,260],{"class":190},[96,418,263],{"class":113},[96,420,422],{"class":98,"line":421},24,[96,423,424],{"class":152},"    \"\"\"Run the test suite with locked dependencies.\"\"\"\n",[96,426,428,431,434],{"class":98,"line":427},25,[96,429,430],{"class":113},"    sync(session, ",[96,432,433],{"class":152},"\"dev\"",[96,435,403],{"class":113},[96,437,439,442,445,447,450,452,454],{"class":98,"line":438},26,[96,440,441],{"class":113},"    session.run(",[96,443,444],{"class":152},"\"pytest\"",[96,446,170],{"class":113},[96,448,449],{"class":152},"\"-q\"",[96,451,170],{"class":113},[96,453,248],{"class":109},[96,455,456],{"class":113},"session.posargs)\n",[96,458,460],{"class":98,"line":459},27,[96,461,127],{"emptyLinePlaceholder":126},[96,463,465],{"class":98,"line":464},28,[96,466,127],{"emptyLinePlaceholder":126},[96,468,470,472,474,476,478,480,483,486],{"class":98,"line":469},29,[96,471,392],{"class":241},[96,473,303],{"class":113},[96,475,91],{"class":359},[96,477,149],{"class":109},[96,479,191],{"class":190},[96,481,482],{"class":113},"[",[96,484,485],{"class":190},"0",[96,487,488],{"class":113},"])\n",[96,490,492,494,497,499,501],{"class":98,"line":491},30,[96,493,238],{"class":109},[96,495,496],{"class":241}," tests_lowest_deps",[96,498,414],{"class":113},[96,500,260],{"class":190},[96,502,263],{"class":113},[96,504,506],{"class":98,"line":505},31,[96,507,508],{"class":152},"    \"\"\"Oldest Python with the lowest versions our constraints allow.\"\"\"\n",[96,510,512,515,518,520,523,525,528,530,533],{"class":98,"line":511},32,[96,513,514],{"class":113},"    session.install(",[96,516,517],{"class":152},"\"--resolution=lowest-direct\"",[96,519,170],{"class":113},[96,521,522],{"class":152},"\"-e\"",[96,524,170],{"class":113},[96,526,527],{"class":152},"\".\"",[96,529,170],{"class":113},[96,531,532],{"class":152},"\"pytest>=8\"",[96,534,403],{"class":113},[96,536,538,540,542,544,546,548,550],{"class":98,"line":537},33,[96,539,441],{"class":113},[96,541,444],{"class":152},[96,543,170],{"class":113},[96,545,449],{"class":152},[96,547,170],{"class":113},[96,549,248],{"class":109},[96,551,456],{"class":113},[96,553,555],{"class":98,"line":554},34,[96,556,127],{"emptyLinePlaceholder":126},[96,558,560],{"class":98,"line":559},35,[96,561,127],{"emptyLinePlaceholder":126},[96,563,565,567,569,571,573,575,577,580,583],{"class":98,"line":564},36,[96,566,392],{"class":241},[96,568,303],{"class":113},[96,570,91],{"class":359},[96,572,149],{"class":109},[96,574,191],{"class":190},[96,576,482],{"class":113},[96,578,579],{"class":109},"-",[96,581,582],{"class":190},"1",[96,584,488],{"class":113},[96,586,588,590,593,595,597],{"class":98,"line":587},37,[96,589,238],{"class":109},[96,591,592],{"class":241}," lint",[96,594,414],{"class":113},[96,596,260],{"class":190},[96,598,263],{"class":113},[96,600,602],{"class":98,"line":601},38,[96,603,604],{"class":152},"    \"\"\"Ruff and mypy, once, on the newest Python.\"\"\"\n",[96,606,608,610,612],{"class":98,"line":607},39,[96,609,430],{"class":113},[96,611,433],{"class":152},[96,613,403],{"class":113},[96,615,617,619,622,624,627,629,631],{"class":98,"line":616},40,[96,618,441],{"class":113},[96,620,621],{"class":152},"\"ruff\"",[96,623,170],{"class":113},[96,625,626],{"class":152},"\"check\"",[96,628,170],{"class":113},[96,630,527],{"class":152},[96,632,403],{"class":113},[96,634,636,638,640,642,645,647,650,652,654],{"class":98,"line":635},41,[96,637,441],{"class":113},[96,639,621],{"class":152},[96,641,170],{"class":113},[96,643,644],{"class":152},"\"format\"",[96,646,170],{"class":113},[96,648,649],{"class":152},"\"--check\"",[96,651,170],{"class":113},[96,653,527],{"class":152},[96,655,403],{"class":113},[96,657,659,661,664,666,669],{"class":98,"line":658},42,[96,660,441],{"class":113},[96,662,663],{"class":152},"\"mypy\"",[96,665,170],{"class":113},[96,667,668],{"class":152},"\"src\"",[96,670,403],{"class":113},[96,672,674],{"class":98,"line":673},43,[96,675,127],{"emptyLinePlaceholder":126},[96,677,679],{"class":98,"line":678},44,[96,680,127],{"emptyLinePlaceholder":126},[96,682,684,686,688,690,692,694,696,698,700,703,705,707,709,711],{"class":98,"line":683},45,[96,685,392],{"class":241},[96,687,303],{"class":113},[96,689,91],{"class":359},[96,691,149],{"class":109},[96,693,482],{"class":113},[96,695,191],{"class":190},[96,697,482],{"class":113},[96,699,485],{"class":190},[96,701,702],{"class":113},"], ",[96,704,191],{"class":190},[96,706,482],{"class":113},[96,708,579],{"class":109},[96,710,582],{"class":190},[96,712,713],{"class":113},"]])\n",[96,715,717,719,722,724,726],{"class":98,"line":716},46,[96,718,238],{"class":109},[96,720,721],{"class":241}," smoke",[96,723,414],{"class":113},[96,725,260],{"class":190},[96,727,263],{"class":113},[96,729,731],{"class":98,"line":730},47,[96,732,733],{"class":152},"    \"\"\"Build the wheel, install it alone, and run the command from elsewhere.\"\"\"\n",[96,735,737,740,742,745,748],{"class":98,"line":736},48,[96,738,739],{"class":113},"    dist ",[96,741,149],{"class":109},[96,743,744],{"class":113}," Path(session.create_tmp()) ",[96,746,747],{"class":109},"\u002F",[96,749,750],{"class":152}," \"dist\"\n",[96,752,754,756,759,761,764,766,769,771,774,776,778,781,784,786,789],{"class":98,"line":753},49,[96,755,441],{"class":113},[96,757,758],{"class":152},"\"uv\"",[96,760,170],{"class":113},[96,762,763],{"class":152},"\"build\"",[96,765,170],{"class":113},[96,767,768],{"class":152},"\"--wheel\"",[96,770,170],{"class":113},[96,772,773],{"class":152},"\"--out-dir\"",[96,775,170],{"class":113},[96,777,254],{"class":190},[96,779,780],{"class":113},"(dist), ",[96,782,783],{"class":359},"external",[96,785,149],{"class":109},[96,787,788],{"class":190},"True",[96,790,403],{"class":113},[96,792,794,796,798,800,803,806,809],{"class":98,"line":793},50,[96,795,514],{"class":113},[96,797,254],{"class":190},[96,799,303],{"class":113},[96,801,802],{"class":190},"next",[96,804,805],{"class":113},"(dist.glob(",[96,807,808],{"class":152},"\"*.whl\"",[96,810,811],{"class":113},"))))\n",[96,813,815,818],{"class":98,"line":814},51,[96,816,817],{"class":109},"    with",[96,819,820],{"class":113}," session.chdir(session.create_tmp()):\n",[96,822,824,827,830,832,835],{"class":98,"line":823},52,[96,825,826],{"class":113},"        session.run(",[96,828,829],{"class":152},"\"mytool\"",[96,831,170],{"class":113},[96,833,834],{"class":152},"\"--version\"",[96,836,403],{"class":113},[96,838,840,842,844,846,849,851,854,856,858],{"class":98,"line":839},53,[96,841,826],{"class":113},[96,843,829],{"class":152},[96,845,170],{"class":113},[96,847,848],{"class":152},"\"--help\"",[96,850,170],{"class":113},[96,852,853],{"class":359},"silent",[96,855,149],{"class":109},[96,857,788],{"class":190},[96,859,403],{"class":113},[861,862,864],"h3",{"id":863},"how-it-fits-together","How it fits together",[79,866],{"name":867},"nox-matrix",[10,869,870,875,876,879],{},[27,871,872],{},[14,873,874],{},"default_venv_backend = \"uv\""," makes nox create environments with uv and install with ",[14,877,878],{},"uv pip",", and lets uv find or download each Python version on demand. A session that needed thirty seconds with virtualenv and pip takes a couple of seconds.",[10,881,882,889,890,892,893,896,897,900,901,903,904,907],{},[27,883,884,885,888],{},"The ",[14,886,887],{},"sync"," helper"," installs from ",[14,891,51],{}," rather than resolving afresh. Pointing ",[14,894,895],{},"UV_PROJECT_ENVIRONMENT"," at the session's environment makes ",[14,898,899],{},"uv sync"," populate nox's environment instead of the project ",[14,902,16],{},", and ",[14,905,906],{},"--locked"," fails if the lockfile is stale — so local sessions test exactly the versions CI and releases use.",[10,909,910,915,916,920,921,924,925,928,929,932],{},[27,911,912],{},[14,913,914],{},"tests_lowest_deps"," deliberately does ",[917,918,919],"em",{},"not"," use the lockfile. ",[14,922,923],{},"--resolution=lowest-direct"," installs the oldest version of each direct dependency your constraints allow, on your oldest Python. It is the only way to discover that you declared ",[14,926,927],{},"typer>=0.12"," but used something added in 0.15. Note that every dependency it installs needs a lower bound — an unbounded ",[14,930,931],{},"pytest"," would resolve to a release from 2010.",[10,934,935,941,942,945,946,949],{},[27,936,937,940],{},[14,938,939],{},"lint"," runs once."," Linting and type-checking do not depend on the runtime Python in any way that matters day to day, so running them on every version wastes time. Type-check against the ",[917,943,944],{},"oldest"," version via ",[14,947,948],{},"python_version"," in the mypy configuration instead.",[10,951,952,957,958,962],{},[27,953,954],{},[14,955,956],{},"smoke"," is the local version of ",[32,959,961],{"href":960},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F","smoke-testing the built wheel in CI",": build, install the wheel on its own into a fresh environment, and run the command from a temporary directory where the source tree cannot be imported by accident.",[38,964,966],{"id":965},"running-it","Running it",[87,968,972],{"className":969,"code":970,"language":971,"meta":92,"style":92},"language-bash shiki shiki-themes github-light github-dark","uvx nox                          # default sessions: tests on every Python, then lint\nuvx nox -s tests-3.10            # one session, one version\nuvx nox -s tests -- -k config    # everything after -- goes to pytest via session.posargs\nuvx nox -s smoke lint            # several sessions\nuvx nox -R -s tests-3.12         # reuse the existing environment, skip installs: fast reruns\nuvx nox --list                   # what is defined\n","bash",[14,973,974,985,1000,1022,1037,1054],{"__ignoreMap":92},[96,975,976,979,982],{"class":98,"line":99},[96,977,978],{"class":241},"uvx",[96,980,981],{"class":152}," nox",[96,983,984],{"class":102},"                          # default sessions: tests on every Python, then lint\n",[96,986,987,989,991,994,997],{"class":98,"line":106},[96,988,978],{"class":241},[96,990,981],{"class":152},[96,992,993],{"class":190}," -s",[96,995,996],{"class":152}," tests-3.10",[96,998,999],{"class":102},"            # one session, one version\n",[96,1001,1002,1004,1006,1008,1010,1013,1016,1019],{"class":98,"line":123},[96,1003,978],{"class":241},[96,1005,981],{"class":152},[96,1007,993],{"class":190},[96,1009,411],{"class":152},[96,1011,1012],{"class":190}," --",[96,1014,1015],{"class":190}," -k",[96,1017,1018],{"class":152}," config",[96,1020,1021],{"class":102},"    # everything after -- goes to pytest via session.posargs\n",[96,1023,1024,1026,1028,1030,1032,1034],{"class":98,"line":130},[96,1025,978],{"class":241},[96,1027,981],{"class":152},[96,1029,993],{"class":190},[96,1031,721],{"class":152},[96,1033,592],{"class":152},[96,1035,1036],{"class":102},"            # several sessions\n",[96,1038,1039,1041,1043,1046,1048,1051],{"class":98,"line":138},[96,1040,978],{"class":241},[96,1042,981],{"class":152},[96,1044,1045],{"class":190}," -R",[96,1047,993],{"class":190},[96,1049,1050],{"class":152}," tests-3.12",[96,1052,1053],{"class":102},"         # reuse the existing environment, skip installs: fast reruns\n",[96,1055,1056,1058,1060,1063],{"class":98,"line":143},[96,1057,978],{"class":241},[96,1059,981],{"class":152},[96,1061,1062],{"class":190}," --list",[96,1064,1065],{"class":102},"                   # what is defined\n",[79,1067],{"name":1068},"nox-terminal",[10,1070,1071,1074],{},[14,1072,1073],{},"-R"," (reuse and skip install) is the everyday speed-up: after the first run, rerunning a single failing session takes as long as the tests themselves.",[861,1076,1078],{"id":1077},"sessions-beyond-tests","Sessions beyond tests",[10,1080,1081],{},"Once a noxfile exists, it becomes the natural home for every repeatable developer task, because each gets a clean, documented environment for free:",[43,1083,1084,1096,1104,1116],{},[46,1085,1086,1091,1092,36],{},[27,1087,1088],{},[14,1089,1090],{},"docs"," builds the documentation, including a CLI reference generated from the real command tree, and fails on warnings. See ",[32,1093,1095],{"href":1094},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli\u002F","generating man pages and docs from a CLI",[46,1097,1098,1103],{},[27,1099,1100],{},[14,1101,1102],{},"completion"," regenerates bundled shell-completion scripts for bash, zsh and fish, so they never go stale relative to the commands.",[46,1105,1106,1111,1112,1115],{},[27,1107,1108],{},[14,1109,1110],{},"release_check"," builds the sdist and wheel, runs ",[14,1113,1114],{},"twine check",", and verifies that the version matches the changelog's newest entry before anyone pushes a tag.",[46,1117,1118,1123,1124,1127,1128,1131,1132,36],{},[27,1119,1120],{},[14,1121,1122],{},"bench"," times ",[14,1125,1126],{},"mytool --help"," with ",[14,1129,1130],{},"hyperfine"," or a small loop, catching startup regressions of the kind described in ",[32,1133,1135],{"href":1134},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002F","profiling Python CLI startup time",[10,1137,1138,1139,1142,1143,1146],{},"Leave these out of ",[14,1140,1141],{},"nox.options.sessions"," so the default run stays fast, and give each a docstring so ",[14,1144,1145],{},"uvx nox --list"," doubles as a list of project chores.",[38,1148,1150],{"id":1149},"using-the-same-sessions-in-ci","Using the same sessions in CI",[10,1152,1153],{},"The value of nox grows when CI runs the same sessions, so a green local run predicts a green pipeline:",[87,1155,1159],{"className":1156,"code":1157,"language":1158,"meta":92,"style":92},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fci.yml (excerpt)\n  nox:\n    strategy:\n      fail-fast: false\n      matrix:\n        session: [\"tests-3.10\", \"tests-3.12\", \"tests-3.14\", \"tests_lowest_deps\", \"lint\"]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: astral-sh\u002Fsetup-uv@v6\n        with: { enable-cache: true }\n      - run: uvx nox -s ${{ matrix.session }}\n","yaml",[14,1160,1161,1166,1174,1181,1192,1199,1231,1241,1248,1261,1272,1291],{"__ignoreMap":92},[96,1162,1163],{"class":98,"line":99},[96,1164,1165],{"class":102},"# .github\u002Fworkflows\u002Fci.yml (excerpt)\n",[96,1167,1168,1172],{"class":98,"line":106},[96,1169,1171],{"class":1170},"s9eBZ","  nox",[96,1173,263],{"class":113},[96,1175,1176,1179],{"class":98,"line":123},[96,1177,1178],{"class":1170},"    strategy",[96,1180,263],{"class":113},[96,1182,1183,1186,1189],{"class":98,"line":130},[96,1184,1185],{"class":1170},"      fail-fast",[96,1187,1188],{"class":113},": ",[96,1190,1191],{"class":190},"false\n",[96,1193,1194,1197],{"class":98,"line":138},[96,1195,1196],{"class":1170},"      matrix",[96,1198,263],{"class":113},[96,1200,1201,1204,1207,1210,1212,1215,1217,1220,1222,1225,1227,1229],{"class":98,"line":143},[96,1202,1203],{"class":1170},"        session",[96,1205,1206],{"class":113},": [",[96,1208,1209],{"class":152},"\"tests-3.10\"",[96,1211,170],{"class":113},[96,1213,1214],{"class":152},"\"tests-3.12\"",[96,1216,170],{"class":113},[96,1218,1219],{"class":152},"\"tests-3.14\"",[96,1221,170],{"class":113},[96,1223,1224],{"class":152},"\"tests_lowest_deps\"",[96,1226,170],{"class":113},[96,1228,173],{"class":152},[96,1230,222],{"class":113},[96,1232,1233,1236,1238],{"class":98,"line":156},[96,1234,1235],{"class":1170},"    runs-on",[96,1237,1188],{"class":113},[96,1239,1240],{"class":152},"ubuntu-latest\n",[96,1242,1243,1246],{"class":98,"line":182},[96,1244,1245],{"class":1170},"    steps",[96,1247,263],{"class":113},[96,1249,1250,1253,1256,1258],{"class":98,"line":187},[96,1251,1252],{"class":113},"      - ",[96,1254,1255],{"class":1170},"uses",[96,1257,1188],{"class":113},[96,1259,1260],{"class":152},"actions\u002Fcheckout@v4\n",[96,1262,1263,1265,1267,1269],{"class":98,"line":225},[96,1264,1252],{"class":113},[96,1266,1255],{"class":1170},[96,1268,1188],{"class":113},[96,1270,1271],{"class":152},"astral-sh\u002Fsetup-uv@v6\n",[96,1273,1274,1277,1280,1283,1285,1288],{"class":98,"line":230},[96,1275,1276],{"class":1170},"        with",[96,1278,1279],{"class":113},": { ",[96,1281,1282],{"class":1170},"enable-cache",[96,1284,1188],{"class":113},[96,1286,1287],{"class":190},"true",[96,1289,1290],{"class":113}," }\n",[96,1292,1293,1295,1298,1300],{"class":98,"line":235},[96,1294,1252],{"class":113},[96,1296,1297],{"class":1170},"run",[96,1299,1188],{"class":113},[96,1301,1302],{"class":152},"uvx nox -s ${{ matrix.session }}\n",[10,1304,1305,1306,170,1309,1312,1313,1317,1318,1320,1321,1324],{},"Windows and macOS jobs can run a subset (",[14,1307,1308],{},"tests-3.10",[14,1310,1311],{},"tests-3.14","), following the matrix strategy in ",[32,1314,1316],{"href":1315},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions\u002F","testing a CLI across Python versions with GitHub Actions",". Keeping the list of versions in one place — ",[14,1319,191],{}," in the noxfile — and generating the CI matrix from ",[14,1322,1323],{},"uvx nox --list --json"," avoids the two drifting apart on larger projects.",[38,1326,1328],{"id":1327},"ux-considerations","UX considerations",[10,1330,1331],{},"The users of a noxfile are contributors, and it should make the right thing easy:",[43,1333,1334,1343,1359,1368],{},[46,1335,1336,1339,1340,1342],{},[27,1337,1338],{},"Make the default useful."," Plain ",[14,1341,62],{}," should run what a contributor needs before pushing — usually tests and lint — without the slow sessions.",[46,1344,1345,1348,1349,1351,1352,1354,1355,1358],{},[27,1346,1347],{},"Name sessions for their purpose."," ",[14,1350,914],{}," and ",[14,1353,956],{}," explain themselves in ",[14,1356,1357],{},"--list","; docstrings appear there too.",[46,1360,1361,1348,1364,1367],{},[27,1362,1363],{},"Pass arguments through.",[14,1365,1366],{},"session.posargs"," lets people run a single test file on every Python version without editing anything.",[46,1369,1370,1373,1374,1376,1377,1380],{},[27,1371,1372],{},"Document it in one line."," \"Run ",[14,1375,62],{}," before pushing\" in ",[14,1378,1379],{},"CONTRIBUTING.md"," replaces a page of environment setup instructions, because nox creates everything it needs.",[38,1382,1384],{"id":1383},"testing-the-behaviour","Testing the behaviour",[10,1386,1387,1388,1390,1391,1394,1395,1397,1398,1401,1402,1405],{},"A noxfile is code that runs rarely enough to rot quietly. Two habits keep it working: run ",[14,1389,1145],{}," in CI's lint job, which fails on syntax errors and import problems in the noxfile itself, and run the full default set occasionally on a clean checkout (",[14,1392,1393],{},"git clean -xdf"," first, or in a fresh clone) so leftover state cannot mask a broken session. When adding a Python version, add it to ",[14,1396,191],{},", run ",[14,1399,1400],{},"uvx nox -s tests-3.15"," locally, and only then add it to ",[14,1403,1404],{},"requires-python"," and the trove classifiers — the order that guarantees you never claim support you have not tested.",[38,1407,1409],{"id":1408},"conclusion","Conclusion",[10,1411,1412],{},"nox brings the CI matrix to your laptop: one Python file, sessions in fresh environments for each version, and uv making those environments nearly free to create. Install from the lockfile for ordinary test sessions, add one lowest-dependency session to keep your lower bounds honest, run lint once, smoke-test the built wheel, and have CI call the same sessions. \"It passes on my machine\" then means the same thing as \"it passes in CI\".",[38,1414,1416],{"id":1415},"frequently-asked-questions","Frequently asked questions",[861,1418,1420],{"id":1419},"nox-or-tox","nox or tox?",[10,1422,1423,1424,1426,1427,1430],{},"Both do the job. tox is configured in INI or TOML and has a large plugin ecosystem; nox is configured in Python, which makes conditional logic and helpers such as ",[14,1425,887],{}," above straightforward. With the ",[14,1428,1429],{},"tox-uv"," plugin, tox gets the same speed benefits. Choose by which configuration style your team prefers.",[861,1432,1434,1435,1437],{"id":1433},"do-i-still-need-a-venv-for-development","Do I still need a ",[14,1436,16],{}," for development?",[10,1439,1440,1441,1443],{},"Yes — nox environments are for checking, not for editing with your IDE's autocompletion. Keep ",[14,1442,899],{}," for your everyday environment and use nox before pushing.",[861,1445,1447],{"id":1446},"how-do-i-test-against-a-python-version-uv-cannot-download","How do I test against a Python version uv cannot download?",[10,1449,1450],{},"Point nox at an interpreter already on your system by listing it by path or name; uv uses installed interpreters before downloading. Unusual builds — a distribution's patched Python, PyPy — can be listed the same way.",[861,1452,1454],{"id":1453},"can-sessions-share-an-environment-to-save-time","Can sessions share an environment to save time?",[10,1456,1457,1458,1461,1462,1464],{},"They can with ",[14,1459,1460],{},"venv_backend=\"none\""," or by reusing environments, but that gives up the isolation that makes nox results trustworthy. Use ",[14,1463,1073],{}," for fast reruns instead.",[38,1466,1468],{"id":1467},"related","Related",[43,1470,1471,1477,1483,1489,1494],{},[46,1472,1473,1474],{},"Up: ",[32,1475,1476],{"href":34},"Virtual environments and isolation best practices",[46,1478,1479],{},[32,1480,1482],{"href":1481},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis\u002F","Managing virtual environments for cross-platform CLIs",[46,1484,1485],{},[32,1486,1488],{"href":1487},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli\u002F","Pinning the Python version for a CLI",[46,1490,1491],{},[32,1492,1493],{"href":1315},"Testing a CLI across Python versions with GitHub Actions",[46,1495,1496],{},[32,1497,1499],{"href":1498},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci\u002F","Caching uv dependencies in CI",[1501,1502,1503],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":92,"searchDepth":106,"depth":106,"links":1505},[1506,1507,1508,1511,1514,1515,1516,1517,1518,1525],{"id":40,"depth":106,"text":41},{"id":69,"depth":106,"text":70},{"id":84,"depth":106,"text":85,"children":1509},[1510],{"id":863,"depth":123,"text":864},{"id":965,"depth":106,"text":966,"children":1512},[1513],{"id":1077,"depth":123,"text":1078},{"id":1149,"depth":106,"text":1150},{"id":1327,"depth":106,"text":1328},{"id":1383,"depth":106,"text":1384},{"id":1408,"depth":106,"text":1409},{"id":1415,"depth":106,"text":1416,"children":1519},[1520,1521,1523,1524],{"id":1419,"depth":123,"text":1420},{"id":1433,"depth":123,"text":1522},"Do I still need a .venv for development?",{"id":1446,"depth":123,"text":1447},{"id":1453,"depth":123,"text":1454},{"id":1467,"depth":106,"text":1468},"2026-09-18","Test a Python CLI on every supported Python locally with nox and uv: fresh environments per version, locked and lowest-dependency runs, wheel smoke tests and CI.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox",{"title":5,"description":1527},"project-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox\u002Findex",[1536,1537,1538,1539,1540],"nox","testing","python-versions","uv","virtualenv","ywwT_t_25mWvnYmN7kqajV_OT5U9cOHjdyqC32qkdt8",[1543,1546,1549,1552,1555,1558,1561,1564,1567,1570,1573,1576,1579,1582,1585,1588,1591,1594,1597,1600,1603,1606,1609,1612,1615,1618,1621,1624,1627,1630,1633,1636,1639,1642,1645,1648,1651,1654,1657,1660,1663,1666,1669,1672,1675,1678,1681,1684,1687,1690,1693,1696,1699,1702,1705,1708,1711,1714,1717,1720,1723,1726,1729,1732,1735,1738,1741,1744,1747,1750,1753,1756,1759,1762,1765,1768,1771,1774,1777,1780,1783,1786,1789,1792,1795,1798,1801,1804,1807,1810,1813,1816,1818,1821,1824,1827,1830,1833,1836,1839,1842,1845,1848,1851,1854,1857,1860,1863,1866,1869,1872,1875,1878,1881,1884,1887,1890,1893,1896,1899,1902,1905,1908,1911,1914,1917,1920,1923,1926,1929,1932,1935,1938,1941,1944,1947,1950,1953,1956,1959,1962,1965,1968,1971,1974,1977,1980,1983,1986,1989,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088],{"path":1544,"title":1545},"\u002Fabout","About Python CLI Toolcraft",{"path":1547,"title":1548},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1550,"title":1551},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1553,"title":1554},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1556,"title":1557},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1559,"title":1560},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1562,"title":1563},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":1565,"title":1566},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":1568,"title":1569},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1571,"title":1572},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1574,"title":1575},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":1577,"title":1578},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":1580,"title":1581},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1583,"title":1584},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1586,"title":1587},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1589,"title":1590},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1592,"title":1593},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1595,"title":1596},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":1598,"title":1599},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1601,"title":1602},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1604,"title":1605},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1607,"title":1608},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":1610,"title":1611},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1613,"title":1614},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1616,"title":1617},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1619,"title":1620},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":1622,"title":1623},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1625,"title":1626},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":1628,"title":1629},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1631,"title":1632},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":1634,"title":1635},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1637,"title":1638},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1640,"title":1641},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1643,"title":1644},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":1646,"title":1647},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1649,"title":1650},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1652,"title":1653},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1655,"title":1656},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1658,"title":1659},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1661,"title":1662},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":1664,"title":1665},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":1667,"title":1668},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1670,"title":1671},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":1673,"title":1674},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1676,"title":1677},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":1679,"title":1680},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1682,"title":1683},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1685,"title":1686},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1688,"title":1689},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":1691,"title":1692},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1694,"title":1695},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1697,"title":1698},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1700,"title":1701},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1703,"title":1704},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":1706,"title":1707},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1709,"title":1710},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":1712,"title":1713},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1715,"title":1716},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1718,"title":1719},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1721,"title":1722},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1724,"title":1725},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":1727,"title":1728},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":1730,"title":1731},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1733,"title":1734},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1736,"title":1737},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1739,"title":1740},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1742,"title":1743},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":1745,"title":1746},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1748,"title":1749},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1751,"title":1752},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1754,"title":1755},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1757,"title":1758},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1760,"title":1761},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1763,"title":1764},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1766,"title":1767},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":1769,"title":1770},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1772,"title":1773},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":1775,"title":1776},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1778,"title":1779},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":1781,"title":1782},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1784,"title":1785},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1787,"title":1788},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1790,"title":1791},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1793,"title":1794},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1796,"title":1797},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":1799,"title":1800},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1802,"title":1803},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1805,"title":1806},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":1808,"title":1809},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":1811,"title":1812},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1814,"title":1815},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":747,"title":1817},"Python CLI Toolcraft",{"path":1819,"title":1820},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":1822,"title":1823},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1825,"title":1826},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1828,"title":1829},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1831,"title":1832},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1834,"title":1835},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1837,"title":1838},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1840,"title":1841},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1843,"title":1844},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1846,"title":1847},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1849,"title":1850},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1852,"title":1853},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":1855,"title":1856},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":1858,"title":1859},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":1861,"title":1862},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1864,"title":1865},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1867,"title":1868},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1870,"title":1871},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1873,"title":1874},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1876,"title":1877},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1879,"title":1880},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1882,"title":1883},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":1885,"title":1886},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":1888,"title":1889},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1891,"title":1892},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":1894,"title":1895},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1897,"title":1898},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1900,"title":1901},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1903,"title":1904},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":1906,"title":1907},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1909,"title":1910},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1912,"title":1913},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":1915,"title":1916},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1918,"title":1919},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1921,"title":1922},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1924,"title":1925},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1927,"title":1928},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":1930,"title":1931},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1933,"title":1934},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":1936,"title":1937},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1939,"title":1940},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1942,"title":1943},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":1945,"title":1946},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":1948,"title":1949},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":1951,"title":1952},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":1954,"title":1955},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":1957,"title":1958},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":1960,"title":1961},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":1963,"title":1964},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":1966,"title":1967},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1969,"title":1970},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1972,"title":1973},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":1975,"title":1976},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1978,"title":1979},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1981,"title":1982},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1984,"title":1985},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1987,"title":1988},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":1990,"title":1991},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":1993,"title":1994},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1996,"title":1997},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":1999,"title":2000},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2002,"title":2003},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2005,"title":2006},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2008,"title":2009},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2011,"title":2012},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2014,"title":2015},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2017,"title":2018},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2020,"title":2021},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2023,"title":2024},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2026,"title":2027},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2029,"title":2030},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2032,"title":2033},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2035,"title":2036},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2038,"title":2039},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2041,"title":2042},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2044,"title":2045},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2047,"title":2048},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2050,"title":2051},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2053,"title":2054},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2056,"title":2057},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":2059,"title":2060},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2062,"title":2063},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2065,"title":2066},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2068,"title":2069},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2071,"title":2072},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":2074,"title":2075},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":2077,"title":2078},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2080,"title":2081},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2083,"title":2084},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2086,"title":2087},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":1532,"title":5},1789736907988]