[{"data":1,"prerenderedAt":2198},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook\u002F":3,"content-directory":1652},{"id":4,"title":5,"body":6,"date":1638,"description":1639,"difficulty":1640,"draft":1641,"extension":1642,"meta":1643,"navigation":239,"path":1644,"seo":1645,"stem":1646,"tags":1647,"updated":1638,"__hash__":1651},"content\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook\u002Findex.md","Shipping Your Python CLI as a pre-commit Hook",{"type":7,"value":8,"toc":1618},"minimark",[9,28,33,61,65,69,79,91,97,100,316,319,322,375,378,437,441,444,930,933,969,973,976,1014,1018,1029,1060,1064,1071,1152,1158,1486,1489,1493,1508,1512,1521,1527,1531,1537,1541,1551,1555,1561,1565,1578,1582,1614],[10,11,12,13,17,18,21,22,27],"p",{},"Your CLI checks or fixes files — it validates deployment configs, lints SQL, formats Terraform variables, enforces naming rules on Kubernetes manifests. People already run it by hand or in CI, and the natural next request is \"can I run it on commit?\". pre-commit is the standard answer: a user adds a few lines naming your repository and hook ID to their ",[14,15,16],"code",{},".pre-commit-config.yaml",", and pre-commit installs your tool into an isolated environment and runs it on every commit, passing only the staged files. This guide shows how to publish a hook from your CLI's repository: the ",[14,19,20],{},".pre-commit-hooks.yaml"," contract, making your command behave well with filename arguments, deciding between checking and fixing, releasing, and testing the hook before anyone else uses it. It belongs to the ",[23,24,26],"a",{"href":25},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002F","pre-commit hooks for CLI projects topic",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,36,52,55],"ul",{},[37,38,39,40,43,44,47,48,51],"li",{},"A Python CLI in a public or shared git repository, installable with ",[14,41,42],{},"pip install ."," (a ",[14,45,46],{},"pyproject.toml"," with ",[14,49,50],{},"[project.scripts]",").",[37,53,54],{},"A command that accepts file paths as arguments — most checkers already do.",[37,56,57,58,27],{},"pre-commit installed for testing: ",[14,59,60],{},"uv tool install pre-commit",[29,62,64],{"id":63},"how-pre-commit-runs-someone-elses-tool","How pre-commit runs someone else's tool",[66,67],"inline-diagram",{"name":68},"pch-flow",[10,70,71,72,74,75,78],{},"When a user's configuration references your repository at a tag, pre-commit clones your repository at that tag, creates a virtual environment, runs ",[14,73,42],{}," in it, and caches the result. On each commit it runs your hook's ",[14,76,77],{},"entry"," command inside that environment, appending the paths of staged files that match your hook's filters. If the command exits non-zero, or modifies any file, the commit is stopped.",[10,80,81,82,86,87,90],{},"Two consequences shape everything else. Your tool runs in ",[83,84,85],"strong",{},"its own"," environment, built from your repository — not the user's project environment — so it cannot import the user's code. And it receives ",[83,88,89],{},"file paths on the command line",", possibly many of them, possibly split across several invocations when there are too many for one command line.",[29,92,94,95],{"id":93},"the-recipe-pre-commit-hooksyaml","The recipe: ",[14,96,20],{},[10,98,99],{},"Add this file to the root of your CLI's repository:",[101,102,107],"pre",{"className":103,"code":104,"language":105,"meta":106,"style":106},"language-yaml shiki shiki-themes github-light github-dark","# .pre-commit-hooks.yaml\n- id: mytool-check\n  name: mytool config check\n  description: Validate deployment configuration files.\n  entry: mytool check\n  language: python\n  types_or: [yaml, json]\n  files: ^(deploy|config)\u002F\n  require_serial: false\n  minimum_pre_commit_version: \"3.0.0\"\n\n- id: mytool-fix\n  name: mytool config fix\n  description: Rewrite deployment configuration files into canonical form.\n  entry: mytool fix\n  language: python\n  types_or: [yaml, json]\n  files: ^(deploy|config)\u002F\n","yaml","",[14,108,109,118,136,147,158,169,180,200,211,223,234,241,253,263,273,283,292,307],{"__ignoreMap":106},[110,111,114],"span",{"class":112,"line":113},"line",1,[110,115,117],{"class":116},"sJ8bj","# .pre-commit-hooks.yaml\n",[110,119,121,125,129,132],{"class":112,"line":120},2,[110,122,124],{"class":123},"sVt8B","- ",[110,126,128],{"class":127},"s9eBZ","id",[110,130,131],{"class":123},": ",[110,133,135],{"class":134},"sZZnC","mytool-check\n",[110,137,139,142,144],{"class":112,"line":138},3,[110,140,141],{"class":127},"  name",[110,143,131],{"class":123},[110,145,146],{"class":134},"mytool config check\n",[110,148,150,153,155],{"class":112,"line":149},4,[110,151,152],{"class":127},"  description",[110,154,131],{"class":123},[110,156,157],{"class":134},"Validate deployment configuration files.\n",[110,159,161,164,166],{"class":112,"line":160},5,[110,162,163],{"class":127},"  entry",[110,165,131],{"class":123},[110,167,168],{"class":134},"mytool check\n",[110,170,172,175,177],{"class":112,"line":171},6,[110,173,174],{"class":127},"  language",[110,176,131],{"class":123},[110,178,179],{"class":134},"python\n",[110,181,183,186,189,191,194,197],{"class":112,"line":182},7,[110,184,185],{"class":127},"  types_or",[110,187,188],{"class":123},": [",[110,190,105],{"class":134},[110,192,193],{"class":123},", ",[110,195,196],{"class":134},"json",[110,198,199],{"class":123},"]\n",[110,201,203,206,208],{"class":112,"line":202},8,[110,204,205],{"class":127},"  files",[110,207,131],{"class":123},[110,209,210],{"class":134},"^(deploy|config)\u002F\n",[110,212,214,217,219],{"class":112,"line":213},9,[110,215,216],{"class":127},"  require_serial",[110,218,131],{"class":123},[110,220,222],{"class":221},"sj4cs","false\n",[110,224,226,229,231],{"class":112,"line":225},10,[110,227,228],{"class":127},"  minimum_pre_commit_version",[110,230,131],{"class":123},[110,232,233],{"class":134},"\"3.0.0\"\n",[110,235,237],{"class":112,"line":236},11,[110,238,240],{"emptyLinePlaceholder":239},true,"\n",[110,242,244,246,248,250],{"class":112,"line":243},12,[110,245,124],{"class":123},[110,247,128],{"class":127},[110,249,131],{"class":123},[110,251,252],{"class":134},"mytool-fix\n",[110,254,256,258,260],{"class":112,"line":255},13,[110,257,141],{"class":127},[110,259,131],{"class":123},[110,261,262],{"class":134},"mytool config fix\n",[110,264,266,268,270],{"class":112,"line":265},14,[110,267,152],{"class":127},[110,269,131],{"class":123},[110,271,272],{"class":134},"Rewrite deployment configuration files into canonical form.\n",[110,274,276,278,280],{"class":112,"line":275},15,[110,277,163],{"class":127},[110,279,131],{"class":123},[110,281,282],{"class":134},"mytool fix\n",[110,284,286,288,290],{"class":112,"line":285},16,[110,287,174],{"class":127},[110,289,131],{"class":123},[110,291,179],{"class":134},[110,293,295,297,299,301,303,305],{"class":112,"line":294},17,[110,296,185],{"class":127},[110,298,188],{"class":123},[110,300,105],{"class":134},[110,302,193],{"class":123},[110,304,196],{"class":134},[110,306,199],{"class":123},[110,308,310,312,314],{"class":112,"line":309},18,[110,311,205],{"class":127},[110,313,131],{"class":123},[110,315,210],{"class":134},[66,317],{"name":318},"pch-yaml",[10,320,321],{},"Each field has a job:",[34,323,324,331,342,349,363],{},[37,325,326,330],{},[83,327,328],{},[14,329,128],{}," is what users reference. It is a public API: renaming it breaks every configuration that uses it.",[37,332,333,337,338,341],{},[83,334,335],{},[14,336,77],{}," is the command to run. With ",[14,339,340],{},"language: python"," it is resolved inside the hook's environment, so it can be your console script.",[37,343,344,348],{},[83,345,346],{},[14,347,340],{}," tells pre-commit to build an isolated virtual environment from your repository. Users need nothing else installed.",[37,350,351,356,357,362],{},[83,352,353],{},[14,354,355],{},"types_or"," and ",[83,358,359],{},[14,360,361],{},"files"," filter which staged files are passed. Filtering here, rather than inside your tool, means your tool is not even started for commits that do not touch relevant files.",[37,364,365,370,371,374],{},[83,366,367],{},[14,368,369],{},"require_serial"," controls parallelism. By default pre-commit may split the file list and run several copies of your command concurrently; set it to ",[14,372,373],{},"true"," if your tool keeps a cache or lock that concurrent runs would fight over.",[10,376,377],{},"Users then enable it with:",[101,379,381],{"className":103,"code":380,"language":105,"meta":106,"style":106},"# their .pre-commit-config.yaml\nrepos:\n  - repo: https:\u002F\u002Fgithub.com\u002Facme\u002Fmytool\n    rev: v2.4.0\n    hooks:\n      - id: mytool-check\n",[14,382,383,388,396,409,419,426],{"__ignoreMap":106},[110,384,385],{"class":112,"line":113},[110,386,387],{"class":116},"# their .pre-commit-config.yaml\n",[110,389,390,393],{"class":112,"line":120},[110,391,392],{"class":127},"repos",[110,394,395],{"class":123},":\n",[110,397,398,401,404,406],{"class":112,"line":138},[110,399,400],{"class":123},"  - ",[110,402,403],{"class":127},"repo",[110,405,131],{"class":123},[110,407,408],{"class":134},"https:\u002F\u002Fgithub.com\u002Facme\u002Fmytool\n",[110,410,411,414,416],{"class":112,"line":149},[110,412,413],{"class":127},"    rev",[110,415,131],{"class":123},[110,417,418],{"class":134},"v2.4.0\n",[110,420,421,424],{"class":112,"line":160},[110,422,423],{"class":127},"    hooks",[110,425,395],{"class":123},[110,427,428,431,433,435],{"class":112,"line":171},[110,429,430],{"class":123},"      - ",[110,432,128],{"class":127},[110,434,131],{"class":123},[110,436,135],{"class":134},[29,438,440],{"id":439},"the-recipe-a-command-that-behaves-like-a-good-hook","The recipe: a command that behaves like a good hook",[10,442,443],{},"pre-commit's contract is simple: files arrive as arguments, the exit code says pass or fail, and any modification to a file counts as a failure (so the user reviews and re-stages the change). Make your command fit it:",[101,445,449],{"className":446,"code":447,"language":448,"meta":106,"style":106},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fcli.py\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\nfrom mytool.validate import Problem, canonical, validate_file\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Deployment configuration tools.\"\"\"\n\n\n@app.command()\ndef check(files: Annotated[list[Path], typer.Argument(exists=True, dir_okay=False)]) -> None:\n    \"\"\"Validate FILES; exit 1 if any problem is found.\"\"\"\n    problems: list[Problem] = []\n    for path in files:\n        problems.extend(validate_file(path))\n    for p in problems:\n        typer.echo(f\"{p.path}:{p.line}: {p.message}\")        # file:line: message\n    raise typer.Exit(1 if problems else 0)\n\n\n@app.command()\ndef fix(files: Annotated[list[Path], typer.Argument(exists=True, dir_okay=False)]) -> None:\n    \"\"\"Rewrite FILES into canonical form; exit 1 if anything changed.\"\"\"\n    changed = 0\n    for path in files:\n        before = path.read_text(encoding=\"utf-8\")\n        after = canonical(before)\n        if after != before:\n            path.write_text(after, encoding=\"utf-8\")\n            typer.echo(f\"fixed {path}\")\n            changed += 1\n    raise typer.Exit(1 if changed else 0)\n","python",[14,450,451,456,471,483,487,494,498,510,514,525,529,533,542,558,563,567,571,578,614,620,631,646,652,665,713,740,745,750,757,787,793,804,815,836,847,862,876,898,910],{"__ignoreMap":106},[110,452,453],{"class":112,"line":113},[110,454,455],{"class":116},"# src\u002Fmytool\u002Fcli.py\n",[110,457,458,462,465,468],{"class":112,"line":120},[110,459,461],{"class":460},"szBVR","from",[110,463,464],{"class":123}," pathlib ",[110,466,467],{"class":460},"import",[110,469,470],{"class":123}," Path\n",[110,472,473,475,478,480],{"class":112,"line":138},[110,474,461],{"class":460},[110,476,477],{"class":123}," typing ",[110,479,467],{"class":460},[110,481,482],{"class":123}," Annotated\n",[110,484,485],{"class":112,"line":149},[110,486,240],{"emptyLinePlaceholder":239},[110,488,489,491],{"class":112,"line":160},[110,490,467],{"class":460},[110,492,493],{"class":123}," typer\n",[110,495,496],{"class":112,"line":171},[110,497,240],{"emptyLinePlaceholder":239},[110,499,500,502,505,507],{"class":112,"line":182},[110,501,461],{"class":460},[110,503,504],{"class":123}," mytool.validate ",[110,506,467],{"class":460},[110,508,509],{"class":123}," Problem, canonical, validate_file\n",[110,511,512],{"class":112,"line":202},[110,513,240],{"emptyLinePlaceholder":239},[110,515,516,519,522],{"class":112,"line":213},[110,517,518],{"class":123},"app ",[110,520,521],{"class":460},"=",[110,523,524],{"class":123}," typer.Typer()\n",[110,526,527],{"class":112,"line":225},[110,528,240],{"emptyLinePlaceholder":239},[110,530,531],{"class":112,"line":236},[110,532,240],{"emptyLinePlaceholder":239},[110,534,535,539],{"class":112,"line":243},[110,536,538],{"class":537},"sScJk","@app.callback",[110,540,541],{"class":123},"()\n",[110,543,544,547,550,553,556],{"class":112,"line":255},[110,545,546],{"class":460},"def",[110,548,549],{"class":537}," main",[110,551,552],{"class":123},"() -> ",[110,554,555],{"class":221},"None",[110,557,395],{"class":123},[110,559,560],{"class":112,"line":265},[110,561,562],{"class":134},"    \"\"\"Deployment configuration tools.\"\"\"\n",[110,564,565],{"class":112,"line":275},[110,566,240],{"emptyLinePlaceholder":239},[110,568,569],{"class":112,"line":285},[110,570,240],{"emptyLinePlaceholder":239},[110,572,573,576],{"class":112,"line":294},[110,574,575],{"class":537},"@app.command",[110,577,541],{"class":123},[110,579,580,582,585,588,592,594,597,599,602,604,607,610,612],{"class":112,"line":309},[110,581,546],{"class":460},[110,583,584],{"class":537}," check",[110,586,587],{"class":123},"(files: Annotated[list[Path], typer.Argument(",[110,589,591],{"class":590},"s4XuR","exists",[110,593,521],{"class":460},[110,595,596],{"class":221},"True",[110,598,193],{"class":123},[110,600,601],{"class":590},"dir_okay",[110,603,521],{"class":460},[110,605,606],{"class":221},"False",[110,608,609],{"class":123},")]) -> ",[110,611,555],{"class":221},[110,613,395],{"class":123},[110,615,617],{"class":112,"line":616},19,[110,618,619],{"class":134},"    \"\"\"Validate FILES; exit 1 if any problem is found.\"\"\"\n",[110,621,623,626,628],{"class":112,"line":622},20,[110,624,625],{"class":123},"    problems: list[Problem] ",[110,627,521],{"class":460},[110,629,630],{"class":123}," []\n",[110,632,634,637,640,643],{"class":112,"line":633},21,[110,635,636],{"class":460},"    for",[110,638,639],{"class":123}," path ",[110,641,642],{"class":460},"in",[110,644,645],{"class":123}," files:\n",[110,647,649],{"class":112,"line":648},22,[110,650,651],{"class":123},"        problems.extend(validate_file(path))\n",[110,653,655,657,660,662],{"class":112,"line":654},23,[110,656,636],{"class":460},[110,658,659],{"class":123}," p ",[110,661,642],{"class":460},[110,663,664],{"class":123}," problems:\n",[110,666,668,671,674,677,680,683,686,689,691,694,696,698,700,703,705,707,710],{"class":112,"line":667},24,[110,669,670],{"class":123},"        typer.echo(",[110,672,673],{"class":460},"f",[110,675,676],{"class":134},"\"",[110,678,679],{"class":221},"{",[110,681,682],{"class":123},"p.path",[110,684,685],{"class":221},"}",[110,687,688],{"class":134},":",[110,690,679],{"class":221},[110,692,693],{"class":123},"p.line",[110,695,685],{"class":221},[110,697,131],{"class":134},[110,699,679],{"class":221},[110,701,702],{"class":123},"p.message",[110,704,685],{"class":221},[110,706,676],{"class":134},[110,708,709],{"class":123},")        ",[110,711,712],{"class":116},"# file:line: message\n",[110,714,716,719,722,725,728,731,734,737],{"class":112,"line":715},25,[110,717,718],{"class":460},"    raise",[110,720,721],{"class":123}," typer.Exit(",[110,723,724],{"class":221},"1",[110,726,727],{"class":460}," if",[110,729,730],{"class":123}," problems ",[110,732,733],{"class":460},"else",[110,735,736],{"class":221}," 0",[110,738,739],{"class":123},")\n",[110,741,743],{"class":112,"line":742},26,[110,744,240],{"emptyLinePlaceholder":239},[110,746,748],{"class":112,"line":747},27,[110,749,240],{"emptyLinePlaceholder":239},[110,751,753,755],{"class":112,"line":752},28,[110,754,575],{"class":537},[110,756,541],{"class":123},[110,758,760,762,765,767,769,771,773,775,777,779,781,783,785],{"class":112,"line":759},29,[110,761,546],{"class":460},[110,763,764],{"class":537}," fix",[110,766,587],{"class":123},[110,768,591],{"class":590},[110,770,521],{"class":460},[110,772,596],{"class":221},[110,774,193],{"class":123},[110,776,601],{"class":590},[110,778,521],{"class":460},[110,780,606],{"class":221},[110,782,609],{"class":123},[110,784,555],{"class":221},[110,786,395],{"class":123},[110,788,790],{"class":112,"line":789},30,[110,791,792],{"class":134},"    \"\"\"Rewrite FILES into canonical form; exit 1 if anything changed.\"\"\"\n",[110,794,796,799,801],{"class":112,"line":795},31,[110,797,798],{"class":123},"    changed ",[110,800,521],{"class":460},[110,802,803],{"class":221}," 0\n",[110,805,807,809,811,813],{"class":112,"line":806},32,[110,808,636],{"class":460},[110,810,639],{"class":123},[110,812,642],{"class":460},[110,814,645],{"class":123},[110,816,818,821,823,826,829,831,834],{"class":112,"line":817},33,[110,819,820],{"class":123},"        before ",[110,822,521],{"class":460},[110,824,825],{"class":123}," path.read_text(",[110,827,828],{"class":590},"encoding",[110,830,521],{"class":460},[110,832,833],{"class":134},"\"utf-8\"",[110,835,739],{"class":123},[110,837,839,842,844],{"class":112,"line":838},34,[110,840,841],{"class":123},"        after ",[110,843,521],{"class":460},[110,845,846],{"class":123}," canonical(before)\n",[110,848,850,853,856,859],{"class":112,"line":849},35,[110,851,852],{"class":460},"        if",[110,854,855],{"class":123}," after ",[110,857,858],{"class":460},"!=",[110,860,861],{"class":123}," before:\n",[110,863,865,868,870,872,874],{"class":112,"line":864},36,[110,866,867],{"class":123},"            path.write_text(after, ",[110,869,828],{"class":590},[110,871,521],{"class":460},[110,873,833],{"class":134},[110,875,739],{"class":123},[110,877,879,882,884,887,889,892,894,896],{"class":112,"line":878},37,[110,880,881],{"class":123},"            typer.echo(",[110,883,673],{"class":460},[110,885,886],{"class":134},"\"fixed ",[110,888,679],{"class":221},[110,890,891],{"class":123},"path",[110,893,685],{"class":221},[110,895,676],{"class":134},[110,897,739],{"class":123},[110,899,901,904,907],{"class":112,"line":900},38,[110,902,903],{"class":123},"            changed ",[110,905,906],{"class":460},"+=",[110,908,909],{"class":221}," 1\n",[110,911,913,915,917,919,921,924,926,928],{"class":112,"line":912},39,[110,914,718],{"class":460},[110,916,721],{"class":123},[110,918,724],{"class":221},[110,920,727],{"class":460},[110,922,923],{"class":123}," changed ",[110,925,733],{"class":460},[110,927,736],{"class":221},[110,929,739],{"class":123},[10,931,932],{},"The details that make a hook pleasant:",[34,934,935,941,951,957,963],{},[37,936,937,940],{},[83,938,939],{},"Accept many files in one call"," and handle each independently; report problems for all of them before exiting.",[37,942,943,950],{},[83,944,945,946,949],{},"Use ",[14,947,948],{},"file:line: message"," output."," Editors, CI log viewers and humans all recognise it, and many terminals make it clickable.",[37,952,953,956],{},[83,954,955],{},"Exit 1 when a fixer changes something."," pre-commit would fail the hook anyway because files changed, but an explicit exit code makes the same command useful in CI, where \"needed fixing\" should fail the build.",[37,958,959,962],{},[83,960,961],{},"Do not print anything on success."," pre-commit shows \"Passed\"; extra output is noise on every commit.",[37,964,965,968],{},[83,966,967],{},"Never touch files you were not given."," A hook that rewrites other files surprises users and confuses pre-commit's change detection.",[29,970,972],{"id":971},"ux-considerations","UX considerations",[66,974],{"name":975},"pch-terminal",[34,977,978,984,994,1000],{},[37,979,980,983],{},[83,981,982],{},"Offer check and fix as separate hooks."," Some teams want automatic fixes on commit; others want a failing check and a manual fix. Two hook IDs let each choose.",[37,985,986,989,990,27],{},[83,987,988],{},"Keep startup fast."," Your command runs on every commit that touches matching files. A CLI that takes 800 ms to import is felt constantly; lazy imports matter here, as covered in ",[23,991,993],{"href":992},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup\u002F","lazy-loading subcommands for faster startup",[37,995,996,999],{},[83,997,998],{},"Suggest the fix in the message."," \"unknown key 'replcas' (did you mean 'replicas'?)\" turns a failed commit into a ten-second correction.",[37,1001,1002,1005,1006,1009,1010,1013],{},[83,1003,1004],{},"Document configuration."," Users pass extra flags through ",[14,1007,1008],{},"args:"," in their configuration (",[14,1011,1012],{},"args: [--strict]","). List the supported flags in the README section about pre-commit.",[29,1015,1017],{"id":1016},"releasing-the-hook","Releasing the hook",[10,1019,1020,1021,1024,1025,1028],{},"Users pin ",[14,1022,1023],{},"rev"," to a tag, and ",[14,1026,1027],{},"pre-commit autoupdate"," moves them to your newest tag. So:",[34,1030,1031,1044,1054],{},[37,1032,1033,1036,1037,1039,1040,27],{},[83,1034,1035],{},"Every release tag is a hook release."," The ",[14,1038,20],{}," at that tag is what users get. Tag releases as described in ",[23,1041,1043],{"href":1042},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags\u002F","automating releases from git tags",[37,1045,1046,1049,1050,27],{},[83,1047,1048],{},"Treat hook IDs and default behaviour as public API."," Adding a hook is a minor change; renaming one, or making a check stricter by default, is breaking for users who update automatically. Follow the policy in ",[23,1051,1053],{"href":1052},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools\u002F","semantic versioning policy for CLI tools",[37,1055,1056,1059],{},[83,1057,1058],{},"Keep install dependencies light."," Every user's first commit after updating builds your environment. Heavy dependencies slow that down noticeably.",[29,1061,1063],{"id":1062},"testing-the-behaviour","Testing the behaviour",[10,1065,1066,1067,1070],{},"pre-commit can run hooks straight from a working tree with ",[14,1068,1069],{},"try-repo",", which is the fastest way to test a hook definition before tagging:",[101,1072,1076],{"className":1073,"code":1074,"language":1075,"meta":106,"style":106},"language-bash shiki shiki-themes github-light github-dark","# In a scratch repository with some sample config files staged:\ngit init \u002Ftmp\u002Fhook-test && cd \u002Ftmp\u002Fhook-test\nmkdir deploy && printf 'replcas: 3\\n' > deploy\u002Fapp.yaml && git add .\npre-commit try-repo ~\u002Fsrc\u002Fmytool mytool-check --all-files\n","bash",[14,1077,1078,1083,1103,1135],{"__ignoreMap":106},[110,1079,1080],{"class":112,"line":113},[110,1081,1082],{"class":116},"# In a scratch repository with some sample config files staged:\n",[110,1084,1085,1088,1091,1094,1097,1100],{"class":112,"line":120},[110,1086,1087],{"class":537},"git",[110,1089,1090],{"class":134}," init",[110,1092,1093],{"class":134}," \u002Ftmp\u002Fhook-test",[110,1095,1096],{"class":123}," && ",[110,1098,1099],{"class":221},"cd",[110,1101,1102],{"class":134}," \u002Ftmp\u002Fhook-test\n",[110,1104,1105,1108,1111,1113,1116,1119,1122,1125,1127,1129,1132],{"class":112,"line":138},[110,1106,1107],{"class":537},"mkdir",[110,1109,1110],{"class":134}," deploy",[110,1112,1096],{"class":123},[110,1114,1115],{"class":221},"printf",[110,1117,1118],{"class":134}," 'replcas: 3\\n'",[110,1120,1121],{"class":460}," >",[110,1123,1124],{"class":134}," deploy\u002Fapp.yaml",[110,1126,1096],{"class":123},[110,1128,1087],{"class":537},[110,1130,1131],{"class":134}," add",[110,1133,1134],{"class":134}," .\n",[110,1136,1137,1140,1143,1146,1149],{"class":112,"line":149},[110,1138,1139],{"class":537},"pre-commit",[110,1141,1142],{"class":134}," try-repo",[110,1144,1145],{"class":134}," ~\u002Fsrc\u002Fmytool",[110,1147,1148],{"class":134}," mytool-check",[110,1150,1151],{"class":221}," --all-files\n",[10,1153,1154,1155,688],{},"For automated tests, test the command's contract directly — exit codes, output format, and that fixers only touch given files — with ",[14,1156,1157],{},"CliRunner",[101,1159,1161],{"className":446,"code":1160,"language":448,"meta":106,"style":106},"# tests\u002Ftest_hook_contract.py\nfrom typer.testing import CliRunner\n\nfrom mytool.cli import app\n\nrunner = CliRunner()\n\n\ndef test_check_reports_file_and_line(tmp_path):\n    bad = tmp_path \u002F \"app.yaml\"\n    bad.write_text(\"replcas: 3\\n\")\n    result = runner.invoke(app, [\"check\", str(bad)])\n    assert result.exit_code == 1\n    assert f\"{bad}:1:\" in result.output\n\n\ndef test_check_is_silent_on_success(tmp_path):\n    good = tmp_path \u002F \"app.yaml\"\n    good.write_text(\"replicas: 3\\n\")\n    result = runner.invoke(app, [\"check\", str(good)])\n    assert (result.exit_code, result.output) == (0, \"\")\n\n\ndef test_fix_exits_1_only_when_it_changes_something(tmp_path):\n    f = tmp_path \u002F \"app.yaml\"\n    f.write_text(\"replicas:   3\\n\")\n    assert runner.invoke(app, [\"fix\", str(f)]).exit_code == 1\n    assert runner.invoke(app, [\"fix\", str(f)]).exit_code == 0     # already canonical\n",[14,1162,1163,1168,1180,1184,1196,1200,1210,1214,1218,1228,1244,1259,1280,1293,1318,1322,1326,1335,1348,1362,1379,1401,1405,1409,1418,1431,1445,1465],{"__ignoreMap":106},[110,1164,1165],{"class":112,"line":113},[110,1166,1167],{"class":116},"# tests\u002Ftest_hook_contract.py\n",[110,1169,1170,1172,1175,1177],{"class":112,"line":120},[110,1171,461],{"class":460},[110,1173,1174],{"class":123}," typer.testing ",[110,1176,467],{"class":460},[110,1178,1179],{"class":123}," CliRunner\n",[110,1181,1182],{"class":112,"line":138},[110,1183,240],{"emptyLinePlaceholder":239},[110,1185,1186,1188,1191,1193],{"class":112,"line":149},[110,1187,461],{"class":460},[110,1189,1190],{"class":123}," mytool.cli ",[110,1192,467],{"class":460},[110,1194,1195],{"class":123}," app\n",[110,1197,1198],{"class":112,"line":160},[110,1199,240],{"emptyLinePlaceholder":239},[110,1201,1202,1205,1207],{"class":112,"line":171},[110,1203,1204],{"class":123},"runner ",[110,1206,521],{"class":460},[110,1208,1209],{"class":123}," CliRunner()\n",[110,1211,1212],{"class":112,"line":182},[110,1213,240],{"emptyLinePlaceholder":239},[110,1215,1216],{"class":112,"line":202},[110,1217,240],{"emptyLinePlaceholder":239},[110,1219,1220,1222,1225],{"class":112,"line":213},[110,1221,546],{"class":460},[110,1223,1224],{"class":537}," test_check_reports_file_and_line",[110,1226,1227],{"class":123},"(tmp_path):\n",[110,1229,1230,1233,1235,1238,1241],{"class":112,"line":225},[110,1231,1232],{"class":123},"    bad ",[110,1234,521],{"class":460},[110,1236,1237],{"class":123}," tmp_path ",[110,1239,1240],{"class":460},"\u002F",[110,1242,1243],{"class":134}," \"app.yaml\"\n",[110,1245,1246,1249,1252,1255,1257],{"class":112,"line":236},[110,1247,1248],{"class":123},"    bad.write_text(",[110,1250,1251],{"class":134},"\"replcas: 3",[110,1253,1254],{"class":221},"\\n",[110,1256,676],{"class":134},[110,1258,739],{"class":123},[110,1260,1261,1264,1266,1269,1272,1274,1277],{"class":112,"line":243},[110,1262,1263],{"class":123},"    result ",[110,1265,521],{"class":460},[110,1267,1268],{"class":123}," runner.invoke(app, [",[110,1270,1271],{"class":134},"\"check\"",[110,1273,193],{"class":123},[110,1275,1276],{"class":221},"str",[110,1278,1279],{"class":123},"(bad)])\n",[110,1281,1282,1285,1288,1291],{"class":112,"line":255},[110,1283,1284],{"class":460},"    assert",[110,1286,1287],{"class":123}," result.exit_code ",[110,1289,1290],{"class":460},"==",[110,1292,909],{"class":221},[110,1294,1295,1297,1300,1302,1304,1307,1309,1312,1315],{"class":112,"line":265},[110,1296,1284],{"class":460},[110,1298,1299],{"class":460}," f",[110,1301,676],{"class":134},[110,1303,679],{"class":221},[110,1305,1306],{"class":123},"bad",[110,1308,685],{"class":221},[110,1310,1311],{"class":134},":1:\"",[110,1313,1314],{"class":460}," in",[110,1316,1317],{"class":123}," result.output\n",[110,1319,1320],{"class":112,"line":275},[110,1321,240],{"emptyLinePlaceholder":239},[110,1323,1324],{"class":112,"line":285},[110,1325,240],{"emptyLinePlaceholder":239},[110,1327,1328,1330,1333],{"class":112,"line":294},[110,1329,546],{"class":460},[110,1331,1332],{"class":537}," test_check_is_silent_on_success",[110,1334,1227],{"class":123},[110,1336,1337,1340,1342,1344,1346],{"class":112,"line":309},[110,1338,1339],{"class":123},"    good ",[110,1341,521],{"class":460},[110,1343,1237],{"class":123},[110,1345,1240],{"class":460},[110,1347,1243],{"class":134},[110,1349,1350,1353,1356,1358,1360],{"class":112,"line":616},[110,1351,1352],{"class":123},"    good.write_text(",[110,1354,1355],{"class":134},"\"replicas: 3",[110,1357,1254],{"class":221},[110,1359,676],{"class":134},[110,1361,739],{"class":123},[110,1363,1364,1366,1368,1370,1372,1374,1376],{"class":112,"line":622},[110,1365,1263],{"class":123},[110,1367,521],{"class":460},[110,1369,1268],{"class":123},[110,1371,1271],{"class":134},[110,1373,193],{"class":123},[110,1375,1276],{"class":221},[110,1377,1378],{"class":123},"(good)])\n",[110,1380,1381,1383,1386,1388,1391,1394,1396,1399],{"class":112,"line":633},[110,1382,1284],{"class":460},[110,1384,1385],{"class":123}," (result.exit_code, result.output) ",[110,1387,1290],{"class":460},[110,1389,1390],{"class":123}," (",[110,1392,1393],{"class":221},"0",[110,1395,193],{"class":123},[110,1397,1398],{"class":134},"\"\"",[110,1400,739],{"class":123},[110,1402,1403],{"class":112,"line":648},[110,1404,240],{"emptyLinePlaceholder":239},[110,1406,1407],{"class":112,"line":654},[110,1408,240],{"emptyLinePlaceholder":239},[110,1410,1411,1413,1416],{"class":112,"line":667},[110,1412,546],{"class":460},[110,1414,1415],{"class":537}," test_fix_exits_1_only_when_it_changes_something",[110,1417,1227],{"class":123},[110,1419,1420,1423,1425,1427,1429],{"class":112,"line":715},[110,1421,1422],{"class":123},"    f ",[110,1424,521],{"class":460},[110,1426,1237],{"class":123},[110,1428,1240],{"class":460},[110,1430,1243],{"class":134},[110,1432,1433,1436,1439,1441,1443],{"class":112,"line":742},[110,1434,1435],{"class":123},"    f.write_text(",[110,1437,1438],{"class":134},"\"replicas:   3",[110,1440,1254],{"class":221},[110,1442,676],{"class":134},[110,1444,739],{"class":123},[110,1446,1447,1449,1451,1454,1456,1458,1461,1463],{"class":112,"line":747},[110,1448,1284],{"class":460},[110,1450,1268],{"class":123},[110,1452,1453],{"class":134},"\"fix\"",[110,1455,193],{"class":123},[110,1457,1276],{"class":221},[110,1459,1460],{"class":123},"(f)]).exit_code ",[110,1462,1290],{"class":460},[110,1464,909],{"class":221},[110,1466,1467,1469,1471,1473,1475,1477,1479,1481,1483],{"class":112,"line":752},[110,1468,1284],{"class":460},[110,1470,1268],{"class":123},[110,1472,1453],{"class":134},[110,1474,193],{"class":123},[110,1476,1276],{"class":221},[110,1478,1460],{"class":123},[110,1480,1290],{"class":460},[110,1482,736],{"class":221},[110,1484,1485],{"class":116},"     # already canonical\n",[10,1487,1488],{},"The idempotence test in the last function matters: a fixer whose output is not stable under a second run makes pre-commit fail forever.",[29,1490,1492],{"id":1491},"conclusion","Conclusion",[10,1494,1495,1496,1498,1499,1501,1502,1504,1505,1507],{},"Publishing a pre-commit hook turns your CLI into something teams adopt with three lines of YAML. Add a ",[14,1497,20],{}," with stable IDs, ",[14,1500,340],{}," and tight file filters; make the command accept many files, print ",[14,1503,948],{},", stay silent on success and exit non-zero when it finds or fixes something; keep it fast; and treat hook IDs and defaults as public API released through tags. Test with ",[14,1506,1069],{}," and a handful of contract tests, and your tool starts running on every commit in every repository that wants it.",[29,1509,1511],{"id":1510},"frequently-asked-questions","Frequently asked questions",[1513,1514,1516,1517,1520],"h3",{"id":1515},"should-the-hook-use-language-system-instead","Should the hook use ",[14,1518,1519],{},"language: system"," instead?",[10,1522,1523,1524,1526],{},"Only for hooks that must run inside the user's own environment, such as a type checker that needs their dependencies. For a published tool, ",[14,1525,340],{}," is what makes it work anywhere without installation instructions.",[1513,1528,1530],{"id":1529},"how-do-i-pass-configuration-to-the-hook","How do I pass configuration to the hook?",[10,1532,1533,1534,1536],{},"Read a config file from the repository root (your tool's usual discovery rules apply, since pre-commit runs from the root), and accept flags through the user's ",[14,1535,1008],{},". Avoid requiring environment variables; hooks run in varied environments.",[1513,1538,1540],{"id":1539},"my-tool-needs-to-see-all-files-not-just-staged-ones-is-that-possible","My tool needs to see all files, not just staged ones. Is that possible?",[10,1542,1543,1544,356,1547,1550],{},"Set ",[14,1545,1546],{},"pass_filenames: false",[14,1548,1549],{},"always_run: true",", and let your tool discover files itself. Use sparingly: it runs on every commit regardless of what changed, so it must be fast.",[1513,1552,1554],{"id":1553},"should-the-hook-repository-be-separate-from-the-cli-repository","Should the hook repository be separate from the CLI repository?",[10,1556,1557,1558,1560],{},"Usually not. Keeping ",[14,1559,20],{}," in the CLI's own repository means every release tag is automatically a hook release and the hook always runs the matching version of the tool. A separate \"mirror\" repository makes sense only when the tool's repository is huge or slow to clone, since pre-commit clones it for every user.",[1513,1562,1564],{"id":1563},"can-i-publish-hooks-for-tools-that-are-not-python","Can I publish hooks for tools that are not Python?",[10,1566,1567,1568,1570,1571,1574,1575,1577],{},"pre-commit supports many languages, including prebuilt binaries via ",[14,1569,1519],{}," or container images via ",[14,1572,1573],{},"language: docker_image",". For a Python CLI, ",[14,1576,340],{}," is the simplest and most portable.",[29,1579,1581],{"id":1580},"related","Related",[34,1583,1584,1590,1596,1602,1608],{},[37,1585,1586,1587],{},"Up: ",[23,1588,1589],{"href":25},"Pre-commit hooks for CLI projects",[37,1591,1592],{},[23,1593,1595],{"href":1594},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python\u002F","Writing local pre-commit hooks in Python",[37,1597,1598],{},[23,1599,1601],{"href":1600},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos\u002F","Setting up pre-commit for Python CLI repos",[37,1603,1604],{},[23,1605,1607],{"href":1606},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","Choosing exit codes for CLI tools",[37,1609,1610],{},[23,1611,1613],{"href":1612},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read\u002F","Writing help text users actually read",[1615,1616,1617],"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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":106,"searchDepth":120,"depth":120,"links":1619},[1620,1621,1622,1624,1625,1626,1627,1628,1629,1637],{"id":31,"depth":120,"text":32},{"id":63,"depth":120,"text":64},{"id":93,"depth":120,"text":1623},"The recipe: .pre-commit-hooks.yaml",{"id":439,"depth":120,"text":440},{"id":971,"depth":120,"text":972},{"id":1016,"depth":120,"text":1017},{"id":1062,"depth":120,"text":1063},{"id":1491,"depth":120,"text":1492},{"id":1510,"depth":120,"text":1511,"children":1630},[1631,1633,1634,1635,1636],{"id":1515,"depth":138,"text":1632},"Should the hook use language: system instead?",{"id":1529,"depth":138,"text":1530},{"id":1539,"depth":138,"text":1540},{"id":1553,"depth":138,"text":1554},{"id":1563,"depth":138,"text":1564},{"id":1580,"depth":120,"text":1581},"2026-09-18","Let other repositories run your Python CLI through pre-commit: a .pre-commit-hooks.yaml, filename handling, exit codes, auto-fixing, release tags and tests.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook",{"title":5,"description":1639},"project-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook\u002Findex",[1139,1648,1649,1650],"distribution","git-hooks","linting","euz6ReJSlmiIt7ucjq-7AzIwEem057ilnRLPBJDwGIU",[1653,1656,1659,1662,1665,1668,1671,1674,1677,1680,1683,1686,1689,1692,1695,1698,1701,1704,1707,1710,1713,1716,1719,1722,1725,1728,1731,1734,1737,1740,1743,1746,1749,1752,1755,1758,1761,1764,1767,1770,1773,1776,1779,1782,1785,1788,1791,1794,1797,1800,1803,1806,1809,1812,1815,1818,1821,1824,1827,1830,1833,1836,1839,1842,1845,1848,1851,1854,1857,1860,1863,1866,1869,1872,1875,1878,1881,1884,1887,1890,1893,1896,1899,1902,1905,1908,1911,1914,1917,1920,1923,1926,1928,1931,1934,1937,1940,1943,1946,1949,1952,1955,1958,1961,1964,1967,1970,1973,1976,1979,1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2039,2042,2045,2048,2051,2054,2057,2060,2063,2066,2069,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2165,2167,2168,2171,2174,2177,2180,2183,2186,2189,2192,2195],{"path":1654,"title":1655},"\u002Fabout","About Python CLI Toolcraft",{"path":1657,"title":1658},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1660,"title":1661},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1663,"title":1664},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1666,"title":1667},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1669,"title":1670},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1672,"title":1673},"\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":1675,"title":1676},"\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":1678,"title":1679},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1681,"title":1682},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1684,"title":1685},"\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":1687,"title":1688},"\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":1690,"title":1691},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1693,"title":1694},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1696,"title":1697},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1699,"title":1700},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1702,"title":1703},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1705,"title":1706},"\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":1708,"title":1709},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1711,"title":1712},"\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":1714,"title":1715},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1717,"title":1718},"\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":1720,"title":1721},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1723,"title":1724},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1726,"title":1727},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1729,"title":1730},"\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":1732,"title":1733},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1735,"title":1736},"\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":1738,"title":1739},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1741,"title":1742},"\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":1744,"title":1745},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1747,"title":1748},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1750,"title":1751},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1753,"title":1754},"\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":1756,"title":1757},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1759,"title":1760},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1762,"title":1763},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1765,"title":1766},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1768,"title":1769},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1771,"title":1772},"\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":1774,"title":1775},"\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":1777,"title":1778},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1780,"title":1781},"\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":1783,"title":1784},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1786,"title":1787},"\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":1789,"title":1790},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1792,"title":1793},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1795,"title":1796},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1798,"title":1799},"\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":1801,"title":1802},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1804,"title":1805},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1807,"title":1808},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1810,"title":1811},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1813,"title":1814},"\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":1816,"title":1817},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1819,"title":1820},"\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":1822,"title":1823},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1825,"title":1826},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1828,"title":1829},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1831,"title":1832},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1834,"title":1835},"\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":1837,"title":1838},"\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":1840,"title":1841},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1843,"title":1844},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1846,"title":1847},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1849,"title":1850},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1852,"title":1853},"\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":1855,"title":1856},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1858,"title":1859},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1861,"title":1862},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1864,"title":1865},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1867,"title":1868},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1870,"title":1871},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1873,"title":1874},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1876,"title":1877},"\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":1879,"title":1880},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1882,"title":1883},"\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":1885,"title":1886},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1888,"title":1889},"\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":1891,"title":1892},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1894,"title":1895},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1897,"title":1898},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1900,"title":1901},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1903,"title":1904},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1906,"title":1907},"\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":1909,"title":1910},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1912,"title":1913},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1915,"title":1916},"\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":1918,"title":1919},"\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":1921,"title":1922},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1924,"title":1925},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1240,"title":1927},"Python CLI Toolcraft",{"path":1929,"title":1930},"\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":1932,"title":1933},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1935,"title":1936},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1938,"title":1939},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1941,"title":1942},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1944,"title":1945},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1947,"title":1948},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1950,"title":1951},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1953,"title":1954},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1956,"title":1957},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1959,"title":1960},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1962,"title":1963},"\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":1965,"title":1966},"\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":1968,"title":1969},"\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":1971,"title":1972},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1974,"title":1975},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1977,"title":1978},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1980,"title":1981},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1983,"title":1984},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1986,"title":1987},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1989,"title":1990},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1992,"title":1993},"\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":1995,"title":1996},"\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":1998,"title":1999},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2001,"title":2002},"\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":2004,"title":2005},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2007,"title":2008},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2010,"title":2011},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2013,"title":2014},"\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":2016,"title":2017},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2019,"title":2020},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2022,"title":2023},"\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":2025,"title":2026},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2028,"title":2029},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2031,"title":2032},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2034,"title":2035},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2037,"title":2038},"\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":2040,"title":2041},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2043,"title":2044},"\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":2046,"title":2047},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2049,"title":2050},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2052,"title":2053},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2055,"title":2056},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2058,"title":2059},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2061,"title":2062},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2064,"title":2065},"\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":2067,"title":2068},"\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":2070,"title":2071},"\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":2073,"title":2074},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2076,"title":2077},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2079,"title":2080},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2082,"title":2083},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2085,"title":2086},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2088,"title":2089},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2091,"title":2092},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2094,"title":2095},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2097,"title":2098},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2100,"title":2101},"\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":2103,"title":2104},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2106,"title":2107},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2109,"title":2110},"\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":2112,"title":2113},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2115,"title":2116},"\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":2118,"title":2119},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2121,"title":2122},"\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":2124,"title":2125},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2127,"title":2128},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2130,"title":2131},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2133,"title":2134},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2136,"title":2137},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2139,"title":2140},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2142,"title":2143},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2145,"title":2146},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2148,"title":2149},"\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":2151,"title":2152},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"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\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2166,"title":1601},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos",{"path":1644,"title":5},{"path":2169,"title":2170},"\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":2172,"title":2173},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2175,"title":2176},"\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":2178,"title":2179},"\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":2181,"title":2182},"\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":2184,"title":2185},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2187,"title":2188},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2190,"title":2191},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2193,"title":2194},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2196,"title":2197},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907969]