[{"data":1,"prerenderedAt":1252},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli\u002F":3,"content-directory":1004},{"id":4,"title":5,"body":6,"date":990,"description":991,"difficulty":992,"draft":993,"extension":994,"meta":995,"navigation":748,"path":996,"seo":997,"stem":998,"tags":999,"updated":990,"__hash__":1003},"content\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli\u002Findex.md","Pinning the Python Version for a CLI",{"type":7,"value":8,"toc":967},"minimark",[9,13,18,58,62,68,72,76,120,140,155,166,170,173,176,192,218,257,282,296,300,303,306,323,329,332,344,348,474,477,492,495,566,570,577,580,603,606,669,673,679,694,704,708,711,850,856,862,866,869,873,878,881,888,891,895,898,905,908,912,918,922,925,929,963],[10,11,12],"p",{},"A CLI runs on whatever interpreter the user has, which is rarely the one you developed against.\nDeclaring which versions you support — and testing exactly those — is what turns \"it should work\"\ninto something an installer can enforce before anything breaks.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,34,42,48,55],"ul",{},[22,23,24,25,29,30,33],"li",{},"Declare ",[26,27,28],"code",{},"requires-python"," in ",[26,31,32],{},"pyproject.toml","; installers refuse unsupported versions up front.",[22,35,36,37,41],{},"Test the ",[38,39,40],"strong",{},"ends"," of that range in CI, not the middle — those are the versions that break.",[22,43,44,47],{},[26,45,46],{},".python-version"," sets the local default; it is not a support statement.",[22,49,50,51,54],{},"Raising the floor is a ",[38,52,53],{},"breaking change",": bump the major version and say so.",[22,56,57],{},"Follow upstream end-of-life dates for a policy nobody has to argue about.",[14,59,61],{"id":60},"prerequisites","Prerequisites",[10,63,64,65,67],{},"A packaged CLI with a ",[26,66,32],{},", and a CI workflow you can add a matrix to.",[14,69,71],{"id":70},"what-requires-python-actually-does","What requires-python actually does",[73,74],"inline-diagram",{"name":75},"requires-python-flow",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-toml shiki shiki-themes github-light github-dark","[project]\nname = \"mytool\"\nrequires-python = \">=3.11\"\n","toml","",[26,84,85,101,111],{"__ignoreMap":82},[86,87,90,94,98],"span",{"class":88,"line":89},"line",1,[86,91,93],{"class":92},"sVt8B","[",[86,95,97],{"class":96},"sScJk","project",[86,99,100],{"class":92},"]\n",[86,102,104,107],{"class":88,"line":103},2,[86,105,106],{"class":92},"name = ",[86,108,110],{"class":109},"sZZnC","\"mytool\"\n",[86,112,114,117],{"class":88,"line":113},3,[86,115,116],{"class":92},"requires-python = ",[86,118,119],{"class":109},"\">=3.11\"\n",[10,121,122,123,127,128,131,132,135,136,139],{},"That line is metadata the installer reads ",[124,125,126],"em",{},"before"," downloading anything. On an unsupported\ninterpreter, ",[26,129,130],{},"pip install mytool"," or ",[26,133,134],{},"pipx install mytool"," fails with a message naming the required\nrange — instead of installing successfully and failing later with a ",[26,137,138],{},"SyntaxError"," on a match\nstatement, which is the failure people actually report.",[10,141,142,143,146,147,150,151,154],{},"Two details are worth getting right. Use ",[26,144,145],{},">="," with no upper bound unless you have a specific reason:\ncapping at ",[26,148,149],{},"\u003C4.0"," is conventional and harmless, but ",[26,152,153],{},"\u003C3.14"," means your tool stops installing the day\na new Python is released, which is rarely what you want and always discovered at the worst time.",[10,156,157,158,161,162,165],{},"And keep it honest. ",[26,159,160],{},">=3.9"," while your code uses ",[26,163,164],{},"X | None"," syntax means the metadata promises\nsomething the code cannot deliver, and the failure moves back to being a confusing runtime error.",[14,167,169],{"id":168},"the-four-places-a-version-is-decided","The four places a version is decided",[73,171],{"name":172},"python-version-matrix",[10,174,175],{},"They serve different purposes and are easy to conflate:",[77,177,179],{"className":79,"code":178,"language":81,"meta":82,"style":82},"requires-python = \">=3.11\"        # what may install this package — a promise to users\n",[26,180,181],{"__ignoreMap":82},[86,182,183,185,188],{"class":88,"line":89},[86,184,116],{"class":92},[86,186,187],{"class":109},"\">=3.11\"",[86,189,191],{"class":190},"sJ8bj","        # what may install this package — a promise to users\n",[77,193,197],{"className":194,"code":195,"language":196,"meta":82,"style":82},"language-bash shiki shiki-themes github-light github-dark","echo \"3.12\" > .python-version     # what uv or pyenv uses locally — a developer convenience\n","bash",[26,198,199],{"__ignoreMap":82},[86,200,201,205,208,212,215],{"class":88,"line":89},[86,202,204],{"class":203},"sj4cs","echo",[86,206,207],{"class":109}," \"3.12\"",[86,209,211],{"class":210},"szBVR"," >",[86,213,214],{"class":109}," .python-version",[86,216,217],{"class":190},"     # what uv or pyenv uses locally — a developer convenience\n",[77,219,223],{"className":220,"code":221,"language":222,"meta":82,"style":82},"language-yaml shiki shiki-themes github-light github-dark","matrix:\n  python: [\"3.11\", \"3.13\"]        # what is actually verified — the truth\n","yaml",[26,224,225,234],{"__ignoreMap":82},[86,226,227,231],{"class":88,"line":89},[86,228,230],{"class":229},"s9eBZ","matrix",[86,232,233],{"class":92},":\n",[86,235,236,239,242,245,248,251,254],{"class":88,"line":103},[86,237,238],{"class":229},"  python",[86,240,241],{"class":92},": [",[86,243,244],{"class":109},"\"3.11\"",[86,246,247],{"class":92},", ",[86,249,250],{"class":109},"\"3.13\"",[86,252,253],{"class":92},"]        ",[86,255,256],{"class":190},"# what is actually verified — the truth\n",[77,258,260],{"className":194,"code":259,"language":196,"meta":82,"style":82},"pipx install mytool --python 3.12 # what the user's install runs on — their choice\n",[26,261,262],{"__ignoreMap":82},[86,263,264,267,270,273,276,279],{"class":88,"line":89},[86,265,266],{"class":96},"pipx",[86,268,269],{"class":109}," install",[86,271,272],{"class":109}," mytool",[86,274,275],{"class":203}," --python",[86,277,278],{"class":203}," 3.12",[86,280,281],{"class":190}," # what the user's install runs on — their choice\n",[10,283,284,285,287,288,291,292,295],{},"The pair that must agree is the first and third. If ",[26,286,28],{}," says ",[26,289,290],{},">=3.10"," and CI tests\nonly 3.12, you are shipping support you have never verified; if CI tests 3.9 and the metadata says\n",[26,293,294],{},">=3.11",", you are testing something nobody can install.",[14,297,299],{"id":298},"choosing-the-floor","Choosing the floor",[73,301],{"name":302},"version-support-timeline",[10,304,305],{},"Two considerations, pulling in opposite directions.",[10,307,308,311,312,315,316,318,319,322],{},[38,309,310],{},"Language features you want."," Each release brings things worth having: ",[26,313,314],{},"tomllib"," in 3.11 removes a\ndependency for TOML configuration, and structural pattern matching, ",[26,317,164],{}," annotations and\n",[26,320,321],{},"ExceptionGroup"," all arrived recently enough to matter. Raising the floor makes code simpler.",[10,324,325,328],{},[38,326,327],{},"Where your users are."," Distribution Pythons lag. A tool aimed at developers can require a recent\nversion; one aimed at servers or CI images has to survive whatever the base image ships.",[10,330,331],{},"The defensible policy is to follow upstream: Python releases get five years of support, and dropping\na version once its security support ends is a rule nobody has to argue about. In practice that keeps\nyou supporting three or four versions at a time, which is a manageable matrix.",[10,333,334,335,337,338,340,341,343],{},"For a new CLI in 2026, ",[26,336,294],{}," is a reasonable default: it gets ",[26,339,314],{},", the improved error\nmessages, and ",[26,342,321],{},", while remaining widely available.",[14,345,347],{"id":346},"a-ci-matrix-that-matches","A CI matrix that matches",[77,349,351],{"className":220,"code":350,"language":222,"meta":82,"style":82},"strategy:\n  fail-fast: false\n  matrix:\n    python: [\"3.11\", \"3.13\"]        # the ends of requires-python\n    os: [ubuntu-latest, windows-latest]\nsteps:\n  - uses: actions\u002Fcheckout@v4\n  - uses: astral-sh\u002Fsetup-uv@v3\n  - run: uv sync --frozen --python ${{ matrix.python }}\n  - run: uv run pytest -q\n",[26,352,353,360,371,378,397,415,423,437,449,462],{"__ignoreMap":82},[86,354,355,358],{"class":88,"line":89},[86,356,357],{"class":229},"strategy",[86,359,233],{"class":92},[86,361,362,365,368],{"class":88,"line":103},[86,363,364],{"class":229},"  fail-fast",[86,366,367],{"class":92},": ",[86,369,370],{"class":203},"false\n",[86,372,373,376],{"class":88,"line":113},[86,374,375],{"class":229},"  matrix",[86,377,233],{"class":92},[86,379,381,384,386,388,390,392,394],{"class":88,"line":380},4,[86,382,383],{"class":229},"    python",[86,385,241],{"class":92},[86,387,244],{"class":109},[86,389,247],{"class":92},[86,391,250],{"class":109},[86,393,253],{"class":92},[86,395,396],{"class":190},"# the ends of requires-python\n",[86,398,400,403,405,408,410,413],{"class":88,"line":399},5,[86,401,402],{"class":229},"    os",[86,404,241],{"class":92},[86,406,407],{"class":109},"ubuntu-latest",[86,409,247],{"class":92},[86,411,412],{"class":109},"windows-latest",[86,414,100],{"class":92},[86,416,418,421],{"class":88,"line":417},6,[86,419,420],{"class":229},"steps",[86,422,233],{"class":92},[86,424,426,429,432,434],{"class":88,"line":425},7,[86,427,428],{"class":92},"  - ",[86,430,431],{"class":229},"uses",[86,433,367],{"class":92},[86,435,436],{"class":109},"actions\u002Fcheckout@v4\n",[86,438,440,442,444,446],{"class":88,"line":439},8,[86,441,428],{"class":92},[86,443,431],{"class":229},[86,445,367],{"class":92},[86,447,448],{"class":109},"astral-sh\u002Fsetup-uv@v3\n",[86,450,452,454,457,459],{"class":88,"line":451},9,[86,453,428],{"class":92},[86,455,456],{"class":229},"run",[86,458,367],{"class":92},[86,460,461],{"class":109},"uv sync --frozen --python ${{ matrix.python }}\n",[86,463,465,467,469,471],{"class":88,"line":464},10,[86,466,428],{"class":92},[86,468,456],{"class":229},[86,470,367],{"class":92},[86,472,473],{"class":109},"uv run pytest -q\n",[10,475,476],{},"Testing the ends rather than every version is a deliberate economy. The oldest supported version\ncatches syntax and library features you accidentally relied on; the newest catches deprecations and\nbehaviour changes. Versions in between rarely surface anything distinct, and each one is CI minutes\nspent for very little.",[10,478,479,480,483,484,487,488,491],{},"What genuinely justifies more entries is the ",[124,481,482],{},"platform"," axis. Windows path handling, console\nencoding and the ",[26,485,486],{},"bin"," versus ",[26,489,490],{},"Scripts"," split break tools that Linux CI declares healthy.",[10,493,494],{},"Add one job that verifies the metadata is enforceable:",[77,496,498],{"className":220,"code":497,"language":222,"meta":82,"style":82},"  refuses-old-python:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fsetup-python@v5\n        with: { python-version: \"3.10\" }\n      - run: pip install dist\u002F*.whl && exit 1 || echo \"correctly refused\"\n",[26,499,500,507,517,524,536,555],{"__ignoreMap":82},[86,501,502,505],{"class":88,"line":89},[86,503,504],{"class":229},"  refuses-old-python",[86,506,233],{"class":92},[86,508,509,512,514],{"class":88,"line":103},[86,510,511],{"class":229},"    runs-on",[86,513,367],{"class":92},[86,515,516],{"class":109},"ubuntu-latest\n",[86,518,519,522],{"class":88,"line":113},[86,520,521],{"class":229},"    steps",[86,523,233],{"class":92},[86,525,526,529,531,533],{"class":88,"line":380},[86,527,528],{"class":92},"      - ",[86,530,431],{"class":229},[86,532,367],{"class":92},[86,534,535],{"class":109},"actions\u002Fsetup-python@v5\n",[86,537,538,541,544,547,549,552],{"class":88,"line":399},[86,539,540],{"class":229},"        with",[86,542,543],{"class":92},": { ",[86,545,546],{"class":229},"python-version",[86,548,367],{"class":92},[86,550,551],{"class":109},"\"3.10\"",[86,553,554],{"class":92}," }\n",[86,556,557,559,561,563],{"class":88,"line":417},[86,558,528],{"class":92},[86,560,456],{"class":229},[86,562,367],{"class":92},[86,564,565],{"class":109},"pip install dist\u002F*.whl && exit 1 || echo \"correctly refused\"\n",[14,567,569],{"id":568},"raising-the-floor-later","Raising the floor later",[10,571,572,573,576],{},"Dropping a Python version is a breaking change for anyone still on it: their next ",[26,574,575],{},"pipx upgrade","\neither refuses or, worse, keeps them silently on an old release.",[10,578,579],{},"The sequence that treats users well:",[581,582,583,589,595],"ol",{},[22,584,585,588],{},[38,586,587],{},"Announce it"," in the release notes of a minor version, with the version and timeframe.",[22,590,591,594],{},[38,592,593],{},"Warn at run time"," for a release or two when running on the interpreter about to be dropped —\none line on stderr, naming the version and the deadline.",[22,596,597,602],{},[38,598,599,600],{},"Raise ",[26,601,28],{}," in a major release, with a changelog entry.",[10,604,605],{},"That last step matters: a user on the old interpreter will keep resolving to your last compatible\nversion, which is exactly the behaviour you want as long as that version was a real release rather\nthan an accident.",[77,607,611],{"className":608,"code":609,"language":610,"meta":82,"style":82},"language-python shiki shiki-themes github-light github-dark","if sys.version_info \u003C (3, 12):\n    err.print(\"[yellow]mytool 3.0 will require Python 3.12; you are on \"\n              f\"{platform.python_version()}[\u002F]\")\n","python",[26,612,613,638,646],{"__ignoreMap":82},[86,614,615,618,621,624,627,630,632,635],{"class":88,"line":89},[86,616,617],{"class":210},"if",[86,619,620],{"class":92}," sys.version_info ",[86,622,623],{"class":210},"\u003C",[86,625,626],{"class":92}," (",[86,628,629],{"class":203},"3",[86,631,247],{"class":92},[86,633,634],{"class":203},"12",[86,636,637],{"class":92},"):\n",[86,639,640,643],{"class":88,"line":103},[86,641,642],{"class":92},"    err.print(",[86,644,645],{"class":109},"\"[yellow]mytool 3.0 will require Python 3.12; you are on \"\n",[86,647,648,651,654,657,660,663,666],{"class":88,"line":113},[86,649,650],{"class":210},"              f",[86,652,653],{"class":109},"\"",[86,655,656],{"class":203},"{",[86,658,659],{"class":92},"platform.python_version()",[86,661,662],{"class":203},"}",[86,664,665],{"class":109},"[\u002F]\"",[86,667,668],{"class":92},")\n",[14,670,672],{"id":671},"ux-considerations","UX considerations",[10,674,675,678],{},[38,676,677],{},"Say the requirement in the README",", next to the install line. A user hitting a resolver error is\nlooking for that sentence, and finding it in the metadata means reading a traceback first.",[10,680,681,688,689,687],{},[38,682,683,684,687],{},"Report the interpreter in ",[26,685,686],{},"--version","."," Half of all \"it does not work\" reports resolve to the\nwrong Python, and printing the path answers it immediately — see\n",[690,691,693],"a",{"href":692},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata\u002F","exposing version info",[10,695,696,699,700,703],{},[38,697,698],{},"Do not pin an exact version for users."," ",[26,701,702],{},"requires-python = \"==3.12.*\""," makes your tool\nuninstallable next to anything with a different constraint. Ranges, always.",[14,705,707],{"id":706},"testing-the-behaviour","Testing the behaviour",[10,709,710],{},"Beyond the matrix, one test keeps the declaration and the code honest:",[77,712,714],{"className":608,"code":713,"language":610,"meta":82,"style":82},"import sys\nimport tomllib\nfrom pathlib import Path\n\ndef test_requires_python_matches_the_ci_matrix():\n    project = tomllib.loads(Path(\"pyproject.toml\").read_text())\n    declared = project[\"project\"][\"requires-python\"]\n    workflow = Path(\".github\u002Fworkflows\u002Ftest.yml\").read_text()\n\n    assert declared == \">=3.11\"\n    assert '\"3.11\"' in workflow          # the floor is actually tested\n",[26,715,716,724,731,744,750,761,778,799,815,819,833],{"__ignoreMap":82},[86,717,718,721],{"class":88,"line":89},[86,719,720],{"class":210},"import",[86,722,723],{"class":92}," sys\n",[86,725,726,728],{"class":88,"line":103},[86,727,720],{"class":210},[86,729,730],{"class":92}," tomllib\n",[86,732,733,736,739,741],{"class":88,"line":113},[86,734,735],{"class":210},"from",[86,737,738],{"class":92}," pathlib ",[86,740,720],{"class":210},[86,742,743],{"class":92}," Path\n",[86,745,746],{"class":88,"line":380},[86,747,749],{"emptyLinePlaceholder":748},true,"\n",[86,751,752,755,758],{"class":88,"line":399},[86,753,754],{"class":210},"def",[86,756,757],{"class":96}," test_requires_python_matches_the_ci_matrix",[86,759,760],{"class":92},"():\n",[86,762,763,766,769,772,775],{"class":88,"line":417},[86,764,765],{"class":92},"    project ",[86,767,768],{"class":210},"=",[86,770,771],{"class":92}," tomllib.loads(Path(",[86,773,774],{"class":109},"\"pyproject.toml\"",[86,776,777],{"class":92},").read_text())\n",[86,779,780,783,785,788,791,794,797],{"class":88,"line":425},[86,781,782],{"class":92},"    declared ",[86,784,768],{"class":210},[86,786,787],{"class":92}," project[",[86,789,790],{"class":109},"\"project\"",[86,792,793],{"class":92},"][",[86,795,796],{"class":109},"\"requires-python\"",[86,798,100],{"class":92},[86,800,801,804,806,809,812],{"class":88,"line":439},[86,802,803],{"class":92},"    workflow ",[86,805,768],{"class":210},[86,807,808],{"class":92}," Path(",[86,810,811],{"class":109},"\".github\u002Fworkflows\u002Ftest.yml\"",[86,813,814],{"class":92},").read_text()\n",[86,816,817],{"class":88,"line":451},[86,818,749],{"emptyLinePlaceholder":748},[86,820,821,824,827,830],{"class":88,"line":464},[86,822,823],{"class":210},"    assert",[86,825,826],{"class":92}," declared ",[86,828,829],{"class":210},"==",[86,831,832],{"class":109}," \">=3.11\"\n",[86,834,836,838,841,844,847],{"class":88,"line":835},11,[86,837,823],{"class":210},[86,839,840],{"class":109}," '\"3.11\"'",[86,842,843],{"class":210}," in",[86,845,846],{"class":92}," workflow          ",[86,848,849],{"class":190},"# the floor is actually tested\n",[10,851,852,853,855],{},"It looks pedantic and it catches the common drift: someone raises the floor in ",[26,854,32],{},"\nwithout touching the matrix, and the oldest supported version silently stops being verified.",[10,857,858,859,687],{},"Running your test suite on the floor version locally is worth doing before a release:\n",[26,860,861],{},"uv run --python 3.11 pytest",[14,863,865],{"id":864},"conclusion","Conclusion",[10,867,868],{},"Declare the range, test its ends, keep the local default separate from the support statement, and\ntreat raising the floor as the breaking change it is. Following upstream end-of-life dates turns\nwhat is otherwise a recurring debate into a rule you can point at.",[14,870,872],{"id":871},"frequently-asked-questions","Frequently asked questions",[874,875,877],"h3",{"id":876},"should-i-support-the-newest-python-immediately","Should I support the newest Python immediately?",[10,879,880],{},"Support it as soon as your dependencies do, which is usually a few weeks after release. Adding it to\nthe matrix early is cheap and catches deprecations before users do; requiring it is a different\nquestion and usually premature.",[874,882,884,885,887],{"id":883},"what-is-python-version-for-then","What is ",[26,886,46],{}," for, then?",[10,889,890],{},"A local convenience: uv and pyenv read it to pick an interpreter for the project. It says nothing\nabout what you support, and it should generally name a version in the middle of your range rather\nthan the floor — you want the fast path locally and the boundaries in CI.",[874,892,894],{"id":893},"how-many-versions-should-the-matrix-cover","How many versions should the matrix cover?",[10,896,897],{},"Two Python versions and two platforms is a good default: the ends of your range on Linux and\nWindows. Add macOS if you ship binaries or touch filesystem semantics; add intermediate Pythons only\nwhen a specific bug justifies it.",[874,899,901,902,904],{"id":900},"does-requires-python-affect-an-already-installed-tool","Does ",[26,903,28],{}," affect an already-installed tool?",[10,906,907],{},"No. It is consulted at install time, so a tool installed before you raised the floor keeps working.\nThat is why the run-time warning is worth adding — it is the only way an existing user learns about\nthe upcoming change.",[874,909,911],{"id":910},"what-if-a-dependency-drops-a-version-before-i-do","What if a dependency drops a version before I do?",[10,913,914,915,917],{},"Then your effective floor has moved whether you like it or not: the resolver will refuse to install\nyour tool with that dependency on the old interpreter. Raise ",[26,916,28],{}," to match, and treat\nit as the breaking change it is — pinning the dependency to an old version instead is a short-term\nanswer with a security cost.",[874,919,921],{"id":920},"can-i-support-a-version-my-ci-cannot-install","Can I support a version my CI cannot install?",[10,923,924],{},"You can declare it, and you should not. Support you cannot verify is a guess, and the first bug\nreport from that version arrives with no way to reproduce it. If a version is genuinely hard to run\nin CI — an old distribution Python, say — either find a container that provides it or narrow the\ndeclared range to match what you test.",[14,926,928],{"id":927},"related","Related",[19,930,931,938,945,950,957],{},[22,932,933,934],{},"Up: ",[690,935,937],{"href":936},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002F","Python CLI environment isolation best practices",[22,939,940,941],{},"Sideways: ",[690,942,944],{"href":943},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis\u002F","Managing virtual environments for cross-platform CLIs",[22,946,940,947],{},[690,948,949],{"href":692},"Exposing version info and build metadata",[22,951,952,953],{},"Related: ",[690,954,956],{"href":955},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002F","uv for Python CLI dependency management",[22,958,952,959],{},[690,960,962],{"href":961},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F","Building wheels and sdists for Python CLIs",[964,965,966],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":82,"searchDepth":103,"depth":103,"links":968},[969,970,971,972,973,974,975,976,977,978,979,989],{"id":16,"depth":103,"text":17},{"id":60,"depth":103,"text":61},{"id":70,"depth":103,"text":71},{"id":168,"depth":103,"text":169},{"id":298,"depth":103,"text":299},{"id":346,"depth":103,"text":347},{"id":568,"depth":103,"text":569},{"id":671,"depth":103,"text":672},{"id":706,"depth":103,"text":707},{"id":864,"depth":103,"text":865},{"id":871,"depth":103,"text":872,"children":980},[981,982,984,985,987,988],{"id":876,"depth":113,"text":877},{"id":883,"depth":113,"text":983},"What is .python-version for, then?",{"id":893,"depth":113,"text":894},{"id":900,"depth":113,"text":986},"Does requires-python affect an already-installed tool?",{"id":910,"depth":113,"text":911},{"id":920,"depth":113,"text":921},{"id":927,"depth":103,"text":928},"2026-08-01","Decide which Python versions a CLI supports - requires-python, .python-version, a CI matrix that matches, and when to drop an old interpreter.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli",{"title":5,"description":991},"project-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli\u002Findex",[546,28,1000,1001,1002],"ci","compatibility","uv","g46EHN5d1DoXVgubczmDUrgHx3zh3Z4lTEScqjAdqXQ",[1005,1008,1011,1014,1017,1020,1023,1026,1029,1032,1035,1038,1041,1044,1047,1050,1053,1056,1059,1062,1065,1068,1071,1074,1077,1080,1083,1086,1089,1092,1095,1098,1101,1104,1107,1110,1113,1116,1119,1122,1125,1128,1131,1134,1137,1140,1143,1146,1149,1152,1155,1158,1161,1164,1167,1170,1173,1176,1179,1182,1185,1188,1191,1194,1197,1200,1203,1206,1209,1212,1215,1218,1221,1224,1227,1230,1233,1236,1239,1242,1245,1248,1251],{"path":1006,"title":1007},"\u002Fabout","About Python CLI Toolcraft",{"path":1009,"title":1010},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1012,"title":1013},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1015,"title":1016},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1018,"title":1019},"\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":1021,"title":1022},"\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":1024,"title":1025},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1027,"title":1028},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1030,"title":1031},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1033,"title":1034},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1036,"title":1037},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1039,"title":1040},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1042,"title":1043},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1045,"title":1046},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1048,"title":1049},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1051,"title":1052},"\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":1054,"title":1055},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1057,"title":1058},"\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":1060,"title":1061},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1063,"title":1064},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1066,"title":1067},"\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":1069,"title":1070},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1072,"title":1073},"\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":1075,"title":1076},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1078,"title":1079},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1081,"title":1082},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1084,"title":1085},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1087,"title":1088},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1090,"title":1091},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1093,"title":1094},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1096,"title":1097},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1099,"title":1100},"\u002F","Python CLI Toolcraft",{"path":1102,"title":1103},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1105,"title":1106},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1108,"title":1109},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1111,"title":1112},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1114,"title":1115},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1117,"title":1118},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1120,"title":1121},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1123,"title":1124},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1126,"title":1127},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1129,"title":1130},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1132,"title":1133},"\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":1135,"title":1136},"\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":1138,"title":1139},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1141,"title":1142},"\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":1144,"title":1145},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1147,"title":1148},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1150,"title":1151},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1153,"title":1154},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1156,"title":1157},"\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":1159,"title":1160},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1162,"title":1163},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1165,"title":1166},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1168,"title":1169},"\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":1171,"title":1172},"\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":1174,"title":1175},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1177,"title":1178},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1180,"title":1181},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1183,"title":1184},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1186,"title":1187},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1189,"title":1190},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1192,"title":1193},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1195,"title":1196},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1198,"title":1199},"\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":1201,"title":1202},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1204,"title":1205},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1207,"title":1208},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1210,"title":1211},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1213,"title":1214},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1216,"title":1217},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1219,"title":1220},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1222,"title":1223},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1225,"title":1226},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1228,"title":1229},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1231,"title":1232},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1234,"title":1235},"\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":1237,"title":1238},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1240,"title":1241},"\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":1243,"title":1244},"\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":1246,"title":1247},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1249,"title":1250},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":996,"title":5},1785614690036]