[{"data":1,"prerenderedAt":1129},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F":3,"content-directory":978},{"id":4,"title":5,"body":6,"date":964,"description":965,"difficulty":966,"draft":967,"extension":968,"meta":969,"navigation":255,"path":970,"seo":971,"stem":972,"tags":973,"updated":964,"__hash__":977},"content\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Findex.md","Shell Completion for Python CLIs",{"type":7,"value":8,"toc":954},"minimark",[9,13,18,68,72,76,91,102,116,120,123,156,159,206,218,222,225,228,390,425,428,575,607,615,619,630,633,777,815,819,822,855,861,865,916,920,950],[10,11,12],"p",{},"Tab completion is the fastest quality-of-life upgrade you can ship for a command-line tool: press Tab and the shell fills in your subcommands, options, and even valid values for an argument. Done well it also teaches the interface — users discover commands without ever opening the docs. This overview explains how completion actually works, what Click and Typer give you out of the box, and how the two deeper guides fit together so you can turn it on and install it on every shell your users run.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,46,57],"ul",{},[22,23,24,25,29],"li",{},"Completion is a handshake: when the user presses Tab, the shell re-invokes ",[26,27,28],"em",{},"your"," program in a special completion mode, your program prints candidate strings, and the shell displays them.",[22,31,32,36,37,41,42,45],{},[33,34,35],"strong",{},"Typer"," has it built in — ",[38,39,40],"code",{},"--install-completion"," writes the shell script for you. ",[33,43,44],{},"Click 8"," has the same engine underneath but you wire the install step yourself.",[22,47,48,49,52,53,56],{},"Completions can be ",[33,50,51],{},"static"," (the fixed set of subcommands and choices, computed for free from your command tree) or ",[33,54,55],{},"dynamic"," (values pulled at Tab-time from a file, an API, or the current arguments).",[22,58,59,60,63,64,67],{},"Two moving parts: ",[26,61,62],{},"enabling"," completion in your Python code, and ",[26,65,66],{},"installing"," the generated script into bash, zsh, or fish. They fail independently, so this section splits them into two guides.",[69,70],"inline-diagram",{"name":71},"shell-completion-flow",[14,73,75],{"id":74},"what-completion-is-and-why-it-matters","What completion is and why it matters",[10,77,78,79,82,83,86,87,90],{},"When you type ",[38,80,81],{},"git com"," and press Tab and it becomes ",[38,84,85],{},"git commit",", the shell did not read a static list of git's subcommands from a config file. It ran a small piece of shell code — a ",[26,88,89],{},"completion function"," — that git registered when your shell started. For a Python CLI the goal is the same: register a completion function that knows your commands, options, and arguments, and keep it in sync with your code automatically instead of by hand.",[10,92,93,94,97,98,101],{},"The payoff is concrete. Users stop mistyping subcommands, stop guessing flag names, and stop grepping ",[38,95,96],{},"--help"," for the option that takes an environment name. For a tool with dynamic inputs — deploy targets, dataset IDs, profile names — completion can suggest the ",[26,99,100],{},"actual"," valid values, which is the difference between a CLI that feels alive and one that feels like a form you have to fill out perfectly on the first try.",[10,103,104,105,110,111,115],{},"Completion also composes with the rest of a good CLI. It works best on a clean command tree (see ",[106,107,109],"a",{"href":108},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","structuring multi-command Python CLIs",") and it pairs naturally with a polished ",[106,112,114],{"href":113},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","interactive terminal UI",": completion helps users assemble the command, Rich makes the result readable.",[14,117,119],{"id":118},"the-completion-handshake","The completion handshake",[10,121,122],{},"Here is the mechanism every framework builds on. Nothing about it is Python-specific.",[124,125,126,133,139,150,153],"ol",{},[22,127,128,129,132],{},"When your shell starts, it sources a small script that registers a completion function for your command name — for example ",[38,130,131],{},"yourcli",".",[22,134,135,136,132],{},"The user types ",[38,137,138],{},"yourcli deploy --env \u003CTab>",[22,140,141,142,145,146,149],{},"The registered function re-runs your program with an environment variable set (Click and Typer use ",[38,143,144],{},"_YOURCLI_COMPLETE",") and the current words passed in. This is ",[26,147,148],{},"completion mode",": your program does not do its real work, it prints candidates.",[22,151,152],{},"Your program prints one candidate per line (plus a type marker) and exits.",[22,154,155],{},"The shell reads that list and either fills in the single match or shows the menu.",[10,157,158],{},"You can watch the handshake happen by hand, which is the single most useful debugging trick in this whole area:",[160,161,166],"pre",{"className":162,"code":163,"language":164,"meta":165,"style":165},"language-bash shiki shiki-themes github-light github-dark","$ _YOURCLI_COMPLETE=bash_complete COMP_WORDS=\"yourcli dep\" COMP_CWORD=1 yourcli\nplain,deploy\nplain,describe\n","bash","",[38,167,168,194,200],{"__ignoreMap":165},[169,170,173,177,181,184,187,191],"span",{"class":171,"line":172},"line",1,[169,174,176],{"class":175},"sScJk","$",[169,178,180],{"class":179},"sZZnC"," _YOURCLI_COMPLETE=bash_complete",[169,182,183],{"class":179}," COMP_WORDS=\"yourcli dep\"",[169,185,186],{"class":179}," COMP_CWORD=",[169,188,190],{"class":189},"sj4cs","1",[169,192,193],{"class":179}," yourcli\n",[169,195,197],{"class":171,"line":196},2,[169,198,199],{"class":175},"plain,deploy\n",[169,201,203],{"class":171,"line":202},3,[169,204,205],{"class":175},"plain,describe\n",[10,207,208,209,213,214,132],{},"That is Click's completion protocol running directly — no shell involved. If this prints your candidates, your Python side works and any problem is in the install step. If it prints nothing, the bug is in your code. Keeping those two failure modes separate is exactly why this section is split into an ",[106,210,212],{"href":211},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer\u002F","enabling guide"," and an ",[106,215,217],{"href":216},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish\u002F","installing guide",[14,219,221],{"id":220},"click-vs-typer-support-at-a-glance","Click vs Typer support at a glance",[10,223,224],{},"Both frameworks share the same completion engine — Typer is built on Click — so the underlying protocol is identical. What differs is how much is handed to you.",[10,226,227],{},"Typer wires up an install command for free. Every Typer app automatically gains two hidden options:",[160,229,233],{"className":230,"code":231,"language":232,"meta":165,"style":165},"language-python shiki shiki-themes github-light github-dark","# app.py\nimport typer\n\napp = typer.Typer()\n\n@app.command()\ndef deploy(env: str, replicas: int = 1) -> None:\n    \"\"\"Deploy the service.\"\"\"\n    typer.echo(f\"Deploying to {env} with {replicas} replicas\")\n\nif __name__ == \"__main__\":\n    app()\n","python",[38,234,235,241,251,257,269,274,283,319,325,362,367,384],{"__ignoreMap":165},[169,236,237],{"class":171,"line":172},[169,238,240],{"class":239},"sJ8bj","# app.py\n",[169,242,243,247],{"class":171,"line":196},[169,244,246],{"class":245},"szBVR","import",[169,248,250],{"class":249},"sVt8B"," typer\n",[169,252,253],{"class":171,"line":202},[169,254,256],{"emptyLinePlaceholder":255},true,"\n",[169,258,260,263,266],{"class":171,"line":259},4,[169,261,262],{"class":249},"app ",[169,264,265],{"class":245},"=",[169,267,268],{"class":249}," typer.Typer()\n",[169,270,272],{"class":171,"line":271},5,[169,273,256],{"emptyLinePlaceholder":255},[169,275,277,280],{"class":171,"line":276},6,[169,278,279],{"class":175},"@app.command",[169,281,282],{"class":249},"()\n",[169,284,286,289,292,295,298,301,304,307,310,313,316],{"class":171,"line":285},7,[169,287,288],{"class":245},"def",[169,290,291],{"class":175}," deploy",[169,293,294],{"class":249},"(env: ",[169,296,297],{"class":189},"str",[169,299,300],{"class":249},", replicas: ",[169,302,303],{"class":189},"int",[169,305,306],{"class":245}," =",[169,308,309],{"class":189}," 1",[169,311,312],{"class":249},") -> ",[169,314,315],{"class":189},"None",[169,317,318],{"class":249},":\n",[169,320,322],{"class":171,"line":321},8,[169,323,324],{"class":179},"    \"\"\"Deploy the service.\"\"\"\n",[169,326,328,331,334,337,340,343,346,349,351,354,356,359],{"class":171,"line":327},9,[169,329,330],{"class":249},"    typer.echo(",[169,332,333],{"class":245},"f",[169,335,336],{"class":179},"\"Deploying to ",[169,338,339],{"class":189},"{",[169,341,342],{"class":249},"env",[169,344,345],{"class":189},"}",[169,347,348],{"class":179}," with ",[169,350,339],{"class":189},[169,352,353],{"class":249},"replicas",[169,355,345],{"class":189},[169,357,358],{"class":179}," replicas\"",[169,360,361],{"class":249},")\n",[169,363,365],{"class":171,"line":364},10,[169,366,256],{"emptyLinePlaceholder":255},[169,368,370,373,376,379,382],{"class":171,"line":369},11,[169,371,372],{"class":245},"if",[169,374,375],{"class":189}," __name__",[169,377,378],{"class":245}," ==",[169,380,381],{"class":179}," \"__main__\"",[169,383,318],{"class":249},[169,385,387],{"class":171,"line":386},12,[169,388,389],{"class":249},"    app()\n",[160,391,393],{"className":162,"code":392,"language":164,"meta":165,"style":165},"$ python app.py --install-completion   # detects your shell and installs\n$ python app.py --show-completion       # prints the script to stdout instead\n",[38,394,395,411],{"__ignoreMap":165},[169,396,397,399,402,405,408],{"class":171,"line":172},[169,398,176],{"class":175},[169,400,401],{"class":179}," python",[169,403,404],{"class":179}," app.py",[169,406,407],{"class":189}," --install-completion",[169,409,410],{"class":239},"   # detects your shell and installs\n",[169,412,413,415,417,419,422],{"class":171,"line":196},[169,414,176],{"class":175},[169,416,401],{"class":179},[169,418,404],{"class":179},[169,420,421],{"class":189}," --show-completion",[169,423,424],{"class":239},"       # prints the script to stdout instead\n",[10,426,427],{},"Click has the same machinery but leaves the install step to you — you tell users to eval or source a generated script:",[160,429,431],{"className":230,"code":430,"language":232,"meta":165,"style":165},"# cli.py\nimport click\n\n@click.group()\ndef cli() -> None:\n    \"\"\"Example tool.\"\"\"\n\n@cli.command()\n@click.option(\"--env\", required=True)\ndef deploy(env: str) -> None:\n    \"\"\"Deploy the service.\"\"\"\n    click.echo(f\"Deploying to {env}\")\n\nif __name__ == \"__main__\":\n    cli()\n",[38,432,433,438,445,449,456,470,475,479,486,511,527,531,551,556,569],{"__ignoreMap":165},[169,434,435],{"class":171,"line":172},[169,436,437],{"class":239},"# cli.py\n",[169,439,440,442],{"class":171,"line":196},[169,441,246],{"class":245},[169,443,444],{"class":249}," click\n",[169,446,447],{"class":171,"line":202},[169,448,256],{"emptyLinePlaceholder":255},[169,450,451,454],{"class":171,"line":259},[169,452,453],{"class":175},"@click.group",[169,455,282],{"class":249},[169,457,458,460,463,466,468],{"class":171,"line":271},[169,459,288],{"class":245},[169,461,462],{"class":175}," cli",[169,464,465],{"class":249},"() -> ",[169,467,315],{"class":189},[169,469,318],{"class":249},[169,471,472],{"class":171,"line":276},[169,473,474],{"class":179},"    \"\"\"Example tool.\"\"\"\n",[169,476,477],{"class":171,"line":285},[169,478,256],{"emptyLinePlaceholder":255},[169,480,481,484],{"class":171,"line":321},[169,482,483],{"class":175},"@cli.command",[169,485,282],{"class":249},[169,487,488,491,494,497,500,504,506,509],{"class":171,"line":327},[169,489,490],{"class":175},"@click.option",[169,492,493],{"class":249},"(",[169,495,496],{"class":179},"\"--env\"",[169,498,499],{"class":249},", ",[169,501,503],{"class":502},"s4XuR","required",[169,505,265],{"class":245},[169,507,508],{"class":189},"True",[169,510,361],{"class":249},[169,512,513,515,517,519,521,523,525],{"class":171,"line":364},[169,514,288],{"class":245},[169,516,291],{"class":175},[169,518,294],{"class":249},[169,520,297],{"class":189},[169,522,312],{"class":249},[169,524,315],{"class":189},[169,526,318],{"class":249},[169,528,529],{"class":171,"line":369},[169,530,324],{"class":179},[169,532,533,536,538,540,542,544,546,549],{"class":171,"line":386},[169,534,535],{"class":249},"    click.echo(",[169,537,333],{"class":245},[169,539,336],{"class":179},[169,541,339],{"class":189},[169,543,342],{"class":249},[169,545,345],{"class":189},[169,547,548],{"class":179},"\"",[169,550,361],{"class":249},[169,552,554],{"class":171,"line":553},13,[169,555,256],{"emptyLinePlaceholder":255},[169,557,559,561,563,565,567],{"class":171,"line":558},14,[169,560,372],{"class":245},[169,562,375],{"class":189},[169,564,378],{"class":245},[169,566,381],{"class":179},[169,568,318],{"class":249},[169,570,572],{"class":171,"line":571},15,[169,573,574],{"class":249},"    cli()\n",[160,576,578],{"className":162,"code":577,"language":164,"meta":165,"style":165},"$ eval \"$(_CLI_COMPLETE=bash_source cli)\"   # activate for the current shell\n",[38,579,580],{"__ignoreMap":165},[169,581,582,584,587,590,593,595,598,601,604],{"class":171,"line":172},[169,583,176],{"class":175},[169,585,586],{"class":179}," eval",[169,588,589],{"class":179}," \"$(",[169,591,592],{"class":249},"_CLI_COMPLETE",[169,594,265],{"class":245},[169,596,597],{"class":179},"bash_source ",[169,599,600],{"class":175},"cli",[169,602,603],{"class":179},")\"",[169,605,606],{"class":239},"   # activate for the current shell\n",[10,608,609,610,614],{},"The trade-off between these two frameworks — convenience versus control — is the same one that runs through ",[106,611,613],{"href":612},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click: when to use each",". For completion specifically, Typer saves you writing an install command, and Click gives you a script you can drop into a package's post-install step.",[14,616,618],{"id":617},"static-vs-dynamic-completions","Static vs dynamic completions",[10,620,621,622,625,626,629],{},"Static completions are the ones your framework can compute from the command tree with no help: the names of subcommands, the option flags, and any ",[38,623,624],{},"click.Choice"," \u002F ",[38,627,628],{},"Enum"," values. You get these the moment completion is enabled — they cost nothing.",[10,631,632],{},"Dynamic completions are computed at Tab-time by a callback you write. Use them when the valid values live outside your source code:",[160,634,636],{"className":230,"code":635,"language":232,"meta":165,"style":165},"import click\n\ndef complete_env(ctx, param, incomplete):\n    # In real code, read from a config file or API.\n    known = [\"staging\", \"prod-eu\", \"prod-us\"]\n    return [e for e in known if e.startswith(incomplete)]\n\n@click.command()\n@click.option(\"--env\", shell_complete=complete_env)\ndef deploy(env: str) -> None:\n    click.echo(f\"Deploying to {env}\")\n",[38,637,638,644,648,658,663,689,714,718,725,743,759],{"__ignoreMap":165},[169,639,640,642],{"class":171,"line":172},[169,641,246],{"class":245},[169,643,444],{"class":249},[169,645,646],{"class":171,"line":196},[169,647,256],{"emptyLinePlaceholder":255},[169,649,650,652,655],{"class":171,"line":202},[169,651,288],{"class":245},[169,653,654],{"class":175}," complete_env",[169,656,657],{"class":249},"(ctx, param, incomplete):\n",[169,659,660],{"class":171,"line":259},[169,661,662],{"class":239},"    # In real code, read from a config file or API.\n",[169,664,665,668,670,673,676,678,681,683,686],{"class":171,"line":271},[169,666,667],{"class":249},"    known ",[169,669,265],{"class":245},[169,671,672],{"class":249}," [",[169,674,675],{"class":179},"\"staging\"",[169,677,499],{"class":249},[169,679,680],{"class":179},"\"prod-eu\"",[169,682,499],{"class":249},[169,684,685],{"class":179},"\"prod-us\"",[169,687,688],{"class":249},"]\n",[169,690,691,694,697,700,703,706,709,711],{"class":171,"line":276},[169,692,693],{"class":245},"    return",[169,695,696],{"class":249}," [e ",[169,698,699],{"class":245},"for",[169,701,702],{"class":249}," e ",[169,704,705],{"class":245},"in",[169,707,708],{"class":249}," known ",[169,710,372],{"class":245},[169,712,713],{"class":249}," e.startswith(incomplete)]\n",[169,715,716],{"class":171,"line":285},[169,717,256],{"emptyLinePlaceholder":255},[169,719,720,723],{"class":171,"line":321},[169,721,722],{"class":175},"@click.command",[169,724,282],{"class":249},[169,726,727,729,731,733,735,738,740],{"class":171,"line":327},[169,728,490],{"class":175},[169,730,493],{"class":249},[169,732,496],{"class":179},[169,734,499],{"class":249},[169,736,737],{"class":502},"shell_complete",[169,739,265],{"class":245},[169,741,742],{"class":249},"complete_env)\n",[169,744,745,747,749,751,753,755,757],{"class":171,"line":364},[169,746,288],{"class":245},[169,748,291],{"class":175},[169,750,294],{"class":249},[169,752,297],{"class":189},[169,754,312],{"class":249},[169,756,315],{"class":189},[169,758,318],{"class":249},[169,760,761,763,765,767,769,771,773,775],{"class":171,"line":369},[169,762,535],{"class":249},[169,764,333],{"class":245},[169,766,336],{"class":179},[169,768,339],{"class":189},[169,770,342],{"class":249},[169,772,345],{"class":189},[169,774,548],{"class":179},[169,776,361],{"class":249},[10,778,779,780,783,784,787,788,791,792,795,796,799,800,803,804,806,807,810,811,814],{},"Now ",[38,781,782],{},"yourcli deploy --env pro\u003CTab>"," offers ",[38,785,786],{},"prod-eu"," and ",[38,789,790],{},"prod-us",". The callback receives the partial word (",[38,793,794],{},"incomplete",") so you can filter server-side and keep the list short. Two rules keep dynamic completion pleasant: make the callback ",[33,797,798],{},"fast"," (it runs on every keypress-plus-Tab, so cache or bound any network call), and make it ",[33,801,802],{},"safe to fail"," (return an empty list rather than raising, so a broken suggestion never blocks the user's shell). The ",[106,805,212],{"href":211}," covers the equivalent ",[38,808,809],{},"autocompletion"," hook in Typer and how to return richer ",[38,812,813],{},"CompletionItem"," values with help text.",[14,816,818],{"id":817},"the-two-guides-in-this-section","The two guides in this section",[10,820,821],{},"Completion has two independent jobs, and this section gives each its own guide:",[19,823,824,843],{},[22,825,826,831,832,835,836,839,840,842],{},[33,827,828],{},[106,829,830],{"href":211},"Enabling tab completion in Click and Typer"," — the Python side. Turn completion on, add dynamic completions for arguments and options with Click's ",[38,833,834],{},"shell_complete="," and Typer's ",[38,837,838],{},"autocompletion=",", complete choices and enums automatically, and understand the ",[38,841,144],{}," trigger.",[22,844,845,850,851,854],{},[33,846,847],{},[106,848,849],{"href":216},"Installing shell completion for bash, zsh, fish"," — the shell side. Generate the completion script per shell, put it where each shell will load it, and troubleshoot completions that never fire (rehash, a fresh shell, ",[38,852,853],{},"PATH",", and caching).",[10,856,857,858,860],{},"Read them in that order: get the handshake working in Python first (verify with the ",[38,859,144],{}," trick above), then install the script so it runs automatically.",[14,862,864],{"id":863},"production-notes","Production notes",[19,866,867,877,886,905],{},[22,868,869,872,873,132],{},[33,870,871],{},"Completion runs your import path on every Tab."," A slow startup makes completion feel laggy, so keep top-level imports light — the same discipline as ",[106,874,876],{"href":875},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI startup performance and lazy loading",[22,878,879,882,883,885],{},[33,880,881],{},"Never print to stdout during completion mode."," Anything your program writes to stdout while ",[38,884,144],{}," is set is parsed as a candidate. Route stray output to stderr or guard it.",[22,887,888,891,892,894,895,897,898,625,901,904],{},[33,889,890],{},"Pin your framework."," The ",[38,893,737],{}," API landed in Click 8.0 and replaced the older ",[38,896,809],{}," argument; Typer's install flags stabilised around 0.12. Pin ",[38,899,900],{},"click>=8.1",[38,902,903],{},"typer>=0.12"," and test against the version you ship.",[22,906,907,910,911,915],{},[33,908,909],{},"The installed name matters."," The completion script keys off your console-script name, so it must match your ",[106,912,914],{"href":913},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","entry point",". Rename the command and you must regenerate the script.",[14,917,919],{"id":918},"related","Related",[19,921,922,927,932,938,943],{},[22,923,924,926],{},[106,925,830],{"href":211}," — the Python side, including dynamic completions.",[22,928,929,931],{},[106,930,849],{"href":216}," — the per-shell install and troubleshooting.",[22,933,934,937],{},[106,935,936],{"href":113},"Interactive terminal UI with Rich"," — the other half of a great CLI experience.",[22,939,940,942],{},[106,941,613],{"href":612}," — how the two frameworks differ, completion included.",[22,944,945,949],{},[106,946,948],{"href":947},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing for Python CLIs"," — the parent track on input and UX.",[951,952,953],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":165,"searchDepth":196,"depth":196,"links":955},[956,957,958,959,960,961,962,963],{"id":16,"depth":196,"text":17},{"id":74,"depth":196,"text":75},{"id":118,"depth":196,"text":119},{"id":220,"depth":196,"text":221},{"id":617,"depth":196,"text":618},{"id":817,"depth":196,"text":818},{"id":863,"depth":196,"text":864},{"id":918,"depth":196,"text":919},"2026-07-05","Add tab completion to Python CLIs: how completion works in Click and Typer, generate scripts for bash, zsh, and fish, and offer dynamic suggestions.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis",{"title":5,"description":965},"advanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Findex",[974,975,976,164,600],"completion","click","typer","EE6PIOGSbFMs1qUdp-pNjt5lRmQdB0f5cz1yOJvWnhM",[979,982,985,988,991,994,997,1000,1003,1006,1008,1011,1014,1017,1018,1021,1024,1027,1030,1033,1036,1039,1042,1045,1048,1051,1054,1057,1060,1063,1066,1069,1072,1075,1078,1081,1084,1087,1090,1093,1096,1099,1102,1105,1108,1111,1114,1117,1120,1123,1126],{"path":980,"title":981},"\u002Fabout","About Python CLI Toolcraft",{"path":983,"title":984},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":986,"title":987},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":989,"title":990},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":992,"title":993},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":995,"title":996},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":998,"title":999},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1001,"title":1002},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1004,"title":1005},"\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":1007,"title":948},"\u002Fadvanced-input-parsing-user-experience",{"path":1009,"title":1010},"\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":1012,"title":1013},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1015,"title":1016},"\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":970,"title":5},{"path":1019,"title":1020},"\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":1022,"title":1023},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1025,"title":1026},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1028,"title":1029},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1031,"title":1032},"\u002F","Python CLI Toolcraft",{"path":1034,"title":1035},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1037,"title":1038},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1040,"title":1041},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1043,"title":1044},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1046,"title":1047},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1049,"title":1050},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1052,"title":1053},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1055,"title":1056},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1058,"title":1059},"\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":1061,"title":1062},"\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":1064,"title":1065},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1067,"title":1068},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1070,"title":1071},"\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":1073,"title":1074},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1076,"title":1077},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1079,"title":1080},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1082,"title":1083},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1085,"title":1086},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1088,"title":1089},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1091,"title":1092},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1094,"title":1095},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1097,"title":1098},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1100,"title":1101},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1103,"title":1104},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1106,"title":1107},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1109,"title":1110},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1112,"title":1113},"\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":1115,"title":1116},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1118,"title":1119},"\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":1121,"title":1122},"\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":1124,"title":1125},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1127,"title":1128},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867196]