[{"data":1,"prerenderedAt":2167},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs\u002F":3,"content-directory":1620},{"id":4,"title":5,"body":6,"date":1606,"description":1607,"difficulty":1608,"draft":1609,"extension":1610,"meta":1611,"navigation":185,"path":1612,"seo":1613,"stem":1614,"tags":1615,"updated":1606,"__hash__":1619},"content\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs\u002Findex.md","Deriving CLI Versions from Git Tags with hatch-vcs",{"type":7,"value":8,"toc":1585},"minimark",[9,48,53,81,85,89,99,103,330,348,351,632,645,649,661,664,703,713,717,731,770,784,800,828,832,871,874,878,884,1424,1427,1431,1444,1448,1453,1456,1463,1471,1475,1482,1486,1512,1520,1531,1535,1547,1551,1581],[10,11,12,13,17,18,21,22,24,25,28,29,32,33,37,38,41,42,47],"p",{},"A version number that lives in ",[14,15,16],"code",{},"pyproject.toml"," has to be edited by hand before every release, and sooner or later someone tags ",[14,19,20],{},"v1.6.0"," on a commit whose ",[14,23,16],{}," still says ",[14,26,27],{},"1.5.0",". The package on PyPI then disagrees with the tag, ",[14,30,31],{},"mytool --version"," prints the wrong thing, and because PyPI never allows a version to be re-uploaded, the fix is another release. Deriving the version from the git tag removes the second copy entirely: the tag ",[34,35,36],"em",{},"is"," the version, read by the build backend at build time and by your CLI at runtime from the installed metadata. This guide sets that up with ",[14,39,40],{},"hatch-vcs",", explains the development versions it produces between releases, and covers the three places it commonly goes wrong — shallow CI clones, source distributions and editable installs. It belongs to the ",[43,44,46],"a",{"href":45},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning and changelogs topic",".",[49,50,52],"h2",{"id":51},"prerequisites","Prerequisites",[54,55,56,72,78],"ul",{},[57,58,59,60,63,64,67,68,71],"li",{},"A CLI built with the ",[14,61,62],{},"hatchling"," backend (uv's default for ",[14,65,66],{},"uv init --package","). Projects on setuptools can use ",[14,69,70],{},"setuptools-scm"," with nearly identical behaviour.",[57,73,74,75,47],{},"Releases marked with annotated git tags such as ",[14,76,77],{},"v1.5.0",[57,79,80],{},"git available wherever the package is built.",[49,82,84],{"id":83},"how-the-version-flows","How the version flows",[86,87],"inline-diagram",{"name":88},"vcs-flow",[10,90,91,92,94,95,98],{},"At build time, ",[14,93,40],{}," asks git to describe the current commit relative to the most recent tag, turns the answer into a PEP 440 version, and hands it to hatchling, which writes it into the wheel's metadata. At runtime your CLI reads the version back from that metadata with ",[14,96,97],{},"importlib.metadata",". Nothing in the repository contains the version number — only the tag.",[49,100,102],{"id":101},"the-recipe","The recipe",[104,105,110],"pre",{"className":106,"code":107,"language":108,"meta":109,"style":109},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[project]\nname = \"mytool\"\ndynamic = [\"version\"]              # no version = \"...\" line\nrequires-python = \">=3.10\"\ndependencies = [\"typer>=0.12\"]\n\n[project.scripts]\nmytool = \"mytool.cli:app\"\n\n[build-system]\nrequires = [\"hatchling\", \"hatch-vcs\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.version]\nsource = \"vcs\"\n\n[tool.hatch.build.hooks.vcs]\nversion-file = \"src\u002Fmytool\u002F_version.py\"   # optional; see below\n","toml","",[14,111,112,121,135,145,160,169,180,187,201,210,215,225,242,251,256,276,285,290,318],{"__ignoreMap":109},[113,114,117],"span",{"class":115,"line":116},"line",1,[113,118,120],{"class":119},"sJ8bj","# pyproject.toml\n",[113,122,124,128,132],{"class":115,"line":123},2,[113,125,127],{"class":126},"sVt8B","[",[113,129,131],{"class":130},"sScJk","project",[113,133,134],{"class":126},"]\n",[113,136,138,141],{"class":115,"line":137},3,[113,139,140],{"class":126},"name = ",[113,142,144],{"class":143},"sZZnC","\"mytool\"\n",[113,146,148,151,154,157],{"class":115,"line":147},4,[113,149,150],{"class":126},"dynamic = [",[113,152,153],{"class":143},"\"version\"",[113,155,156],{"class":126},"]              ",[113,158,159],{"class":119},"# no version = \"...\" line\n",[113,161,163,166],{"class":115,"line":162},5,[113,164,165],{"class":126},"requires-python = ",[113,167,168],{"class":143},"\">=3.10\"\n",[113,170,172,175,178],{"class":115,"line":171},6,[113,173,174],{"class":126},"dependencies = [",[113,176,177],{"class":143},"\"typer>=0.12\"",[113,179,134],{"class":126},[113,181,183],{"class":115,"line":182},7,[113,184,186],{"emptyLinePlaceholder":185},true,"\n",[113,188,190,192,194,196,199],{"class":115,"line":189},8,[113,191,127],{"class":126},[113,193,131],{"class":130},[113,195,47],{"class":126},[113,197,198],{"class":130},"scripts",[113,200,134],{"class":126},[113,202,204,207],{"class":115,"line":203},9,[113,205,206],{"class":126},"mytool = ",[113,208,209],{"class":143},"\"mytool.cli:app\"\n",[113,211,213],{"class":115,"line":212},10,[113,214,186],{"emptyLinePlaceholder":185},[113,216,218,220,223],{"class":115,"line":217},11,[113,219,127],{"class":126},[113,221,222],{"class":130},"build-system",[113,224,134],{"class":126},[113,226,228,231,234,237,240],{"class":115,"line":227},12,[113,229,230],{"class":126},"requires = [",[113,232,233],{"class":143},"\"hatchling\"",[113,235,236],{"class":126},", ",[113,238,239],{"class":143},"\"hatch-vcs\"",[113,241,134],{"class":126},[113,243,245,248],{"class":115,"line":244},13,[113,246,247],{"class":126},"build-backend = ",[113,249,250],{"class":143},"\"hatchling.build\"\n",[113,252,254],{"class":115,"line":253},14,[113,255,186],{"emptyLinePlaceholder":185},[113,257,259,261,264,266,269,271,274],{"class":115,"line":258},15,[113,260,127],{"class":126},[113,262,263],{"class":130},"tool",[113,265,47],{"class":126},[113,267,268],{"class":130},"hatch",[113,270,47],{"class":126},[113,272,273],{"class":130},"version",[113,275,134],{"class":126},[113,277,279,282],{"class":115,"line":278},16,[113,280,281],{"class":126},"source = ",[113,283,284],{"class":143},"\"vcs\"\n",[113,286,288],{"class":115,"line":287},17,[113,289,186],{"emptyLinePlaceholder":185},[113,291,293,295,297,299,301,303,306,308,311,313,316],{"class":115,"line":292},18,[113,294,127],{"class":126},[113,296,263],{"class":130},[113,298,47],{"class":126},[113,300,268],{"class":130},[113,302,47],{"class":126},[113,304,305],{"class":130},"build",[113,307,47],{"class":126},[113,309,310],{"class":130},"hooks",[113,312,47],{"class":126},[113,314,315],{"class":130},"vcs",[113,317,134],{"class":126},[113,319,321,324,327],{"class":115,"line":320},19,[113,322,323],{"class":126},"version-file = ",[113,325,326],{"class":143},"\"src\u002Fmytool\u002F_version.py\"",[113,328,329],{"class":119},"   # optional; see below\n",[10,331,332,335,336,339,340,343,344,347],{},[14,333,334],{},"dynamic = [\"version\"]"," tells build tools that the version is computed. ",[14,337,338],{},"source = \"vcs\""," selects hatch-vcs. The optional build hook writes a ",[14,341,342],{},"_version.py"," module into the package at build time — add it to ",[14,345,346],{},".gitignore",", since it is generated.",[10,349,350],{},"At runtime, read the version from the installed distribution:",[104,352,356],{"className":353,"code":354,"language":355,"meta":109,"style":109},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fcli.py\nfrom importlib.metadata import PackageNotFoundError, version\nfrom typing import Annotated\n\nimport typer\n\ntry:\n    __version__ = version(\"mytool\")\nexcept PackageNotFoundError:          # running from a checkout that was never installed\n    __version__ = \"0+unknown\"\n\napp = typer.Typer()\n\n\ndef _show_version(value: bool) -> None:\n    if value:\n        typer.echo(f\"mytool {__version__}\")\n        raise typer.Exit()\n\n\n@app.callback()\ndef main(\n    _: Annotated[bool, typer.Option(\"--version\", is_eager=True, callback=_show_version,\n                                    help=\"Show the version and exit.\")] = False,\n) -> None:\n    \"\"\"mytool — deployment helpers.\"\"\"\n","python",[14,357,358,363,378,390,394,401,405,413,431,442,451,455,466,470,474,496,504,523,531,535,540,549,560,595,617,626],{"__ignoreMap":109},[113,359,360],{"class":115,"line":116},[113,361,362],{"class":119},"# src\u002Fmytool\u002Fcli.py\n",[113,364,365,369,372,375],{"class":115,"line":123},[113,366,368],{"class":367},"szBVR","from",[113,370,371],{"class":126}," importlib.metadata ",[113,373,374],{"class":367},"import",[113,376,377],{"class":126}," PackageNotFoundError, version\n",[113,379,380,382,385,387],{"class":115,"line":137},[113,381,368],{"class":367},[113,383,384],{"class":126}," typing ",[113,386,374],{"class":367},[113,388,389],{"class":126}," Annotated\n",[113,391,392],{"class":115,"line":147},[113,393,186],{"emptyLinePlaceholder":185},[113,395,396,398],{"class":115,"line":162},[113,397,374],{"class":367},[113,399,400],{"class":126}," typer\n",[113,402,403],{"class":115,"line":171},[113,404,186],{"emptyLinePlaceholder":185},[113,406,407,410],{"class":115,"line":182},[113,408,409],{"class":367},"try",[113,411,412],{"class":126},":\n",[113,414,415,419,422,425,428],{"class":115,"line":189},[113,416,418],{"class":417},"sj4cs","    __version__",[113,420,421],{"class":367}," =",[113,423,424],{"class":126}," version(",[113,426,427],{"class":143},"\"mytool\"",[113,429,430],{"class":126},")\n",[113,432,433,436,439],{"class":115,"line":203},[113,434,435],{"class":367},"except",[113,437,438],{"class":126}," PackageNotFoundError:          ",[113,440,441],{"class":119},"# running from a checkout that was never installed\n",[113,443,444,446,448],{"class":115,"line":212},[113,445,418],{"class":417},[113,447,421],{"class":367},[113,449,450],{"class":143}," \"0+unknown\"\n",[113,452,453],{"class":115,"line":217},[113,454,186],{"emptyLinePlaceholder":185},[113,456,457,460,463],{"class":115,"line":227},[113,458,459],{"class":126},"app ",[113,461,462],{"class":367},"=",[113,464,465],{"class":126}," typer.Typer()\n",[113,467,468],{"class":115,"line":244},[113,469,186],{"emptyLinePlaceholder":185},[113,471,472],{"class":115,"line":253},[113,473,186],{"emptyLinePlaceholder":185},[113,475,476,479,482,485,488,491,494],{"class":115,"line":258},[113,477,478],{"class":367},"def",[113,480,481],{"class":130}," _show_version",[113,483,484],{"class":126},"(value: ",[113,486,487],{"class":417},"bool",[113,489,490],{"class":126},") -> ",[113,492,493],{"class":417},"None",[113,495,412],{"class":126},[113,497,498,501],{"class":115,"line":278},[113,499,500],{"class":367},"    if",[113,502,503],{"class":126}," value:\n",[113,505,506,509,512,515,518,521],{"class":115,"line":287},[113,507,508],{"class":126},"        typer.echo(",[113,510,511],{"class":367},"f",[113,513,514],{"class":143},"\"mytool ",[113,516,517],{"class":417},"{__version__}",[113,519,520],{"class":143},"\"",[113,522,430],{"class":126},[113,524,525,528],{"class":115,"line":292},[113,526,527],{"class":367},"        raise",[113,529,530],{"class":126}," typer.Exit()\n",[113,532,533],{"class":115,"line":320},[113,534,186],{"emptyLinePlaceholder":185},[113,536,538],{"class":115,"line":537},20,[113,539,186],{"emptyLinePlaceholder":185},[113,541,543,546],{"class":115,"line":542},21,[113,544,545],{"class":130},"@app.callback",[113,547,548],{"class":126},"()\n",[113,550,552,554,557],{"class":115,"line":551},22,[113,553,478],{"class":367},[113,555,556],{"class":130}," main",[113,558,559],{"class":126},"(\n",[113,561,563,566,568,571,574,576,580,582,585,587,590,592],{"class":115,"line":562},23,[113,564,565],{"class":126},"    _: Annotated[",[113,567,487],{"class":417},[113,569,570],{"class":126},", typer.Option(",[113,572,573],{"class":143},"\"--version\"",[113,575,236],{"class":126},[113,577,579],{"class":578},"s4XuR","is_eager",[113,581,462],{"class":367},[113,583,584],{"class":417},"True",[113,586,236],{"class":126},[113,588,589],{"class":578},"callback",[113,591,462],{"class":367},[113,593,594],{"class":126},"_show_version,\n",[113,596,598,601,603,606,609,611,614],{"class":115,"line":597},24,[113,599,600],{"class":578},"                                    help",[113,602,462],{"class":367},[113,604,605],{"class":143},"\"Show the version and exit.\"",[113,607,608],{"class":126},")] ",[113,610,462],{"class":367},[113,612,613],{"class":417}," False",[113,615,616],{"class":126},",\n",[113,618,620,622,624],{"class":115,"line":619},25,[113,621,490],{"class":126},[113,623,493],{"class":417},[113,625,412],{"class":126},[113,627,629],{"class":115,"line":628},26,[113,630,631],{"class":143},"    \"\"\"mytool — deployment helpers.\"\"\"\n",[10,633,634,637,638,640,641,47],{},[14,635,636],{},"importlib.metadata.version()"," is the most reliable source: it reads the metadata of whatever is actually installed, so it is right for wheels, sdists, editable installs and frozen builds alike. The generated ",[14,639,342],{}," is useful when something needs the version without package metadata — a PyInstaller binary built from a source tree, for example — but for a normally installed CLI, metadata is enough. More on presenting version information is in ",[43,642,644],{"href":643},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata\u002F","exposing version info and build metadata",[49,646,648],{"id":647},"what-version-does-a-given-checkout-produce","What version does a given checkout produce?",[10,650,651,652,655,656,660],{},"On a tagged commit, the version is the tag minus its ",[14,653,654],{},"v",". Between tags, hatch-vcs produces a ",[657,658,659],"strong",{},"development version"," that sorts after the last release and before the next one, and records exactly which commit it came from:",[86,662],{"name":663},"vcs-describe",[54,665,666,674,691,697],{},[57,667,668,670,671,673],{},[14,669,27],{}," — exactly on tag ",[14,672,77],{},", clean working tree.",[57,675,676,679,680,682,683,686,687,690],{},[14,677,678],{},"1.5.1.dev3+g1a2b3c4"," — three commits after ",[14,681,77],{},"; the next patch is assumed, ",[14,684,685],{},"dev3"," counts commits, and ",[14,688,689],{},"g1a2b3c4"," is the commit hash.",[57,692,693,696],{},[14,694,695],{},"...+g1a2b3c4.d20260918"," — the same, with uncommitted changes; the date marks a dirty tree.",[57,698,699,702],{},[14,700,701],{},"0.1.dev1+g..."," — no tag reachable, usually because of a shallow clone.",[10,704,705,706,708,709,712],{},"Development versions are valid PEP 440 versions, so ",[14,707,31],{}," on a developer's machine tells you precisely which commit they are running — useful in bug reports. PyPI rejects versions with a ",[14,710,711],{},"+local"," part, which is a helpful safety net: a build from an untagged commit cannot be published by accident.",[49,714,716],{"id":715},"where-it-goes-wrong-and-the-fixes","Where it goes wrong, and the fixes",[10,718,719,722,723,726,727,730],{},[657,720,721],{},"Shallow clones in CI."," ",[14,724,725],{},"actions\u002Fcheckout"," fetches one commit by default, with no tags, so hatch-vcs falls back to ",[14,728,729],{},"0.1.dev1",". Fetch full history in any job that builds the package:",[104,732,736],{"className":733,"code":734,"language":735,"meta":109,"style":109},"language-yaml shiki shiki-themes github-light github-dark","- uses: actions\u002Fcheckout@v4\n  with:\n    fetch-depth: 0\n","yaml",[14,737,738,753,760],{"__ignoreMap":109},[113,739,740,743,747,750],{"class":115,"line":116},[113,741,742],{"class":126},"- ",[113,744,746],{"class":745},"s9eBZ","uses",[113,748,749],{"class":126},": ",[113,751,752],{"class":143},"actions\u002Fcheckout@v4\n",[113,754,755,758],{"class":115,"line":123},[113,756,757],{"class":745},"  with",[113,759,412],{"class":126},[113,761,762,765,767],{"class":115,"line":137},[113,763,764],{"class":745},"    fetch-depth",[113,766,749],{"class":126},[113,768,769],{"class":417},"0\n",[10,771,772,775,776,779,780,783],{},[657,773,774],{},"Building from an sdist without git."," A source distribution is not a git repository. hatch-vcs handles this by writing the version into the sdist's ",[14,777,778],{},"PKG-INFO"," when the sdist is built, and reading it back when building a wheel from the sdist — which is why you should build the sdist in an environment that ",[34,781,782],{},"does"," have git history, and let downstream packagers build wheels from it.",[10,785,786,722,789,792,793,795,796,799],{},[657,787,788],{},"Editable installs go stale.",[14,790,791],{},"uv sync"," installs your project in editable mode, and the version in its metadata is computed when the environment is created. After tagging, ",[14,794,31],{}," in the dev environment may still show the old dev version until you reinstall with ",[14,797,798],{},"uv sync --reinstall-package mytool",". It is cosmetic, but it confuses people exactly once.",[10,801,802,805,806,809,810,813,814,816,817,820,821,824,825,47],{},[657,803,804],{},"Building outside a repository."," Docker builds that ",[14,807,808],{},"COPY"," only ",[14,811,812],{},"src\u002F"," and ",[14,815,16],{}," have no ",[14,818,819],{},".git"," directory. Either build the wheel before the Docker build and install the wheel, or pass the version in with the ",[14,822,823],{},"SETUPTOOLS_SCM_PRETEND_VERSION"," environment variable, which hatch-vcs honours: ",[14,826,827],{},"ENV SETUPTOOLS_SCM_PRETEND_VERSION=1.5.0",[49,829,831],{"id":830},"ux-considerations","UX considerations",[54,833,834,848,862],{},[57,835,836,843,844,847],{},[657,837,838,839,842],{},"Keep ",[14,840,841],{},"--version"," output simple."," One line, ",[14,845,846],{},"mytool 1.5.0",", parseable by scripts. Development builds show their full version, which is exactly what a bug report needs.",[57,849,850,853,854,857,858,47],{},[657,851,852],{},"Make release tagging a one-liner."," With the version coming from the tag, a release is ",[14,855,856],{},"git tag -a v1.6.0 -m \"mytool 1.6.0\" && git push origin v1.6.0",". The CI side is in ",[43,859,861],{"href":860},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags\u002F","automating releases from git tags",[57,863,864,722,867,870],{},[657,865,866],{},"Check before you push the tag.",[14,868,869],{},"uvx hatch version"," prints what the build would produce; if it is not the version you expect, the tag is missing, mistyped or on the wrong commit.",[86,872],{"name":873},"vcs-terminal",[49,875,877],{"id":876},"testing-the-behaviour","Testing the behaviour",[10,879,880,881,883],{},"Two tests are worth having. A unit test that ",[14,882,841],{}," prints something version-shaped, and a slower test that builds from a temporary repository with a tag and checks the result — the kind of test that catches a broken build configuration before a release does:",[104,885,887],{"className":353,"code":886,"language":355,"meta":109,"style":109},"# tests\u002Ftest_version.py\nimport os\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nimport pytest\nfrom typer.testing import CliRunner\n\nfrom mytool.cli import app\n\nVERSION = re.compile(r\"^mytool \\d+\\.\\d+\\.\\d+(\\.dev\\d+)?(\\+[\\w.]+)?$\")\n\n\ndef test_version_flag():\n    result = CliRunner().invoke(app, [\"--version\"])\n    assert result.exit_code == 0\n    assert VERSION.match(result.output.strip()), result.output\n\n\n@pytest.mark.slow\ndef test_tag_becomes_version(tmp_path: Path):\n    repo = Path(__file__).parent.parent\n    subprocess.run([\"git\", \"clone\", \"-q\", str(repo), str(tmp_path \u002F \"r\")], check=True)\n    r = tmp_path \u002F \"r\"\n    env = {**os.environ, \"GIT_AUTHOR_NAME\": \"t\", \"GIT_AUTHOR_EMAIL\": \"t@e\",\n           \"GIT_COMMITTER_NAME\": \"t\", \"GIT_COMMITTER_EMAIL\": \"t@e\"}\n    subprocess.run([\"git\", \"tag\", \"-a\", \"v9.8.7\", \"-m\", \"test\"], cwd=r, check=True, env=env)\n    subprocess.run([sys.executable, \"-m\", \"build\", \"--wheel\", \"--outdir\", str(tmp_path \u002F \"d\")],\n                   cwd=r, check=True, capture_output=True)\n    assert list((tmp_path \u002F \"d\").glob(\"mytool-9.8.7-*.whl\"))\n",[14,888,889,894,901,908,915,922,934,938,945,957,961,973,977,1058,1062,1066,1076,1091,1105,1115,1119,1123,1128,1138,1154,1203,1218,1255,1277,1336,1373,1400],{"__ignoreMap":109},[113,890,891],{"class":115,"line":116},[113,892,893],{"class":119},"# tests\u002Ftest_version.py\n",[113,895,896,898],{"class":115,"line":123},[113,897,374],{"class":367},[113,899,900],{"class":126}," os\n",[113,902,903,905],{"class":115,"line":137},[113,904,374],{"class":367},[113,906,907],{"class":126}," re\n",[113,909,910,912],{"class":115,"line":147},[113,911,374],{"class":367},[113,913,914],{"class":126}," subprocess\n",[113,916,917,919],{"class":115,"line":162},[113,918,374],{"class":367},[113,920,921],{"class":126}," sys\n",[113,923,924,926,929,931],{"class":115,"line":171},[113,925,368],{"class":367},[113,927,928],{"class":126}," pathlib ",[113,930,374],{"class":367},[113,932,933],{"class":126}," Path\n",[113,935,936],{"class":115,"line":182},[113,937,186],{"emptyLinePlaceholder":185},[113,939,940,942],{"class":115,"line":189},[113,941,374],{"class":367},[113,943,944],{"class":126}," pytest\n",[113,946,947,949,952,954],{"class":115,"line":203},[113,948,368],{"class":367},[113,950,951],{"class":126}," typer.testing ",[113,953,374],{"class":367},[113,955,956],{"class":126}," CliRunner\n",[113,958,959],{"class":115,"line":212},[113,960,186],{"emptyLinePlaceholder":185},[113,962,963,965,968,970],{"class":115,"line":217},[113,964,368],{"class":367},[113,966,967],{"class":126}," mytool.cli ",[113,969,374],{"class":367},[113,971,972],{"class":126}," app\n",[113,974,975],{"class":115,"line":227},[113,976,186],{"emptyLinePlaceholder":185},[113,978,979,982,984,987,990,992,995,999,1002,1005,1009,1011,1013,1015,1017,1019,1022,1024,1027,1029,1031,1034,1037,1039,1042,1045,1047,1049,1051,1054,1056],{"class":115,"line":244},[113,980,981],{"class":417},"VERSION",[113,983,421],{"class":367},[113,985,986],{"class":126}," re.compile(",[113,988,989],{"class":367},"r",[113,991,520],{"class":143},[113,993,994],{"class":417},"^",[113,996,998],{"class":997},"sA_wV","mytool ",[113,1000,1001],{"class":417},"\\d",[113,1003,1004],{"class":367},"+",[113,1006,1008],{"class":1007},"snhLl","\\.",[113,1010,1001],{"class":417},[113,1012,1004],{"class":367},[113,1014,1008],{"class":1007},[113,1016,1001],{"class":417},[113,1018,1004],{"class":367},[113,1020,1021],{"class":417},"(",[113,1023,1008],{"class":1007},[113,1025,1026],{"class":997},"dev",[113,1028,1001],{"class":417},[113,1030,1004],{"class":367},[113,1032,1033],{"class":417},")",[113,1035,1036],{"class":367},"?",[113,1038,1021],{"class":417},[113,1040,1041],{"class":1007},"\\+",[113,1043,1044],{"class":417},"[\\w.]",[113,1046,1004],{"class":367},[113,1048,1033],{"class":417},[113,1050,1036],{"class":367},[113,1052,1053],{"class":417},"$",[113,1055,520],{"class":143},[113,1057,430],{"class":126},[113,1059,1060],{"class":115,"line":253},[113,1061,186],{"emptyLinePlaceholder":185},[113,1063,1064],{"class":115,"line":258},[113,1065,186],{"emptyLinePlaceholder":185},[113,1067,1068,1070,1073],{"class":115,"line":278},[113,1069,478],{"class":367},[113,1071,1072],{"class":130}," test_version_flag",[113,1074,1075],{"class":126},"():\n",[113,1077,1078,1081,1083,1086,1088],{"class":115,"line":287},[113,1079,1080],{"class":126},"    result ",[113,1082,462],{"class":367},[113,1084,1085],{"class":126}," CliRunner().invoke(app, [",[113,1087,573],{"class":143},[113,1089,1090],{"class":126},"])\n",[113,1092,1093,1096,1099,1102],{"class":115,"line":292},[113,1094,1095],{"class":367},"    assert",[113,1097,1098],{"class":126}," result.exit_code ",[113,1100,1101],{"class":367},"==",[113,1103,1104],{"class":417}," 0\n",[113,1106,1107,1109,1112],{"class":115,"line":320},[113,1108,1095],{"class":367},[113,1110,1111],{"class":417}," VERSION",[113,1113,1114],{"class":126},".match(result.output.strip()), result.output\n",[113,1116,1117],{"class":115,"line":537},[113,1118,186],{"emptyLinePlaceholder":185},[113,1120,1121],{"class":115,"line":542},[113,1122,186],{"emptyLinePlaceholder":185},[113,1124,1125],{"class":115,"line":551},[113,1126,1127],{"class":130},"@pytest.mark.slow\n",[113,1129,1130,1132,1135],{"class":115,"line":562},[113,1131,478],{"class":367},[113,1133,1134],{"class":130}," test_tag_becomes_version",[113,1136,1137],{"class":126},"(tmp_path: Path):\n",[113,1139,1140,1143,1145,1148,1151],{"class":115,"line":597},[113,1141,1142],{"class":126},"    repo ",[113,1144,462],{"class":367},[113,1146,1147],{"class":126}," Path(",[113,1149,1150],{"class":417},"__file__",[113,1152,1153],{"class":126},").parent.parent\n",[113,1155,1156,1159,1162,1164,1167,1169,1172,1174,1177,1180,1182,1185,1188,1191,1194,1197,1199,1201],{"class":115,"line":619},[113,1157,1158],{"class":126},"    subprocess.run([",[113,1160,1161],{"class":143},"\"git\"",[113,1163,236],{"class":126},[113,1165,1166],{"class":143},"\"clone\"",[113,1168,236],{"class":126},[113,1170,1171],{"class":143},"\"-q\"",[113,1173,236],{"class":126},[113,1175,1176],{"class":417},"str",[113,1178,1179],{"class":126},"(repo), ",[113,1181,1176],{"class":417},[113,1183,1184],{"class":126},"(tmp_path ",[113,1186,1187],{"class":367},"\u002F",[113,1189,1190],{"class":143}," \"r\"",[113,1192,1193],{"class":126},")], ",[113,1195,1196],{"class":578},"check",[113,1198,462],{"class":367},[113,1200,584],{"class":417},[113,1202,430],{"class":126},[113,1204,1205,1208,1210,1213,1215],{"class":115,"line":628},[113,1206,1207],{"class":126},"    r ",[113,1209,462],{"class":367},[113,1211,1212],{"class":126}," tmp_path ",[113,1214,1187],{"class":367},[113,1216,1217],{"class":143}," \"r\"\n",[113,1219,1221,1224,1226,1229,1232,1235,1238,1240,1243,1245,1248,1250,1253],{"class":115,"line":1220},27,[113,1222,1223],{"class":126},"    env ",[113,1225,462],{"class":367},[113,1227,1228],{"class":126}," {",[113,1230,1231],{"class":367},"**",[113,1233,1234],{"class":126},"os.environ, ",[113,1236,1237],{"class":143},"\"GIT_AUTHOR_NAME\"",[113,1239,749],{"class":126},[113,1241,1242],{"class":143},"\"t\"",[113,1244,236],{"class":126},[113,1246,1247],{"class":143},"\"GIT_AUTHOR_EMAIL\"",[113,1249,749],{"class":126},[113,1251,1252],{"class":143},"\"t@e\"",[113,1254,616],{"class":126},[113,1256,1258,1261,1263,1265,1267,1270,1272,1274],{"class":115,"line":1257},28,[113,1259,1260],{"class":143},"           \"GIT_COMMITTER_NAME\"",[113,1262,749],{"class":126},[113,1264,1242],{"class":143},[113,1266,236],{"class":126},[113,1268,1269],{"class":143},"\"GIT_COMMITTER_EMAIL\"",[113,1271,749],{"class":126},[113,1273,1252],{"class":143},[113,1275,1276],{"class":126},"}\n",[113,1278,1280,1282,1284,1286,1289,1291,1294,1296,1299,1301,1304,1306,1309,1312,1315,1317,1320,1322,1324,1326,1328,1331,1333],{"class":115,"line":1279},29,[113,1281,1158],{"class":126},[113,1283,1161],{"class":143},[113,1285,236],{"class":126},[113,1287,1288],{"class":143},"\"tag\"",[113,1290,236],{"class":126},[113,1292,1293],{"class":143},"\"-a\"",[113,1295,236],{"class":126},[113,1297,1298],{"class":143},"\"v9.8.7\"",[113,1300,236],{"class":126},[113,1302,1303],{"class":143},"\"-m\"",[113,1305,236],{"class":126},[113,1307,1308],{"class":143},"\"test\"",[113,1310,1311],{"class":126},"], ",[113,1313,1314],{"class":578},"cwd",[113,1316,462],{"class":367},[113,1318,1319],{"class":126},"r, ",[113,1321,1196],{"class":578},[113,1323,462],{"class":367},[113,1325,584],{"class":417},[113,1327,236],{"class":126},[113,1329,1330],{"class":578},"env",[113,1332,462],{"class":367},[113,1334,1335],{"class":126},"env)\n",[113,1337,1339,1342,1344,1346,1349,1351,1354,1356,1359,1361,1363,1365,1367,1370],{"class":115,"line":1338},30,[113,1340,1341],{"class":126},"    subprocess.run([sys.executable, ",[113,1343,1303],{"class":143},[113,1345,236],{"class":126},[113,1347,1348],{"class":143},"\"build\"",[113,1350,236],{"class":126},[113,1352,1353],{"class":143},"\"--wheel\"",[113,1355,236],{"class":126},[113,1357,1358],{"class":143},"\"--outdir\"",[113,1360,236],{"class":126},[113,1362,1176],{"class":417},[113,1364,1184],{"class":126},[113,1366,1187],{"class":367},[113,1368,1369],{"class":143}," \"d\"",[113,1371,1372],{"class":126},")],\n",[113,1374,1376,1379,1381,1383,1385,1387,1389,1391,1394,1396,1398],{"class":115,"line":1375},31,[113,1377,1378],{"class":578},"                   cwd",[113,1380,462],{"class":367},[113,1382,1319],{"class":126},[113,1384,1196],{"class":578},[113,1386,462],{"class":367},[113,1388,584],{"class":417},[113,1390,236],{"class":126},[113,1392,1393],{"class":578},"capture_output",[113,1395,462],{"class":367},[113,1397,584],{"class":417},[113,1399,430],{"class":126},[113,1401,1403,1405,1408,1411,1413,1415,1418,1421],{"class":115,"line":1402},32,[113,1404,1095],{"class":367},[113,1406,1407],{"class":417}," list",[113,1409,1410],{"class":126},"((tmp_path ",[113,1412,1187],{"class":367},[113,1414,1369],{"class":143},[113,1416,1417],{"class":126},").glob(",[113,1419,1420],{"class":143},"\"mytool-9.8.7-*.whl\"",[113,1422,1423],{"class":126},"))\n",[10,1425,1426],{},"The second test clones the repository (so it has history and can take a throwaway tag), tags it, builds, and asserts that the wheel carries exactly the tagged version.",[49,1428,1430],{"id":1429},"conclusion","Conclusion",[10,1432,1433,1434,1436,1437,1439,1440,1443],{},"A version number should exist in one place, and for a project released from git that place is the tag. ",[14,1435,40],{}," reads it at build time and writes it into the package metadata; ",[14,1438,636],{}," reads it back at runtime; development builds get precise, unpublishable dev versions for free. Fetch full history in CI, build sdists where git is available, remember to reinstall editable environments after tagging, and releasing becomes a single ",[14,1441,1442],{},"git tag"," with no file to forget.",[49,1445,1447],{"id":1446},"frequently-asked-questions","Frequently asked questions",[1449,1450,1452],"h3",{"id":1451},"hatch-vcs-or-setuptools-scm","hatch-vcs or setuptools-scm?",[10,1454,1455],{},"They share an engine: hatch-vcs is the hatchling plugin built on setuptools-scm's version logic, so versions come out identical. Use whichever matches your build backend.",[1449,1457,1459,1460,1462],{"id":1458},"can-i-use-tags-without-the-v-prefix","Can I use tags without the ",[14,1461,654],{}," prefix?",[10,1464,1465,1466,813,1468,1470],{},"Yes; both ",[14,1467,77],{},[14,1469,27],{}," are recognised by default. Pick one convention and stick to it, since mixing them makes history confusing and can confuse other tooling.",[1449,1472,1474],{"id":1473},"how-do-i-make-the-next-dev-version-a-minor-rather-than-a-patch-bump","How do I make the next dev version a minor rather than a patch bump?",[10,1476,1477,1478,1481],{},"Set ",[14,1479,1480],{},"[tool.hatch.version.raw-options] version_scheme = \"release-branch-semver\""," or another setuptools-scm scheme. The default guesses the next patch, which is fine — dev versions only need to sort correctly, not predict the future.",[1449,1483,1485],{"id":1484},"how-does-this-work-in-a-repository-with-several-packages","How does this work in a repository with several packages?",[10,1487,1488,1489,813,1492,1495,1496,1499,1500,1503,1504,1507,1508,47],{},"Each package needs its own tag namespace, such as ",[14,1490,1491],{},"cli-v1.5.0",[14,1493,1494],{},"core-v2.1.0",". Configure each package's ",[14,1497,1498],{},"tag-pattern"," (via ",[14,1501,1502],{},"[tool.hatch.version.raw-options]",") to match only its own tags, and set ",[14,1505,1506],{},"root"," to the repository root so git is found from the package's subdirectory. The workspace layout that usually goes with this is described in ",[43,1509,1511],{"href":1510},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis\u002F","uv workspaces for multi-package CLIs",[1449,1513,1515,1516,1519],{"id":1514},"should-i-still-expose-__version__-in-the-package","Should I still expose ",[14,1517,1518],{},"__version__"," in the package?",[10,1521,1522,1523,1526,1527,1530],{},"It is a convention some users and tools still look for. Setting ",[14,1524,1525],{},"__version__ = version(\"mytool\")"," in ",[14,1528,1529],{},"__init__.py",", as the recipe does in the CLI module, costs one metadata lookup at import time and keeps both styles working. Avoid hard-coding it anywhere; that reintroduces the second copy this whole approach removes.",[1449,1532,1534],{"id":1533},"does-this-work-with-uvs-own-build-backend","Does this work with uv's own build backend?",[10,1536,1537,1540,1541,1543,1544,1546],{},[14,1538,1539],{},"uv_build"," intentionally supports only static versions at the time of writing, so projects that want tag-derived versions keep ",[14,1542,62],{}," with ",[14,1545,40],{}," as the backend. uv builds, installs and publishes them without any difference.",[49,1548,1550],{"id":1549},"related","Related",[54,1552,1553,1559,1564,1570,1575],{},[57,1554,1555,1556],{},"Up: ",[43,1557,1558],{"href":45},"Managing CLI versioning and changelogs",[57,1560,1561],{},[43,1562,1563],{"href":643},"Exposing version info and build metadata",[57,1565,1566],{},[43,1567,1569],{"href":1568},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools\u002F","Semantic versioning policy for CLI tools",[57,1571,1572],{},[43,1573,1574],{"href":860},"Automating releases from git tags",[57,1576,1577],{},[43,1578,1580],{"href":1579},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F","Building wheels and sdists for Python CLIs",[1582,1583,1584],"style",{},"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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":109,"searchDepth":123,"depth":123,"links":1586},[1587,1588,1589,1590,1591,1592,1593,1594,1595,1605],{"id":51,"depth":123,"text":52},{"id":83,"depth":123,"text":84},{"id":101,"depth":123,"text":102},{"id":647,"depth":123,"text":648},{"id":715,"depth":123,"text":716},{"id":830,"depth":123,"text":831},{"id":876,"depth":123,"text":877},{"id":1429,"depth":123,"text":1430},{"id":1446,"depth":123,"text":1447,"children":1596},[1597,1598,1600,1601,1602,1604],{"id":1451,"depth":137,"text":1452},{"id":1458,"depth":137,"text":1599},"Can I use tags without the v prefix?",{"id":1473,"depth":137,"text":1474},{"id":1484,"depth":137,"text":1485},{"id":1514,"depth":137,"text":1603},"Should I still expose __version__ in the package?",{"id":1533,"depth":137,"text":1534},{"id":1549,"depth":123,"text":1550},"2026-09-18","Stop editing version numbers by hand: derive a Python CLI’s version from git tags with hatch-vcs, read it at runtime, and handle dev builds, CI clones and sdists.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs",{"title":5,"description":1607},"project-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs\u002Findex",[1616,40,1617,1618],"versioning","git","packaging","q5kt9_Lz2gbL_JntfU5h4GulNc6wPdxgBogLISV3xZw",[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,1896,1899,1902,1905,1908,1911,1914,1917,1920,1923,1926,1929,1932,1935,1938,1941,1944,1947,1950,1953,1956,1959,1962,1965,1968,1971,1974,1977,1980,1983,1986,1989,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088,2089,2092,2095,2098,2101,2104,2107,2110,2113,2116,2119,2122,2125,2128,2131,2134,2137,2140,2143,2146,2149,2152,2155,2158,2161,2164],{"path":1622,"title":1623},"\u002Fabout","About Python CLI Toolcraft",{"path":1625,"title":1626},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1628,"title":1629},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1631,"title":1632},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1634,"title":1635},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1637,"title":1638},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1640,"title":1641},"\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":1643,"title":1644},"\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":1646,"title":1647},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1649,"title":1650},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1652,"title":1653},"\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":1655,"title":1656},"\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":1658,"title":1659},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1661,"title":1662},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1664,"title":1665},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1667,"title":1668},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1670,"title":1671},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1673,"title":1674},"\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":1676,"title":1677},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1679,"title":1680},"\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":1682,"title":1683},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1685,"title":1686},"\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":1688,"title":1689},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1691,"title":1692},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1694,"title":1695},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1697,"title":1698},"\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":1700,"title":1701},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1703,"title":1704},"\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":1706,"title":1707},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1709,"title":1710},"\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":1712,"title":1713},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1715,"title":1716},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1718,"title":1719},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1721,"title":1722},"\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":1724,"title":1725},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1727,"title":1728},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1730,"title":1731},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1733,"title":1734},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1736,"title":1737},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1739,"title":1740},"\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":1742,"title":1743},"\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":1745,"title":1746},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1748,"title":1749},"\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":1751,"title":1752},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1754,"title":1755},"\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":1757,"title":1758},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1760,"title":1761},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1763,"title":1764},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1766,"title":1767},"\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":1769,"title":1770},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1772,"title":1773},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1775,"title":1776},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1778,"title":1779},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1781,"title":1782},"\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":1784,"title":1785},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1787,"title":1788},"\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":1790,"title":1791},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1793,"title":1794},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1796,"title":1797},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1799,"title":1800},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1802,"title":1803},"\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":1805,"title":1806},"\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":1808,"title":1809},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1811,"title":1812},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1814,"title":1815},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1817,"title":1818},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1820,"title":1821},"\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":1823,"title":1824},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1826,"title":1827},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1829,"title":1830},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1832,"title":1833},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1835,"title":1836},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1838,"title":1839},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1841,"title":1842},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1844,"title":1845},"\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":1847,"title":1848},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1850,"title":1851},"\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":1853,"title":1854},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1856,"title":1857},"\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":1859,"title":1860},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1862,"title":1863},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1865,"title":1866},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1868,"title":1869},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1871,"title":1872},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1874,"title":1875},"\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":1877,"title":1878},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1880,"title":1881},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1883,"title":1884},"\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":1886,"title":1887},"\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":1889,"title":1890},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1892,"title":1893},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1187,"title":1895},"Python CLI Toolcraft",{"path":1897,"title":1898},"\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":1900,"title":1901},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1903,"title":1904},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1906,"title":1907},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1909,"title":1910},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1912,"title":1913},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1915,"title":1916},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1918,"title":1919},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1921,"title":1922},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1924,"title":1925},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1927,"title":1928},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1930,"title":1931},"\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":1933,"title":1934},"\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":1936,"title":1937},"\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":1939,"title":1940},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1942,"title":1943},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1945,"title":1946},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1948,"title":1949},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1951,"title":1952},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1954,"title":1955},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1957,"title":1958},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1960,"title":1961},"\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":1963,"title":1964},"\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":1966,"title":1967},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1969,"title":1970},"\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":1972,"title":1973},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1975,"title":1976},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1978,"title":1979},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1981,"title":1982},"\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":1984,"title":1985},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1987,"title":1988},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1990,"title":1991},"\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":1993,"title":1994},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1996,"title":1997},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1999,"title":2000},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2002,"title":2003},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2005,"title":2006},"\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":2008,"title":2009},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2011,"title":2012},"\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":2014,"title":2015},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2017,"title":2018},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2020,"title":2021},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2023,"title":2024},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2026,"title":2027},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2029,"title":2030},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2032,"title":2033},"\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":2035,"title":2036},"\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":2038,"title":2039},"\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":2041,"title":2042},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2044,"title":2045},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2047,"title":2048},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2050,"title":2051},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2053,"title":2054},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2056,"title":2057},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2059,"title":2060},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2062,"title":2063},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2065,"title":2066},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2068,"title":2069},"\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":2071,"title":2072},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2074,"title":2075},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2077,"title":2078},"\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":2080,"title":2081},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2083,"title":2084},"\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":2086,"title":2087},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1612,"title":5},{"path":2090,"title":2091},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2093,"title":2094},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2096,"title":2097},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2099,"title":2100},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2102,"title":2103},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2105,"title":2106},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2108,"title":2109},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2111,"title":2112},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2114,"title":2115},"\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":2117,"title":2118},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2120,"title":2121},"\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":2123,"title":2124},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2126,"title":2127},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2129,"title":2130},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2132,"title":2133},"\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":2135,"title":2136},"\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":2138,"title":2139},"\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":2141,"title":2142},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2144,"title":2145},"\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":2147,"title":2148},"\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":2150,"title":2151},"\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":2153,"title":2154},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2156,"title":2157},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2159,"title":2160},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2162,"title":2163},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2165,"title":2166},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907496]