[{"data":1,"prerenderedAt":2246},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project\u002F":3,"content-directory":1401},{"id":4,"title":5,"body":6,"date":1387,"description":1388,"difficulty":1389,"draft":1390,"extension":1391,"meta":1392,"navigation":468,"path":1393,"seo":1394,"stem":1395,"tags":1396,"updated":1387,"__hash__":1400},"content\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project\u002Findex.md","Bumping Versions Consistently Across a Python CLI Project",{"type":7,"value":8,"toc":1364},"minimark",[9,36,41,63,67,70,74,124,131,135,138,179,182,315,318,322,330,336,359,363,379,615,648,710,713,719,723,744,748,751,797,800,804,807,1236,1251,1255,1270,1274,1281,1293,1300,1307,1311,1318,1322,1328,1332,1360],[10,11,12,13,17,18,21,22,25,26,29,30,35],"p",{},"The version number of a CLI shows up in more places than anyone remembers: ",[14,15,16],"code",{},"pyproject.toml",", the lock file, a ",[14,19,20],{},"__version__"," constant someone added years ago, the changelog heading, a Homebrew formula, a Dockerfile label, the docs' installation snippet, perhaps a man page. Releases go wrong when these disagree — ",[14,23,24],{},"mytool --version"," says 1.5.2 while PyPI has 1.6.0, the formula still downloads last month's tarball, the changelog's newest heading is two releases old. This guide reduces the number of places a version is written by hand to one, automates the rest with ",[14,27,28],{},"bump-my-version",", and adds a test that fails the build when anything drifts. It belongs to the ",[31,32,34],"a",{"href":33},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","versioning and changelogs topic",".",[37,38,40],"h2",{"id":39},"prerequisites","Prerequisites",[42,43,44,51,60],"ul",{},[45,46,47,48,50],"li",{},"A CLI project with a ",[14,49,16],{},"; uv for the commands shown (other tools work the same way).",[45,52,53,55,56,59],{},[14,54,28],{},", run with ",[14,57,58],{},"uvx bump-my-version"," so it needs no project dependency.",[45,61,62],{},"Git, since the bump commits and tags.",[37,64,66],{"id":65},"fewer-copies-first","Fewer copies first",[10,68,69],{},"The best way to keep copies in sync is to not have them. Before automating anything, remove every copy that can be derived:",[71,72],"inline-diagram",{"name":73},"bump-sources",[42,75,76,92,108,114],{},[45,77,78,82,83,86,87,91],{},[79,80,81],"strong",{},"Inside the package, read the installed metadata"," rather than storing a constant: ",[14,84,85],{},"importlib.metadata.version(\"mytool\")"," always matches what is installed. ",[31,88,90],{"href":89},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata\u002F","Exposing version info and build metadata"," covers the details, including the fallback for running from a source checkout.",[45,93,94,99,100,103,104,107],{},[79,95,96,97,35],{},"Let the lock file follow ",[14,98,16],{}," ",[14,101,102],{},"uv lock"," updates the project's own entry; nobody should edit ",[14,105,106],{},"uv.lock"," by hand.",[45,109,110,113],{},[79,111,112],{},"Generate documentation snippets"," from the version at build time instead of hard-coding \"pip install mytool==1.5.2\".",[45,115,116,119,120,35],{},[79,117,118],{},"Or derive everything from git tags."," With hatch-vcs or setuptools-scm, the tag is the only place a version is written, and the build fills in the rest — see ",[31,121,123],{"href":122},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs\u002F","deriving versions from git tags with hatch-vcs",[10,125,126,127,130],{},"What remains are files that must contain a literal version and that you do not want to template: the changelog, a Homebrew formula or Scoop manifest kept in the repository, perhaps a ",[14,128,129],{},"CITATION.cff",". Those are what a bump tool is for.",[37,132,134],{"id":133},"finding-every-copy","Finding every copy",[10,136,137],{},"Before deciding what to automate, find out where the current version actually appears. A quick search for the literal string usually turns up surprises:",[139,140,145],"pre",{"className":141,"code":142,"language":143,"meta":144,"style":144},"language-bash shiki shiki-themes github-light github-dark","git grep -n --fixed-strings \"1.5.2\" -- ':!uv.lock' ':!CHANGELOG.md'\n","bash","",[14,146,147],{"__ignoreMap":144},[148,149,152,156,160,164,167,170,173,176],"span",{"class":150,"line":151},"line",1,[148,153,155],{"class":154},"sScJk","git",[148,157,159],{"class":158},"sZZnC"," grep",[148,161,163],{"class":162},"sj4cs"," -n",[148,165,166],{"class":162}," --fixed-strings",[148,168,169],{"class":158}," \"1.5.2\"",[148,171,172],{"class":162}," --",[148,174,175],{"class":158}," ':!uv.lock'",[148,177,178],{"class":158}," ':!CHANGELOG.md'\n",[10,180,181],{},"Typical results in a CLI repository, and what to do with each:",[183,184,185,201],"table",{},[186,187,188],"thead",{},[189,190,191,195,198],"tr",{},[192,193,194],"th",{},"Where",[192,196,197],{},"Typical content",[192,199,200],{},"Action",[202,203,204,219,237,252,267,283,298],"tbody",{},[189,205,206,211,216],{},[207,208,209],"td",{},[14,210,16],{},[207,212,213],{},[14,214,215],{},"version = \"1.5.2\"",[207,217,218],{},"the single source",[189,220,221,226,231],{},[207,222,223],{},[14,224,225],{},"src\u002Fmytool\u002F__init__.py",[207,227,228],{},[14,229,230],{},"__version__ = \"1.5.2\"",[207,232,233,234],{},"replace with ",[14,235,236],{},"importlib.metadata",[189,238,239,244,249],{},[207,240,241],{},[14,242,243],{},"README.md",[207,245,246],{},[14,247,248],{},"uv tool install mytool==1.5.2",[207,250,251],{},"drop the pin, or generate",[189,253,254,259,264],{},[207,255,256],{},[14,257,258],{},"Dockerfile",[207,260,261],{},[14,262,263],{},"LABEL version=\"1.5.2\"",[207,265,266],{},"pass as a build argument from CI",[189,268,269,274,280],{},[207,270,271],{},[14,272,273],{},"packaging\u002Fhomebrew\u002Fmytool.rb",[207,275,276,279],{},[14,277,278],{},"version \"1.5.2\""," plus a URL and checksum",[207,281,282],{},"bump tool, or update from CI after publishing",[189,284,285,290,295],{},[207,286,287],{},[14,288,289],{},"docs\u002Fconf.py",[207,291,292],{},[14,293,294],{},"release = \"1.5.2\"",[207,296,297],{},"read from metadata at build time",[189,299,300,305,312],{},[207,301,302],{},[14,303,304],{},"tests\u002F",[207,306,307,308,311],{},"expected ",[14,309,310],{},"--version"," output",[207,313,314],{},"compute from metadata in the test",[10,316,317],{},"Excluding the lock file and changelog keeps the output focused — those are expected to contain versions. Anything else that matches is either a copy to remove, a file for the bump tool, or a coincidence (a dependency that happens to share the number) that confirms why precise search patterns matter. Re-run the search occasionally; new copies creep in with new files.",[37,319,321],{"id":320},"the-recipe","The recipe",[323,324,326,327],"h3",{"id":325},"simple-case-uv-version","Simple case: ",[14,328,329],{},"uv version",[10,331,332,333,335],{},"If ",[14,334,16],{}," is the only file with a literal version, uv does the whole job:",[139,337,339],{"className":141,"code":338,"language":143,"meta":144,"style":144},"uv version --bump minor         # pyproject.toml and uv.lock, together\n",[14,340,341],{"__ignoreMap":144},[148,342,343,346,349,352,355],{"class":150,"line":151},[148,344,345],{"class":154},"uv",[148,347,348],{"class":158}," version",[148,350,351],{"class":162}," --bump",[148,353,354],{"class":158}," minor",[148,356,358],{"class":357},"sJ8bj","         # pyproject.toml and uv.lock, together\n",[323,360,362],{"id":361},"several-files-bump-my-version","Several files: bump-my-version",[10,364,365,367,368,371,372,375,376,378],{},[14,366,28],{}," (the maintained successor to ",[14,369,370],{},"bumpversion"," and ",[14,373,374],{},"bump2version",") reads its configuration from ",[14,377,16],{},", rewrites every listed file, and optionally commits and tags:",[139,380,384],{"className":381,"code":382,"language":383,"meta":144,"style":144},"language-toml shiki shiki-themes github-light github-dark","[tool.bumpversion]\ncurrent_version = \"1.5.2\"\ncommit = true\ntag = true\ntag_name = \"v{new_version}\"\nmessage = \"Release {new_version}\"\npre_commit_hooks = [\"uv lock\", \"git add uv.lock\"]\n\n[[tool.bumpversion.files]]\nfilename = \"pyproject.toml\"\nsearch = 'version = \"{current_version}\"'\nreplace = 'version = \"{new_version}\"'\n\n[[tool.bumpversion.files]]\nfilename = \"CHANGELOG.md\"\nsearch = \"## [Unreleased]\"\nreplace = \"## [Unreleased]\\n\\n## [{new_version}] - {now:%Y-%m-%d}\"\n\n[[tool.bumpversion.files]]\nfilename = \"packaging\u002Fhomebrew\u002Fmytool.rb\"\nsearch = 'version \"{current_version}\"'\nreplace = 'version \"{new_version}\"'\n","toml",[14,385,386,402,411,420,428,437,446,463,470,490,499,508,517,522,539,547,555,569,574,591,599,607],{"__ignoreMap":144},[148,387,388,392,395,397,399],{"class":150,"line":151},[148,389,391],{"class":390},"sVt8B","[",[148,393,394],{"class":154},"tool",[148,396,35],{"class":390},[148,398,370],{"class":154},[148,400,401],{"class":390},"]\n",[148,403,405,408],{"class":150,"line":404},2,[148,406,407],{"class":390},"current_version = ",[148,409,410],{"class":158},"\"1.5.2\"\n",[148,412,414,417],{"class":150,"line":413},3,[148,415,416],{"class":390},"commit = ",[148,418,419],{"class":162},"true\n",[148,421,423,426],{"class":150,"line":422},4,[148,424,425],{"class":390},"tag = ",[148,427,419],{"class":162},[148,429,431,434],{"class":150,"line":430},5,[148,432,433],{"class":390},"tag_name = ",[148,435,436],{"class":158},"\"v{new_version}\"\n",[148,438,440,443],{"class":150,"line":439},6,[148,441,442],{"class":390},"message = ",[148,444,445],{"class":158},"\"Release {new_version}\"\n",[148,447,449,452,455,458,461],{"class":150,"line":448},7,[148,450,451],{"class":390},"pre_commit_hooks = [",[148,453,454],{"class":158},"\"uv lock\"",[148,456,457],{"class":390},", ",[148,459,460],{"class":158},"\"git add uv.lock\"",[148,462,401],{"class":390},[148,464,466],{"class":150,"line":465},8,[148,467,469],{"emptyLinePlaceholder":468},true,"\n",[148,471,473,476,478,480,482,484,487],{"class":150,"line":472},9,[148,474,475],{"class":390},"[[",[148,477,394],{"class":154},[148,479,35],{"class":390},[148,481,370],{"class":154},[148,483,35],{"class":390},[148,485,486],{"class":154},"files",[148,488,489],{"class":390},"]]\n",[148,491,493,496],{"class":150,"line":492},10,[148,494,495],{"class":390},"filename = ",[148,497,498],{"class":158},"\"pyproject.toml\"\n",[148,500,502,505],{"class":150,"line":501},11,[148,503,504],{"class":390},"search = ",[148,506,507],{"class":158},"'version = \"{current_version}\"'\n",[148,509,511,514],{"class":150,"line":510},12,[148,512,513],{"class":390},"replace = ",[148,515,516],{"class":158},"'version = \"{new_version}\"'\n",[148,518,520],{"class":150,"line":519},13,[148,521,469],{"emptyLinePlaceholder":468},[148,523,525,527,529,531,533,535,537],{"class":150,"line":524},14,[148,526,475],{"class":390},[148,528,394],{"class":154},[148,530,35],{"class":390},[148,532,370],{"class":154},[148,534,35],{"class":390},[148,536,486],{"class":154},[148,538,489],{"class":390},[148,540,542,544],{"class":150,"line":541},15,[148,543,495],{"class":390},[148,545,546],{"class":158},"\"CHANGELOG.md\"\n",[148,548,550,552],{"class":150,"line":549},16,[148,551,504],{"class":390},[148,553,554],{"class":158},"\"## [Unreleased]\"\n",[148,556,558,560,563,566],{"class":150,"line":557},17,[148,559,513],{"class":390},[148,561,562],{"class":158},"\"## [Unreleased]",[148,564,565],{"class":162},"\\n\\n",[148,567,568],{"class":158},"## [{new_version}] - {now:%Y-%m-%d}\"\n",[148,570,572],{"class":150,"line":571},18,[148,573,469],{"emptyLinePlaceholder":468},[148,575,577,579,581,583,585,587,589],{"class":150,"line":576},19,[148,578,475],{"class":390},[148,580,394],{"class":154},[148,582,35],{"class":390},[148,584,370],{"class":154},[148,586,35],{"class":390},[148,588,486],{"class":154},[148,590,489],{"class":390},[148,592,594,596],{"class":150,"line":593},20,[148,595,495],{"class":390},[148,597,598],{"class":158},"\"packaging\u002Fhomebrew\u002Fmytool.rb\"\n",[148,600,602,604],{"class":150,"line":601},21,[148,603,504],{"class":390},[148,605,606],{"class":158},"'version \"{current_version}\"'\n",[148,608,610,612],{"class":150,"line":609},22,[148,611,513],{"class":390},[148,613,614],{"class":158},"'version \"{new_version}\"'\n",[10,616,617,618,621,622,624,625,628,629,632,633,635,636,640,641,644,645,647],{},"Each ",[14,619,620],{},"[[tool.bumpversion.files]]"," entry names a file and the exact text to find and replace. Searching for ",[14,623,215],{}," rather than the bare ",[14,626,627],{},"1.5.2"," matters: the bare number might also appear as a dependency pin, a date or an unrelated example, and a bump tool that replaces all of them silently corrupts files. The changelog entry turns the ",[14,630,631],{},"Unreleased"," heading into a dated release heading and opens a fresh ",[14,634,631],{}," section above it, which is the ",[31,637,639],{"href":638},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits\u002F","Keep a Changelog"," convention. The ",[14,642,643],{},"pre_commit_hooks"," run after the files are rewritten and before the commit, so ",[14,646,106],{}," is updated and included in the same commit.",[139,649,651],{"className":141,"code":650,"language":143,"meta":144,"style":144},"uvx bump-my-version show-bump          # preview the possible next versions\nuvx bump-my-version bump minor --dry-run -v\nuvx bump-my-version bump minor         # rewrite, re-lock, commit, tag\ngit push --follow-tags                 # CI publishes from the tag\n",[14,652,653,667,684,697],{"__ignoreMap":144},[148,654,655,658,661,664],{"class":150,"line":151},[148,656,657],{"class":154},"uvx",[148,659,660],{"class":158}," bump-my-version",[148,662,663],{"class":158}," show-bump",[148,665,666],{"class":357},"          # preview the possible next versions\n",[148,668,669,671,673,676,678,681],{"class":150,"line":404},[148,670,657],{"class":154},[148,672,660],{"class":158},[148,674,675],{"class":158}," bump",[148,677,354],{"class":158},[148,679,680],{"class":162}," --dry-run",[148,682,683],{"class":162}," -v\n",[148,685,686,688,690,692,694],{"class":150,"line":413},[148,687,657],{"class":154},[148,689,660],{"class":158},[148,691,675],{"class":158},[148,693,354],{"class":158},[148,695,696],{"class":357},"         # rewrite, re-lock, commit, tag\n",[148,698,699,701,704,707],{"class":150,"line":422},[148,700,155],{"class":154},[148,702,703],{"class":158}," push",[148,705,706],{"class":162}," --follow-tags",[148,708,709],{"class":357},"                 # CI publishes from the tag\n",[71,711],{"name":712},"bump-terminal",[10,714,715,716,35],{},"The result is one commit that changes every version-bearing file at once, and a tag pointing at it. Because CI publishes from the tag, the published artefacts and the repository can never disagree about which commit is ",[14,717,718],{},"1.6.0",[323,720,722],{"id":721},"pre-releases","Pre-releases",[10,724,725,727,728,731,732,735,736,738,739,743],{},[14,726,28],{},"'s default version pattern is ",[14,729,730],{},"major.minor.patch",". To bump into and out of release candidates, extend the parse and serialise patterns, or use ",[14,733,734],{},"uv version --bump rc"," for ",[14,737,16],{}," and let bump-my-version handle only the files that should change for final releases. ",[31,740,742],{"href":741},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates\u002F","Shipping pre-releases and release candidates"," describes the cycle; most projects only update the Homebrew formula and changelog heading for final releases anyway.",[37,745,747],{"id":746},"ux-considerations","UX considerations",[10,749,750],{},"The users of this process are maintainers who release a few times a month at most and forget the steps in between:",[42,752,753,766,772,781,787],{},[45,754,755,99,758,761,762,765],{},[79,756,757],{},"One command.",[14,759,760],{},"bump-my-version bump minor"," or a ",[14,763,764],{},"just release minor"," wrapper. Anything that needs a checklist will eventually be done wrong.",[45,767,768,771],{},[79,769,770],{},"Refuse a dirty tree."," bump-my-version does this by default when committing; keep it that way, so a release never includes unrelated edits.",[45,773,774,99,777,780],{},[79,775,776],{},"Preview before writing.",[14,778,779],{},"--dry-run -v"," prints every replacement it would make — the fastest way to debug a search pattern that does not match.",[45,782,783,786],{},[79,784,785],{},"Fail when a pattern is missing."," By default bump-my-version errors if a configured search string is not found in a file; do not turn that off, because it is how you learn that someone reformatted the Homebrew formula.",[45,788,789,796],{},[79,790,791,792,795],{},"Keep ",[14,793,794],{},"current_version"," honest."," It is the tool's only memory. If someone bumps by hand, the next automated bump fails to find its patterns, which is the right outcome — fix the config rather than forcing it.",[71,798],{"name":799},"bump-checklist",[37,801,803],{"id":802},"testing-the-behaviour","Testing the behaviour",[10,805,806],{},"A consistency test turns \"the versions agree\" into something CI checks on every commit, independent of how the bump was done:",[139,808,812],{"className":809,"code":810,"language":811,"meta":144,"style":144},"language-python shiki shiki-themes github-light github-dark","# tests\u002Ftest_version_consistency.py\nimport re\nimport tomllib\nfrom pathlib import Path\n\nROOT = Path(__file__).resolve().parents[1]\n\n\ndef project_version() -> str:\n    return tomllib.loads((ROOT \u002F \"pyproject.toml\").read_text())[\"project\"][\"version\"]\n\n\ndef test_bumpversion_config_matches_project():\n    data = tomllib.loads((ROOT \u002F \"pyproject.toml\").read_text())\n    assert data[\"tool\"][\"bumpversion\"][\"current_version\"] == project_version()\n\n\ndef test_lock_file_matches_project():\n    lock = tomllib.loads((ROOT \u002F \"uv.lock\").read_text())\n    entry = next(p for p in lock[\"package\"] if p[\"name\"] == \"mytool\")\n    assert entry[\"version\"] == project_version()\n\n\ndef test_changelog_has_heading_for_current_version():\n    text = (ROOT \u002F \"CHANGELOG.md\").read_text()\n    assert re.search(rf\"^## \\[{re.escape(project_version())}\\]\", text, re.M), \\\n        \"CHANGELOG.md has no heading for the current version\"\n\n\ndef test_homebrew_formula_matches_project():\n    formula = (ROOT \u002F \"packaging\u002Fhomebrew\u002Fmytool.rb\").read_text()\n    assert f'version \"{project_version()}\"' in formula\n","python",[14,813,814,819,828,835,848,852,874,878,882,899,929,933,937,947,966,996,1000,1004,1013,1031,1080,1095,1099,1104,1114,1135,1164,1170,1175,1180,1190,1209],{"__ignoreMap":144},[148,815,816],{"class":150,"line":151},[148,817,818],{"class":357},"# tests\u002Ftest_version_consistency.py\n",[148,820,821,825],{"class":150,"line":404},[148,822,824],{"class":823},"szBVR","import",[148,826,827],{"class":390}," re\n",[148,829,830,832],{"class":150,"line":413},[148,831,824],{"class":823},[148,833,834],{"class":390}," tomllib\n",[148,836,837,840,843,845],{"class":150,"line":422},[148,838,839],{"class":823},"from",[148,841,842],{"class":390}," pathlib ",[148,844,824],{"class":823},[148,846,847],{"class":390}," Path\n",[148,849,850],{"class":150,"line":430},[148,851,469],{"emptyLinePlaceholder":468},[148,853,854,857,860,863,866,869,872],{"class":150,"line":439},[148,855,856],{"class":162},"ROOT",[148,858,859],{"class":823}," =",[148,861,862],{"class":390}," Path(",[148,864,865],{"class":162},"__file__",[148,867,868],{"class":390},").resolve().parents[",[148,870,871],{"class":162},"1",[148,873,401],{"class":390},[148,875,876],{"class":150,"line":448},[148,877,469],{"emptyLinePlaceholder":468},[148,879,880],{"class":150,"line":465},[148,881,469],{"emptyLinePlaceholder":468},[148,883,884,887,890,893,896],{"class":150,"line":472},[148,885,886],{"class":823},"def",[148,888,889],{"class":154}," project_version",[148,891,892],{"class":390},"() -> ",[148,894,895],{"class":162},"str",[148,897,898],{"class":390},":\n",[148,900,901,904,907,909,912,915,918,921,924,927],{"class":150,"line":492},[148,902,903],{"class":823},"    return",[148,905,906],{"class":390}," tomllib.loads((",[148,908,856],{"class":162},[148,910,911],{"class":823}," \u002F",[148,913,914],{"class":158}," \"pyproject.toml\"",[148,916,917],{"class":390},").read_text())[",[148,919,920],{"class":158},"\"project\"",[148,922,923],{"class":390},"][",[148,925,926],{"class":158},"\"version\"",[148,928,401],{"class":390},[148,930,931],{"class":150,"line":501},[148,932,469],{"emptyLinePlaceholder":468},[148,934,935],{"class":150,"line":510},[148,936,469],{"emptyLinePlaceholder":468},[148,938,939,941,944],{"class":150,"line":519},[148,940,886],{"class":823},[148,942,943],{"class":154}," test_bumpversion_config_matches_project",[148,945,946],{"class":390},"():\n",[148,948,949,952,955,957,959,961,963],{"class":150,"line":524},[148,950,951],{"class":390},"    data ",[148,953,954],{"class":823},"=",[148,956,906],{"class":390},[148,958,856],{"class":162},[148,960,911],{"class":823},[148,962,914],{"class":158},[148,964,965],{"class":390},").read_text())\n",[148,967,968,971,974,977,979,982,984,987,990,993],{"class":150,"line":541},[148,969,970],{"class":823},"    assert",[148,972,973],{"class":390}," data[",[148,975,976],{"class":158},"\"tool\"",[148,978,923],{"class":390},[148,980,981],{"class":158},"\"bumpversion\"",[148,983,923],{"class":390},[148,985,986],{"class":158},"\"current_version\"",[148,988,989],{"class":390},"] ",[148,991,992],{"class":823},"==",[148,994,995],{"class":390}," project_version()\n",[148,997,998],{"class":150,"line":549},[148,999,469],{"emptyLinePlaceholder":468},[148,1001,1002],{"class":150,"line":557},[148,1003,469],{"emptyLinePlaceholder":468},[148,1005,1006,1008,1011],{"class":150,"line":571},[148,1007,886],{"class":823},[148,1009,1010],{"class":154}," test_lock_file_matches_project",[148,1012,946],{"class":390},[148,1014,1015,1018,1020,1022,1024,1026,1029],{"class":150,"line":576},[148,1016,1017],{"class":390},"    lock ",[148,1019,954],{"class":823},[148,1021,906],{"class":390},[148,1023,856],{"class":162},[148,1025,911],{"class":823},[148,1027,1028],{"class":158}," \"uv.lock\"",[148,1030,965],{"class":390},[148,1032,1033,1036,1038,1041,1044,1047,1050,1053,1056,1059,1061,1064,1067,1070,1072,1074,1077],{"class":150,"line":593},[148,1034,1035],{"class":390},"    entry ",[148,1037,954],{"class":823},[148,1039,1040],{"class":162}," next",[148,1042,1043],{"class":390},"(p ",[148,1045,1046],{"class":823},"for",[148,1048,1049],{"class":390}," p ",[148,1051,1052],{"class":823},"in",[148,1054,1055],{"class":390}," lock[",[148,1057,1058],{"class":158},"\"package\"",[148,1060,989],{"class":390},[148,1062,1063],{"class":823},"if",[148,1065,1066],{"class":390}," p[",[148,1068,1069],{"class":158},"\"name\"",[148,1071,989],{"class":390},[148,1073,992],{"class":823},[148,1075,1076],{"class":158}," \"mytool\"",[148,1078,1079],{"class":390},")\n",[148,1081,1082,1084,1087,1089,1091,1093],{"class":150,"line":601},[148,1083,970],{"class":823},[148,1085,1086],{"class":390}," entry[",[148,1088,926],{"class":158},[148,1090,989],{"class":390},[148,1092,992],{"class":823},[148,1094,995],{"class":390},[148,1096,1097],{"class":150,"line":609},[148,1098,469],{"emptyLinePlaceholder":468},[148,1100,1102],{"class":150,"line":1101},23,[148,1103,469],{"emptyLinePlaceholder":468},[148,1105,1107,1109,1112],{"class":150,"line":1106},24,[148,1108,886],{"class":823},[148,1110,1111],{"class":154}," test_changelog_has_heading_for_current_version",[148,1113,946],{"class":390},[148,1115,1117,1120,1122,1125,1127,1129,1132],{"class":150,"line":1116},25,[148,1118,1119],{"class":390},"    text ",[148,1121,954],{"class":823},[148,1123,1124],{"class":390}," (",[148,1126,856],{"class":162},[148,1128,911],{"class":823},[148,1130,1131],{"class":158}," \"CHANGELOG.md\"",[148,1133,1134],{"class":390},").read_text()\n",[148,1136,1138,1140,1143,1146,1149,1152,1155,1158,1161],{"class":150,"line":1137},26,[148,1139,970],{"class":823},[148,1141,1142],{"class":390}," re.search(",[148,1144,1145],{"class":823},"rf",[148,1147,1148],{"class":158},"\"^## \\[",[148,1150,1151],{"class":162},"{",[148,1153,1154],{"class":390},"re.escape(project_version())",[148,1156,1157],{"class":162},"}",[148,1159,1160],{"class":158},"\\]\"",[148,1162,1163],{"class":390},", text, re.M), \\\n",[148,1165,1167],{"class":150,"line":1166},27,[148,1168,1169],{"class":158},"        \"CHANGELOG.md has no heading for the current version\"\n",[148,1171,1173],{"class":150,"line":1172},28,[148,1174,469],{"emptyLinePlaceholder":468},[148,1176,1178],{"class":150,"line":1177},29,[148,1179,469],{"emptyLinePlaceholder":468},[148,1181,1183,1185,1188],{"class":150,"line":1182},30,[148,1184,886],{"class":823},[148,1186,1187],{"class":154}," test_homebrew_formula_matches_project",[148,1189,946],{"class":390},[148,1191,1193,1196,1198,1200,1202,1204,1207],{"class":150,"line":1192},31,[148,1194,1195],{"class":390},"    formula ",[148,1197,954],{"class":823},[148,1199,1124],{"class":390},[148,1201,856],{"class":162},[148,1203,911],{"class":823},[148,1205,1206],{"class":158}," \"packaging\u002Fhomebrew\u002Fmytool.rb\"",[148,1208,1134],{"class":390},[148,1210,1212,1214,1217,1220,1222,1225,1227,1230,1233],{"class":150,"line":1211},32,[148,1213,970],{"class":823},[148,1215,1216],{"class":823}," f",[148,1218,1219],{"class":158},"'version \"",[148,1221,1151],{"class":162},[148,1223,1224],{"class":390},"project_version()",[148,1226,1157],{"class":162},[148,1228,1229],{"class":158},"\"'",[148,1231,1232],{"class":823}," in",[148,1234,1235],{"class":390}," formula\n",[10,1237,1238,1239,1242,1243,1247,1248,1250],{},"During development between releases, the changelog test fails if the newest heading is still ",[14,1240,1241],{},"[Unreleased]"," and the version was bumped without a release — which is precisely the inconsistency you want to know about. Add one more check to the ",[31,1244,1246],{"href":1245},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F","built-wheel smoke test",": that ",[14,1249,24],{}," prints the same number.",[37,1252,1254],{"id":1253},"conclusion","Conclusion",[10,1256,1257,1258,1260,1261,1263,1264,1266,1267,1269],{},"Version drift is a symptom of too many hand-maintained copies. Remove the ones you can derive — read installed metadata in code, let ",[14,1259,102],{}," follow ",[14,1262,16],{},", generate docs snippets — then let ",[14,1265,329],{}," or ",[14,1268,28],{}," rewrite the rest in one commit with a tag, using precise search patterns and a pre-commit hook for the lock file. A small consistency test makes sure that whichever way a bump happens, CI notices when the numbers disagree.",[37,1271,1273],{"id":1272},"frequently-asked-questions","Frequently asked questions",[323,1275,1277,1278,1280],{"id":1276},"is-bump-my-version-better-than-uv-version","Is bump-my-version better than ",[14,1279,329],{},"?",[10,1282,1283,1284,1286,1287,1289,1290,1292],{},"They solve different sizes of problem. ",[14,1285,329],{}," updates ",[14,1288,16],{}," and the lock file and understands PEP 440 pre-release segments. bump-my-version updates any number of files with search-and-replace rules and can commit and tag. Many projects use ",[14,1291,329],{}," until a second file needs a literal version.",[323,1294,1296,1297,1299],{"id":1295},"should-__version__-still-exist","Should ",[14,1298,20],{}," still exist?",[10,1301,1302,1303,1306],{},"Only if something depends on it. If you keep it, set it from metadata — ",[14,1304,1305],{},"__version__ = importlib.metadata.version(\"mytool\")"," — rather than as a literal, so it can never drift.",[323,1308,1310],{"id":1309},"what-about-version-numbers-in-documentation","What about version numbers in documentation?",[10,1312,1313,1314,1317],{},"Prefer phrasing that does not need them (\"install the latest release with ",[14,1315,1316],{},"uv tool install mytool","\"). Where a version is necessary, have the docs build read it from the package metadata or the latest git tag.",[323,1319,1321],{"id":1320},"can-ci-do-the-bump-instead-of-a-maintainer","Can CI do the bump instead of a maintainer?",[10,1323,1324,1325,1327],{},"Yes — a manually triggered workflow can run ",[14,1326,760],{}," and push the commit and tag, which then trigger the publish workflow. Use a dedicated token with permission to push to the protected branch, and keep the publish job's protections as they are.",[37,1329,1331],{"id":1330},"related","Related",[42,1333,1334,1340,1344,1348,1354],{},[45,1335,1336,1337],{},"Up: ",[31,1338,1339],{"href":33},"Managing CLI versioning and changelogs",[45,1341,1342],{},[31,1343,742],{"href":741},[45,1345,1346],{},[31,1347,90],{"href":89},[45,1349,1350],{},[31,1351,1353],{"href":1352},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv\u002F","Building and publishing a CLI with uv",[45,1355,1356],{},[31,1357,1359],{"href":1358},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis\u002F","Homebrew and Scoop packaging for Python CLIs",[1361,1362,1363],"style",{},"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 pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":144,"searchDepth":404,"depth":404,"links":1365},[1366,1367,1368,1369,1375,1376,1377,1378,1386],{"id":39,"depth":404,"text":40},{"id":65,"depth":404,"text":66},{"id":133,"depth":404,"text":134},{"id":320,"depth":404,"text":321,"children":1370},[1371,1373,1374],{"id":325,"depth":413,"text":1372},"Simple case: uv version",{"id":361,"depth":413,"text":362},{"id":721,"depth":413,"text":722},{"id":746,"depth":404,"text":747},{"id":802,"depth":404,"text":803},{"id":1253,"depth":404,"text":1254},{"id":1272,"depth":404,"text":1273,"children":1379},[1380,1382,1384,1385],{"id":1276,"depth":413,"text":1381},"Is bump-my-version better than uv version?",{"id":1295,"depth":413,"text":1383},"Should __version__ still exist?",{"id":1309,"depth":413,"text":1310},{"id":1320,"depth":413,"text":1321},{"id":1330,"depth":404,"text":1331},"2026-10-02","Keep a CLI’s version in sync across pyproject.toml, uv.lock, the changelog and packaging files with one source of truth, bump-my-version and a consistency test.","beginner",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project",{"title":5,"description":1388},"project-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project\u002Findex",[1397,28,345,1398,1399],"versioning","changelog","releases","VBUZSD-kDQvruyb1QzbnDRsUqsGiOi-jXu4dbg_hN-M",[1402,1405,1408,1411,1414,1417,1420,1423,1426,1429,1432,1435,1438,1441,1444,1447,1450,1453,1456,1459,1462,1465,1468,1471,1474,1477,1480,1483,1486,1489,1492,1495,1498,1501,1504,1507,1510,1513,1516,1519,1522,1525,1528,1531,1534,1537,1540,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,1819,1822,1825,1828,1831,1834,1837,1840,1843,1846,1849,1852,1855,1858,1861,1864,1867,1870,1873,1876,1879,1882,1885,1888,1891,1894,1897,1900,1903,1906,1909,1912,1915,1918,1921,1924,1927,1930,1933,1936,1939,1942,1945,1948,1951,1954,1957,1960,1963,1966,1969,1972,1975,1978,1981,1984,1987,1990,1993,1996,1999,2002,2005,2008,2011,2014,2017,2020,2023,2026,2029,2032,2035,2038,2041,2044,2047,2050,2053,2056,2059,2062,2065,2068,2071,2074,2077,2080,2083,2086,2089,2092,2095,2098,2101,2104,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2165,2168,2171,2174,2177,2180,2183,2186,2189,2192,2195,2198,2201,2204,2207,2210,2213,2216,2219,2222,2225,2228,2231,2234,2237,2240,2243],{"path":1403,"title":1404},"\u002Fabout","About Python CLI Toolcraft",{"path":1406,"title":1407},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1409,"title":1410},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1412,"title":1413},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":1415,"title":1416},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1418,"title":1419},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1421,"title":1422},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":1424,"title":1425},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1427,"title":1428},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":1430,"title":1431},"\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":1433,"title":1434},"\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":1436,"title":1437},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1439,"title":1440},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":1442,"title":1443},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":1445,"title":1446},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1448,"title":1449},"\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":1451,"title":1452},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":1454,"title":1455},"\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":1457,"title":1458},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1460,"title":1461},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":1463,"title":1464},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1466,"title":1467},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1469,"title":1470},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1472,"title":1473},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1475,"title":1476},"\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":1478,"title":1479},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":1481,"title":1482},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1484,"title":1485},"\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":1487,"title":1488},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":1490,"title":1491},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1493,"title":1494},"\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":1496,"title":1497},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":1499,"title":1500},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1502,"title":1503},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1505,"title":1506},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1508,"title":1509},"\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":1511,"title":1512},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":1514,"title":1515},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1517,"title":1518},"\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":1520,"title":1521},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1523,"title":1524},"\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":1526,"title":1527},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1529,"title":1530},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1532,"title":1533},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":1535,"title":1536},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":1538,"title":1539},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1541,"title":1542},"\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":1544,"title":1545},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1547,"title":1548},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":1550,"title":1551},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1553,"title":1554},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1556,"title":1557},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":1559,"title":1560},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1562,"title":1563},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1565,"title":1566},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":1568,"title":1569},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":1571,"title":1572},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":1574,"title":1575},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":1577,"title":1578},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":1580,"title":1581},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":1583,"title":1584},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":1586,"title":1587},"\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":1589,"title":1590},"\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":1592,"title":1593},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1595,"title":1596},"\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":1598,"title":1599},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":1601,"title":1602},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1604,"title":1605},"\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":1607,"title":1608},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1610,"title":1611},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1613,"title":1614},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":1616,"title":1617},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":1619,"title":1620},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1622,"title":1623},"\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":1625,"title":1626},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1628,"title":1629},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1631,"title":1632},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1634,"title":1635},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1637,"title":1638},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":1640,"title":1641},"\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":1643,"title":1644},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1646,"title":1647},"\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":1649,"title":1650},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1652,"title":1653},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1655,"title":1656},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":1658,"title":1659},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1661,"title":1662},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1664,"title":1665},"\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":1667,"title":1668},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":1670,"title":1671},"\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":1673,"title":1674},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1676,"title":1677},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1679,"title":1680},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1682,"title":1683},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1685,"title":1686},"\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":1688,"title":1689},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":1691,"title":1692},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":1694,"title":1695},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1697,"title":1698},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1700,"title":1701},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":1703,"title":1704},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1706,"title":1707},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1709,"title":1710},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1712,"title":1713},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":1715,"title":1716},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1718,"title":1719},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1721,"title":1722},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":1724,"title":1725},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":1727,"title":1728},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":1730,"title":1731},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":1733,"title":1734},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":1736,"title":1737},"\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":1739,"title":1740},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1742,"title":1743},"\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":1745,"title":1746},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1748,"title":1749},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":1751,"title":1752},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":1754,"title":1755},"\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":1757,"title":1758},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":1760,"title":1761},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1763,"title":1764},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1766,"title":1767},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1769,"title":1770},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1772,"title":1773},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":1775,"title":1776},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":1778,"title":1779},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1781,"title":1782},"\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":1784,"title":1785},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1787,"title":1788},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1790,"title":1791},"\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":1793,"title":1794},"\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":1796,"title":1797},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":1799,"title":1800},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1802,"title":1803},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1805,"title":1806},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":1808,"title":1809},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":1811,"title":1812},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":1814,"title":1815},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":1817,"title":1818},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":1820,"title":1821},"\u002F","Python CLI Toolcraft",{"path":1823,"title":1824},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":1826,"title":1827},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":1829,"title":1830},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":1832,"title":1833},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":1835,"title":1836},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":1838,"title":1839},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":1841,"title":1842},"\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":1844,"title":1845},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":1847,"title":1848},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1850,"title":1851},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1853,"title":1854},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1856,"title":1857},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1859,"title":1860},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1862,"title":1863},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1865,"title":1866},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":1868,"title":1869},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1871,"title":1872},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1874,"title":1875},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1877,"title":1878},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":1880,"title":1881},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1883,"title":1884},"\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":1886,"title":1887},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":1889,"title":1890},"\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":1892,"title":1893},"\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":1895,"title":1896},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1898,"title":1899},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1901,"title":1902},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":1904,"title":1905},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1907,"title":1908},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1910,"title":1911},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1913,"title":1914},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1916,"title":1917},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":1919,"title":1920},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":1922,"title":1923},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1925,"title":1926},"\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":1928,"title":1929},"\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":1931,"title":1932},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1934,"title":1935},"\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":1937,"title":1938},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1940,"title":1941},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":1943,"title":1944},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":1946,"title":1947},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1949,"title":1950},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1952,"title":1953},"\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":1955,"title":1956},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1958,"title":1959},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1961,"title":1962},"\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":1964,"title":1965},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1967,"title":1968},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1970,"title":1971},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1973,"title":1974},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":1976,"title":1977},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1979,"title":1980},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":1982,"title":1983},"\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":1985,"title":1986},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1988,"title":1989},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":1991,"title":1992},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":1994,"title":1995},"\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":1997,"title":1998},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2000,"title":2001},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":2003,"title":2004},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2006,"title":2007},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2009,"title":2010},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2012,"title":2013},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2015,"title":2016},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2018,"title":2019},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":2021,"title":2022},"\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":2024,"title":2025},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":2027,"title":2028},"\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":2030,"title":2031},"\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":2033,"title":2034},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2036,"title":2037},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2039,"title":2040},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2042,"title":2043},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2045,"title":2046},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":2048,"title":2049},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":2051,"title":2052},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":2054,"title":2055},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2057,"title":2058},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2060,"title":2061},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":2063,"title":2064},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2066,"title":2067},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2069,"title":2070},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2072,"title":2073},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":2075,"title":2076},"\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":2078,"title":2079},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2081,"title":2082},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2084,"title":2085},"\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":2087,"title":2088},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":2090,"title":2091},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2093,"title":2094},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":2096,"title":2097},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":2099,"title":2100},"\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":2102,"title":2103},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1393,"title":5},{"path":2106,"title":2107},"\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":2109,"title":2110},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2112,"title":2113},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2115,"title":2116},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2118,"title":2119},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":2121,"title":2122},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2124,"title":2125},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2127,"title":2128},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":2130,"title":2131},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2133,"title":2134},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2136,"title":2137},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":2139,"title":2140},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2142,"title":2143},"\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":2145,"title":2146},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":2148,"title":2149},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2151,"title":2152},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":2154,"title":2155},"\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":2157,"title":2158},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2160,"title":2161},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2163,"title":2164},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":2166,"title":2167},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2169,"title":2170},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":2172,"title":2173},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":2175,"title":2176},"\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":2178,"title":2179},"\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":2181,"title":2182},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":2184,"title":2185},"\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":2187,"title":2188},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":2190,"title":2191},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":2193,"title":2194},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":2196,"title":2197},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":2199,"title":2200},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":2202,"title":2203},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":2205,"title":2206},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2208,"title":2209},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":2211,"title":2212},"\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":2214,"title":2215},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":2217,"title":2218},"\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":2220,"title":2221},"\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":2223,"title":2224},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2226,"title":2227},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":2229,"title":2230},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":2232,"title":2233},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2235,"title":2236},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":2238,"title":2239},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2241,"title":2242},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2244,"title":2245},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967542620]