[{"data":1,"prerenderedAt":2078},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer\u002F":3,"content-directory":1927},{"id":4,"title":5,"body":6,"date":1911,"description":1912,"difficulty":1913,"draft":1914,"extension":1915,"meta":1916,"navigation":145,"path":1917,"seo":1918,"stem":1919,"tags":1920,"updated":1911,"__hash__":1926},"content\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer\u002Findex.md","Enabling Tab Completion in Click and Typer",{"type":7,"value":8,"toc":1899},"minimark",[9,24,29,104,108,111,351,389,418,430,434,437,580,587,659,698,702,709,789,802,925,939,943,950,1110,1124,1130,1258,1261,1265,1273,1512,1523,1534,1538,1552,1726,1745,1749,1752,1791,1801,1805,1863,1867,1895],[10,11,12,13,17,18,23],"p",{},"This is the Python side of shell completion: everything you do in your code so that pressing Tab produces useful suggestions. Both Click and Typer ship a completion engine, so subcommand and option names complete for free the moment it is switched on. The work you actually write is the ",[14,15,16],"em",{},"dynamic"," part — completing an argument from a live data source, a config file, or the other arguments already on the line. This guide turns completion on in both frameworks and then builds up dynamic completions, choice\u002Fenum completion, and a one-command install experience. Installing the generated script into each shell is the ",[19,20,22],"a",{"href":21},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish\u002F","sibling guide","; here we stay in Python.",[25,26,28],"h2",{"id":27},"tldr","TL;DR",[30,31,32,49,59,73,90],"ul",{},[33,34,35,39,40,44,45,48],"li",{},[36,37,38],"strong",{},"Typer:"," completion is built in. Every app gains ",[41,42,43],"code",{},"--install-completion"," and ",[41,46,47],{},"--show-completion"," with no code from you.",[33,50,51,54,55,58],{},[36,52,53],{},"Click:"," completion is built in too, but you activate it with ",[41,56,57],{},"eval \"$(_YOURCLI_COMPLETE=bash_source yourcli)\""," — the framework prints the script, you decide where it goes.",[33,60,61,64,65,68,69,72],{},[36,62,63],{},"Dynamic completion in Click:"," pass ",[41,66,67],{},"shell_complete=callback"," to an option\u002Fargument; the callback returns strings or ",[41,70,71],{},"CompletionItem","s.",[33,74,75,64,78,81,82,85,86,89],{},[36,76,77],{},"Dynamic completion in Typer:",[41,79,80],{},"autocompletion=callback"," to ",[41,83,84],{},"typer.Option","\u002F",[41,87,88],{},"typer.Argument",".",[33,91,92,95,96,99,100,103],{},[36,93,94],{},"Choices and enums complete automatically"," — ",[41,97,98],{},"click.Choice([...])"," and a Python ",[41,101,102],{},"Enum"," in Typer need no callback at all.",[25,105,107],{"id":106},"typer-completion-with-zero-code","Typer: completion with zero code",[10,109,110],{},"A Typer app has completion wired in. Define commands as usual and the framework already knows how to install a completion script:",[112,113,118],"pre",{"className":114,"code":115,"language":116,"meta":117,"style":117},"language-python shiki shiki-themes github-light github-dark","# app.py\nimport typer\n\napp = typer.Typer(help=\"Deploy things.\")\n\n@app.command()\ndef deploy(env: str, replicas: int = 1) -> None:\n    \"\"\"Deploy the service to ENV.\"\"\"\n    typer.echo(f\"Deploying to {env} with {replicas} replicas\")\n\n@app.command()\ndef status(env: str) -> None:\n    \"\"\"Show deployment status for ENV.\"\"\"\n    typer.echo(f\"Status for {env}\")\n\nif __name__ == \"__main__\":\n    app()\n","python","",[41,119,120,129,140,147,172,177,187,224,230,266,271,278,296,302,323,328,345],{"__ignoreMap":117},[121,122,125],"span",{"class":123,"line":124},"line",1,[121,126,128],{"class":127},"sJ8bj","# app.py\n",[121,130,132,136],{"class":123,"line":131},2,[121,133,135],{"class":134},"szBVR","import",[121,137,139],{"class":138},"sVt8B"," typer\n",[121,141,143],{"class":123,"line":142},3,[121,144,146],{"emptyLinePlaceholder":145},true,"\n",[121,148,150,153,156,159,163,165,169],{"class":123,"line":149},4,[121,151,152],{"class":138},"app ",[121,154,155],{"class":134},"=",[121,157,158],{"class":138}," typer.Typer(",[121,160,162],{"class":161},"s4XuR","help",[121,164,155],{"class":134},[121,166,168],{"class":167},"sZZnC","\"Deploy things.\"",[121,170,171],{"class":138},")\n",[121,173,175],{"class":123,"line":174},5,[121,176,146],{"emptyLinePlaceholder":145},[121,178,180,184],{"class":123,"line":179},6,[121,181,183],{"class":182},"sScJk","@app.command",[121,185,186],{"class":138},"()\n",[121,188,190,193,196,199,203,206,209,212,215,218,221],{"class":123,"line":189},7,[121,191,192],{"class":134},"def",[121,194,195],{"class":182}," deploy",[121,197,198],{"class":138},"(env: ",[121,200,202],{"class":201},"sj4cs","str",[121,204,205],{"class":138},", replicas: ",[121,207,208],{"class":201},"int",[121,210,211],{"class":134}," =",[121,213,214],{"class":201}," 1",[121,216,217],{"class":138},") -> ",[121,219,220],{"class":201},"None",[121,222,223],{"class":138},":\n",[121,225,227],{"class":123,"line":226},8,[121,228,229],{"class":167},"    \"\"\"Deploy the service to ENV.\"\"\"\n",[121,231,233,236,239,242,245,248,251,254,256,259,261,264],{"class":123,"line":232},9,[121,234,235],{"class":138},"    typer.echo(",[121,237,238],{"class":134},"f",[121,240,241],{"class":167},"\"Deploying to ",[121,243,244],{"class":201},"{",[121,246,247],{"class":138},"env",[121,249,250],{"class":201},"}",[121,252,253],{"class":167}," with ",[121,255,244],{"class":201},[121,257,258],{"class":138},"replicas",[121,260,250],{"class":201},[121,262,263],{"class":167}," replicas\"",[121,265,171],{"class":138},[121,267,269],{"class":123,"line":268},10,[121,270,146],{"emptyLinePlaceholder":145},[121,272,274,276],{"class":123,"line":273},11,[121,275,183],{"class":182},[121,277,186],{"class":138},[121,279,281,283,286,288,290,292,294],{"class":123,"line":280},12,[121,282,192],{"class":134},[121,284,285],{"class":182}," status",[121,287,198],{"class":138},[121,289,202],{"class":201},[121,291,217],{"class":138},[121,293,220],{"class":201},[121,295,223],{"class":138},[121,297,299],{"class":123,"line":298},13,[121,300,301],{"class":167},"    \"\"\"Show deployment status for ENV.\"\"\"\n",[121,303,305,307,309,312,314,316,318,321],{"class":123,"line":304},14,[121,306,235],{"class":138},[121,308,238],{"class":134},[121,310,311],{"class":167},"\"Status for ",[121,313,244],{"class":201},[121,315,247],{"class":138},[121,317,250],{"class":201},[121,319,320],{"class":167},"\"",[121,322,171],{"class":138},[121,324,326],{"class":123,"line":325},15,[121,327,146],{"emptyLinePlaceholder":145},[121,329,331,334,337,340,343],{"class":123,"line":330},16,[121,332,333],{"class":134},"if",[121,335,336],{"class":201}," __name__",[121,338,339],{"class":134}," ==",[121,341,342],{"class":167}," \"__main__\"",[121,344,223],{"class":138},[121,346,348],{"class":123,"line":347},17,[121,349,350],{"class":138},"    app()\n",[112,352,356],{"className":353,"code":354,"language":355,"meta":117,"style":117},"language-bash shiki shiki-themes github-light github-dark","$ python app.py --install-completion    # writes + registers the script for your shell\n$ python app.py --show-completion        # prints the script to stdout so you can inspect it\n","bash",[41,357,358,375],{"__ignoreMap":117},[121,359,360,363,366,369,372],{"class":123,"line":124},[121,361,362],{"class":182},"$",[121,364,365],{"class":167}," python",[121,367,368],{"class":167}," app.py",[121,370,371],{"class":201}," --install-completion",[121,373,374],{"class":127},"    # writes + registers the script for your shell\n",[121,376,377,379,381,383,386],{"class":123,"line":131},[121,378,362],{"class":182},[121,380,365],{"class":167},[121,382,368],{"class":167},[121,384,385],{"class":201}," --show-completion",[121,387,388],{"class":127},"        # prints the script to stdout so you can inspect it\n",[10,390,391,393,394,396,397,400,401,44,404,407,408,400,411,44,414,417],{},[41,392,43],{}," detects your current shell, writes the completion script into the right location, and (for zsh\u002Ffish) makes sure it will be sourced on the next shell start. ",[41,395,47],{}," prints the same script without touching your system — handy for packaging it or for feeding it to a system directory yourself. Once installed, ",[41,398,399],{},"python app.py \u003CTab>"," offers ",[41,402,403],{},"deploy",[41,405,406],{},"status",", and ",[41,409,410],{},"python app.py deploy --\u003CTab>",[41,412,413],{},"--replicas",[41,415,416],{},"--help",". You wrote no completion code to get any of that.",[10,419,420,421,424,425,429],{},"For a real tool you would expose it through a console script rather than ",[41,422,423],{},"python app.py",", so completion keys off the installed command name (see ",[19,426,428],{"href":427},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","entry points for Python CLIs",").",[25,431,433],{"id":432},"click-turning-completion-on","Click: turning completion on",[10,435,436],{},"Click uses the same engine but does not add an install command — you activate completion by evaluating the script it generates. Given this app:",[112,438,440],{"className":114,"code":439,"language":116,"meta":117,"style":117},"# 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",[41,441,442,447,454,458,465,479,484,488,495,519,535,540,559,563,575],{"__ignoreMap":117},[121,443,444],{"class":123,"line":124},[121,445,446],{"class":127},"# cli.py\n",[121,448,449,451],{"class":123,"line":131},[121,450,135],{"class":134},[121,452,453],{"class":138}," click\n",[121,455,456],{"class":123,"line":142},[121,457,146],{"emptyLinePlaceholder":145},[121,459,460,463],{"class":123,"line":149},[121,461,462],{"class":182},"@click.group",[121,464,186],{"class":138},[121,466,467,469,472,475,477],{"class":123,"line":174},[121,468,192],{"class":134},[121,470,471],{"class":182}," cli",[121,473,474],{"class":138},"() -> ",[121,476,220],{"class":201},[121,478,223],{"class":138},[121,480,481],{"class":123,"line":179},[121,482,483],{"class":167},"    \"\"\"Example tool.\"\"\"\n",[121,485,486],{"class":123,"line":189},[121,487,146],{"emptyLinePlaceholder":145},[121,489,490,493],{"class":123,"line":226},[121,491,492],{"class":182},"@cli.command",[121,494,186],{"class":138},[121,496,497,500,503,506,509,512,514,517],{"class":123,"line":232},[121,498,499],{"class":182},"@click.option",[121,501,502],{"class":138},"(",[121,504,505],{"class":167},"\"--env\"",[121,507,508],{"class":138},", ",[121,510,511],{"class":161},"required",[121,513,155],{"class":134},[121,515,516],{"class":201},"True",[121,518,171],{"class":138},[121,520,521,523,525,527,529,531,533],{"class":123,"line":268},[121,522,192],{"class":134},[121,524,195],{"class":182},[121,526,198],{"class":138},[121,528,202],{"class":201},[121,530,217],{"class":138},[121,532,220],{"class":201},[121,534,223],{"class":138},[121,536,537],{"class":123,"line":273},[121,538,539],{"class":167},"    \"\"\"Deploy the service.\"\"\"\n",[121,541,542,545,547,549,551,553,555,557],{"class":123,"line":280},[121,543,544],{"class":138},"    click.echo(",[121,546,238],{"class":134},[121,548,241],{"class":167},[121,550,244],{"class":201},[121,552,247],{"class":138},[121,554,250],{"class":201},[121,556,320],{"class":167},[121,558,171],{"class":138},[121,560,561],{"class":123,"line":298},[121,562,146],{"emptyLinePlaceholder":145},[121,564,565,567,569,571,573],{"class":123,"line":304},[121,566,333],{"class":134},[121,568,336],{"class":201},[121,570,339],{"class":134},[121,572,342],{"class":167},[121,574,223],{"class":138},[121,576,577],{"class":123,"line":325},[121,578,579],{"class":138},"    cli()\n",[10,581,582,583,586],{},"If it is installed as the console script ",[41,584,585],{},"yourcli",", a user activates completion for the current shell like this:",[112,588,590],{"className":353,"code":589,"language":355,"meta":117,"style":117},"$ eval \"$(_YOURCLI_COMPLETE=bash_source yourcli)\"     # bash\n$ eval \"$(_YOURCLI_COMPLETE=zsh_source yourcli)\"      # zsh\n$ _YOURCLI_COMPLETE=fish_source yourcli | source      # fish\n",[41,591,592,618,640],{"__ignoreMap":117},[121,593,594,596,599,602,605,607,610,612,615],{"class":123,"line":124},[121,595,362],{"class":182},[121,597,598],{"class":167}," eval",[121,600,601],{"class":167}," \"$(",[121,603,604],{"class":138},"_YOURCLI_COMPLETE",[121,606,155],{"class":134},[121,608,609],{"class":167},"bash_source ",[121,611,585],{"class":182},[121,613,614],{"class":167},")\"",[121,616,617],{"class":127},"     # bash\n",[121,619,620,622,624,626,628,630,633,635,637],{"class":123,"line":131},[121,621,362],{"class":182},[121,623,598],{"class":167},[121,625,601],{"class":167},[121,627,604],{"class":138},[121,629,155],{"class":134},[121,631,632],{"class":167},"zsh_source ",[121,634,585],{"class":182},[121,636,614],{"class":167},[121,638,639],{"class":127},"      # zsh\n",[121,641,642,644,647,650,653,656],{"class":123,"line":142},[121,643,362],{"class":182},[121,645,646],{"class":167}," _YOURCLI_COMPLETE=fish_source",[121,648,649],{"class":167}," yourcli",[121,651,652],{"class":134}," |",[121,654,655],{"class":201}," source",[121,657,658],{"class":127},"      # fish\n",[10,660,661,662,664,665,668,669,671,672,407,674,671,677,680,681,508,684,508,687,690,691,694,695,89],{},"The ",[41,663,604],{}," variable is Click's trigger. Its name is derived from your command: uppercase the console-script name, replace hyphens with underscores, and append ",[41,666,667],{},"_COMPLETE",". So ",[41,670,585],{}," → ",[41,673,604],{},[41,675,676],{},"my-tool",[41,678,679],{},"_MY_TOOL_COMPLETE",". The value (",[41,682,683],{},"bash_source",[41,685,686],{},"zsh_source",[41,688,689],{},"fish_source",") selects which shell's script to print. Persisting that ",[41,692,693],{},"eval"," into a startup file — or shipping a generated script — is the job of the ",[19,696,697],{"href":21},"installing guide",[25,699,701],{"id":700},"completing-choices-and-enums-for-free","Completing choices and enums for free",[10,703,704,705,708],{},"Before writing any callback, know that fixed value sets complete automatically. In Click, a ",[41,706,707],{},"click.Choice"," completes its members:",[112,710,712],{"className":114,"code":711,"language":116,"meta":117,"style":117},"import click\n\n@click.command()\n@click.option(\"--level\", type=click.Choice([\"debug\", \"info\", \"warn\"]))\ndef run(level: str) -> None:\n    click.echo(level)\n",[41,713,714,720,724,731,766,784],{"__ignoreMap":117},[121,715,716,718],{"class":123,"line":124},[121,717,135],{"class":134},[121,719,453],{"class":138},[121,721,722],{"class":123,"line":131},[121,723,146],{"emptyLinePlaceholder":145},[121,725,726,729],{"class":123,"line":142},[121,727,728],{"class":182},"@click.command",[121,730,186],{"class":138},[121,732,733,735,737,740,742,745,747,750,753,755,758,760,763],{"class":123,"line":149},[121,734,499],{"class":182},[121,736,502],{"class":138},[121,738,739],{"class":167},"\"--level\"",[121,741,508],{"class":138},[121,743,744],{"class":161},"type",[121,746,155],{"class":134},[121,748,749],{"class":138},"click.Choice([",[121,751,752],{"class":167},"\"debug\"",[121,754,508],{"class":138},[121,756,757],{"class":167},"\"info\"",[121,759,508],{"class":138},[121,761,762],{"class":167},"\"warn\"",[121,764,765],{"class":138},"]))\n",[121,767,768,770,773,776,778,780,782],{"class":123,"line":174},[121,769,192],{"class":134},[121,771,772],{"class":182}," run",[121,774,775],{"class":138},"(level: ",[121,777,202],{"class":201},[121,779,217],{"class":138},[121,781,220],{"class":201},[121,783,223],{"class":138},[121,785,786],{"class":123,"line":179},[121,787,788],{"class":138},"    click.echo(level)\n",[10,790,791,794,795,798,799,801],{},[41,792,793],{},"yourcli run --level d\u003CTab>"," fills in ",[41,796,797],{},"debug",". In Typer, a Python ",[41,800,102],{}," does the same and also gives you validation and type safety:",[112,803,805],{"className":114,"code":804,"language":116,"meta":117,"style":117},"import typer\nfrom enum import Enum\n\nclass Level(str, Enum):\n    debug = \"debug\"\n    info = \"info\"\n    warn = \"warn\"\n\napp = typer.Typer()\n\n@app.command()\ndef run(level: Level = Level.info) -> None:\n    typer.echo(level.value)\n",[41,806,807,813,826,830,849,859,869,879,883,892,896,902,920],{"__ignoreMap":117},[121,808,809,811],{"class":123,"line":124},[121,810,135],{"class":134},[121,812,139],{"class":138},[121,814,815,818,821,823],{"class":123,"line":131},[121,816,817],{"class":134},"from",[121,819,820],{"class":138}," enum ",[121,822,135],{"class":134},[121,824,825],{"class":138}," Enum\n",[121,827,828],{"class":123,"line":142},[121,829,146],{"emptyLinePlaceholder":145},[121,831,832,835,838,840,842,844,846],{"class":123,"line":149},[121,833,834],{"class":134},"class",[121,836,837],{"class":182}," Level",[121,839,502],{"class":138},[121,841,202],{"class":201},[121,843,508],{"class":138},[121,845,102],{"class":182},[121,847,848],{"class":138},"):\n",[121,850,851,854,856],{"class":123,"line":174},[121,852,853],{"class":138},"    debug ",[121,855,155],{"class":134},[121,857,858],{"class":167}," \"debug\"\n",[121,860,861,864,866],{"class":123,"line":179},[121,862,863],{"class":138},"    info ",[121,865,155],{"class":134},[121,867,868],{"class":167}," \"info\"\n",[121,870,871,874,876],{"class":123,"line":189},[121,872,873],{"class":138},"    warn ",[121,875,155],{"class":134},[121,877,878],{"class":167}," \"warn\"\n",[121,880,881],{"class":123,"line":226},[121,882,146],{"emptyLinePlaceholder":145},[121,884,885,887,889],{"class":123,"line":232},[121,886,152],{"class":138},[121,888,155],{"class":134},[121,890,891],{"class":138}," typer.Typer()\n",[121,893,894],{"class":123,"line":268},[121,895,146],{"emptyLinePlaceholder":145},[121,897,898,900],{"class":123,"line":273},[121,899,183],{"class":182},[121,901,186],{"class":138},[121,903,904,906,908,911,913,916,918],{"class":123,"line":280},[121,905,192],{"class":134},[121,907,772],{"class":182},[121,909,910],{"class":138},"(level: Level ",[121,912,155],{"class":134},[121,914,915],{"class":138}," Level.info) -> ",[121,917,220],{"class":201},[121,919,223],{"class":138},[121,921,922],{"class":123,"line":298},[121,923,924],{"class":138},"    typer.echo(level.value)\n",[10,926,927,928,931,932,935,936,938],{},"Reach for a callback only when the valid values are ",[14,929,930],{},"not"," knowable at code-writing time. If they are a fixed list, a ",[41,933,934],{},"Choice"," or ",[41,937,102],{}," is less code and completes just as well.",[25,940,942],{"id":941},"dynamic-completion-in-click-with-shell_complete","Dynamic completion in Click with shell_complete",[10,944,945,946,949],{},"When the valid values live outside your source — deploy targets in a config file, dataset IDs from an API, branch names from git — write a ",[41,947,948],{},"shell_complete"," callback. It receives the Click context, the parameter, and the partial word the user has typed so far, and returns the candidates:",[112,951,953],{"className":114,"code":952,"language":116,"meta":117,"style":117},"import click\n\ndef complete_env(ctx: click.Context, param: click.Parameter, incomplete: str) -> list[str]:\n    known = [\"staging\", \"prod-eu\", \"prod-us\", \"prod-ap\"]\n    return [e for e in known if e.startswith(incomplete)]\n\n@click.command()\n@click.option(\"--env\", shell_complete=complete_env, required=True)\ndef deploy(env: str) -> None:\n    click.echo(f\"Deploying to {env}\")\n",[41,954,955,961,965,985,1016,1041,1045,1051,1076,1092],{"__ignoreMap":117},[121,956,957,959],{"class":123,"line":124},[121,958,135],{"class":134},[121,960,453],{"class":138},[121,962,963],{"class":123,"line":131},[121,964,146],{"emptyLinePlaceholder":145},[121,966,967,969,972,975,977,980,982],{"class":123,"line":142},[121,968,192],{"class":134},[121,970,971],{"class":182}," complete_env",[121,973,974],{"class":138},"(ctx: click.Context, param: click.Parameter, incomplete: ",[121,976,202],{"class":201},[121,978,979],{"class":138},") -> list[",[121,981,202],{"class":201},[121,983,984],{"class":138},"]:\n",[121,986,987,990,992,995,998,1000,1003,1005,1008,1010,1013],{"class":123,"line":149},[121,988,989],{"class":138},"    known ",[121,991,155],{"class":134},[121,993,994],{"class":138}," [",[121,996,997],{"class":167},"\"staging\"",[121,999,508],{"class":138},[121,1001,1002],{"class":167},"\"prod-eu\"",[121,1004,508],{"class":138},[121,1006,1007],{"class":167},"\"prod-us\"",[121,1009,508],{"class":138},[121,1011,1012],{"class":167},"\"prod-ap\"",[121,1014,1015],{"class":138},"]\n",[121,1017,1018,1021,1024,1027,1030,1033,1036,1038],{"class":123,"line":174},[121,1019,1020],{"class":134},"    return",[121,1022,1023],{"class":138}," [e ",[121,1025,1026],{"class":134},"for",[121,1028,1029],{"class":138}," e ",[121,1031,1032],{"class":134},"in",[121,1034,1035],{"class":138}," known ",[121,1037,333],{"class":134},[121,1039,1040],{"class":138}," e.startswith(incomplete)]\n",[121,1042,1043],{"class":123,"line":179},[121,1044,146],{"emptyLinePlaceholder":145},[121,1046,1047,1049],{"class":123,"line":189},[121,1048,728],{"class":182},[121,1050,186],{"class":138},[121,1052,1053,1055,1057,1059,1061,1063,1065,1068,1070,1072,1074],{"class":123,"line":226},[121,1054,499],{"class":182},[121,1056,502],{"class":138},[121,1058,505],{"class":167},[121,1060,508],{"class":138},[121,1062,948],{"class":161},[121,1064,155],{"class":134},[121,1066,1067],{"class":138},"complete_env, ",[121,1069,511],{"class":161},[121,1071,155],{"class":134},[121,1073,516],{"class":201},[121,1075,171],{"class":138},[121,1077,1078,1080,1082,1084,1086,1088,1090],{"class":123,"line":232},[121,1079,192],{"class":134},[121,1081,195],{"class":182},[121,1083,198],{"class":138},[121,1085,202],{"class":201},[121,1087,217],{"class":138},[121,1089,220],{"class":201},[121,1091,223],{"class":138},[121,1093,1094,1096,1098,1100,1102,1104,1106,1108],{"class":123,"line":268},[121,1095,544],{"class":138},[121,1097,238],{"class":134},[121,1099,241],{"class":167},[121,1101,244],{"class":201},[121,1103,247],{"class":138},[121,1105,250],{"class":201},[121,1107,320],{"class":167},[121,1109,171],{"class":138},[10,1111,1112,1115,1116,1119,1120,1123],{},[41,1113,1114],{},"yourcli deploy --env prod-\u003CTab>"," now offers the three ",[41,1117,1118],{},"prod-*"," targets. Filtering on ",[41,1121,1122],{},"incomplete"," yourself keeps the returned list short, which matters because the shell shows everything you return.",[10,1125,1126,1127,1129],{},"To attach help text to each suggestion, return ",[41,1128,71],{}," objects instead of bare strings. The second argument becomes the description the shell shows alongside the value:",[112,1131,1133],{"className":114,"code":1132,"language":116,"meta":117,"style":117},"from click.shell_completion import CompletionItem\n\ndef complete_env(ctx, param, incomplete):\n    targets = {\n        \"staging\": \"shared pre-prod\",\n        \"prod-eu\": \"Frankfurt\",\n        \"prod-us\": \"N. Virginia\",\n    }\n    return [\n        CompletionItem(name, help=desc)\n        for name, desc in targets.items()\n        if name.startswith(incomplete)\n    ]\n",[41,1134,1135,1147,1151,1160,1170,1184,1196,1208,1213,1220,1232,1245,1253],{"__ignoreMap":117},[121,1136,1137,1139,1142,1144],{"class":123,"line":124},[121,1138,817],{"class":134},[121,1140,1141],{"class":138}," click.shell_completion ",[121,1143,135],{"class":134},[121,1145,1146],{"class":138}," CompletionItem\n",[121,1148,1149],{"class":123,"line":131},[121,1150,146],{"emptyLinePlaceholder":145},[121,1152,1153,1155,1157],{"class":123,"line":142},[121,1154,192],{"class":134},[121,1156,971],{"class":182},[121,1158,1159],{"class":138},"(ctx, param, incomplete):\n",[121,1161,1162,1165,1167],{"class":123,"line":149},[121,1163,1164],{"class":138},"    targets ",[121,1166,155],{"class":134},[121,1168,1169],{"class":138}," {\n",[121,1171,1172,1175,1178,1181],{"class":123,"line":174},[121,1173,1174],{"class":167},"        \"staging\"",[121,1176,1177],{"class":138},": ",[121,1179,1180],{"class":167},"\"shared pre-prod\"",[121,1182,1183],{"class":138},",\n",[121,1185,1186,1189,1191,1194],{"class":123,"line":179},[121,1187,1188],{"class":167},"        \"prod-eu\"",[121,1190,1177],{"class":138},[121,1192,1193],{"class":167},"\"Frankfurt\"",[121,1195,1183],{"class":138},[121,1197,1198,1201,1203,1206],{"class":123,"line":189},[121,1199,1200],{"class":167},"        \"prod-us\"",[121,1202,1177],{"class":138},[121,1204,1205],{"class":167},"\"N. Virginia\"",[121,1207,1183],{"class":138},[121,1209,1210],{"class":123,"line":226},[121,1211,1212],{"class":138},"    }\n",[121,1214,1215,1217],{"class":123,"line":232},[121,1216,1020],{"class":134},[121,1218,1219],{"class":138}," [\n",[121,1221,1222,1225,1227,1229],{"class":123,"line":268},[121,1223,1224],{"class":138},"        CompletionItem(name, ",[121,1226,162],{"class":161},[121,1228,155],{"class":134},[121,1230,1231],{"class":138},"desc)\n",[121,1233,1234,1237,1240,1242],{"class":123,"line":273},[121,1235,1236],{"class":134},"        for",[121,1238,1239],{"class":138}," name, desc ",[121,1241,1032],{"class":134},[121,1243,1244],{"class":138}," targets.items()\n",[121,1246,1247,1250],{"class":123,"line":280},[121,1248,1249],{"class":134},"        if",[121,1251,1252],{"class":138}," name.startswith(incomplete)\n",[121,1254,1255],{"class":123,"line":298},[121,1256,1257],{"class":138},"    ]\n",[10,1259,1260],{},"zsh and fish render that help text next to each candidate; bash shows the value alone. That is a shell limitation, not a bug in your code.",[25,1262,1264],{"id":1263},"dynamic-completion-from-a-real-data-source","Dynamic completion from a real data source",[10,1266,1267,1268,1272],{},"The point of dynamic completion is reaching outside your program. Here the targets come from a YAML config, so completion always reflects what the user has actually configured — a natural fit with ",[19,1269,1271],{"href":1270},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002F","handling config files and env vars",":",[112,1274,1276],{"className":114,"code":1275,"language":116,"meta":117,"style":117},"import click\nimport yaml\nfrom pathlib import Path\n\ndef load_targets() -> list[str]:\n    cfg = Path.home() \u002F \".config\" \u002F \"yourcli\" \u002F \"targets.yaml\"\n    try:\n        data = yaml.safe_load(cfg.read_text()) or {}\n        return list(data.get(\"environments\", {}))\n    except (OSError, yaml.YAMLError):\n        return []   # fail closed: a bad config must never break the shell\n\ndef complete_env(ctx, param, incomplete):\n    return [e for e in load_targets() if e.startswith(incomplete)]\n\n@click.command()\n@click.option(\"--env\", shell_complete=complete_env, required=True)\ndef deploy(env: str) -> None:\n    click.echo(f\"Deploying to {env}\")\n",[41,1277,1278,1284,1291,1303,1307,1321,1347,1354,1370,1387,1401,1411,1415,1423,1442,1446,1452,1476,1493],{"__ignoreMap":117},[121,1279,1280,1282],{"class":123,"line":124},[121,1281,135],{"class":134},[121,1283,453],{"class":138},[121,1285,1286,1288],{"class":123,"line":131},[121,1287,135],{"class":134},[121,1289,1290],{"class":138}," yaml\n",[121,1292,1293,1295,1298,1300],{"class":123,"line":142},[121,1294,817],{"class":134},[121,1296,1297],{"class":138}," pathlib ",[121,1299,135],{"class":134},[121,1301,1302],{"class":138}," Path\n",[121,1304,1305],{"class":123,"line":149},[121,1306,146],{"emptyLinePlaceholder":145},[121,1308,1309,1311,1314,1317,1319],{"class":123,"line":174},[121,1310,192],{"class":134},[121,1312,1313],{"class":182}," load_targets",[121,1315,1316],{"class":138},"() -> list[",[121,1318,202],{"class":201},[121,1320,984],{"class":138},[121,1322,1323,1326,1328,1331,1333,1336,1339,1342,1344],{"class":123,"line":179},[121,1324,1325],{"class":138},"    cfg ",[121,1327,155],{"class":134},[121,1329,1330],{"class":138}," Path.home() ",[121,1332,85],{"class":134},[121,1334,1335],{"class":167}," \".config\"",[121,1337,1338],{"class":134}," \u002F",[121,1340,1341],{"class":167}," \"yourcli\"",[121,1343,1338],{"class":134},[121,1345,1346],{"class":167}," \"targets.yaml\"\n",[121,1348,1349,1352],{"class":123,"line":189},[121,1350,1351],{"class":134},"    try",[121,1353,223],{"class":138},[121,1355,1356,1359,1361,1364,1367],{"class":123,"line":226},[121,1357,1358],{"class":138},"        data ",[121,1360,155],{"class":134},[121,1362,1363],{"class":138}," yaml.safe_load(cfg.read_text()) ",[121,1365,1366],{"class":134},"or",[121,1368,1369],{"class":138}," {}\n",[121,1371,1372,1375,1378,1381,1384],{"class":123,"line":232},[121,1373,1374],{"class":134},"        return",[121,1376,1377],{"class":201}," list",[121,1379,1380],{"class":138},"(data.get(",[121,1382,1383],{"class":167},"\"environments\"",[121,1385,1386],{"class":138},", {}))\n",[121,1388,1389,1392,1395,1398],{"class":123,"line":268},[121,1390,1391],{"class":134},"    except",[121,1393,1394],{"class":138}," (",[121,1396,1397],{"class":201},"OSError",[121,1399,1400],{"class":138},", yaml.YAMLError):\n",[121,1402,1403,1405,1408],{"class":123,"line":273},[121,1404,1374],{"class":134},[121,1406,1407],{"class":138}," []   ",[121,1409,1410],{"class":127},"# fail closed: a bad config must never break the shell\n",[121,1412,1413],{"class":123,"line":280},[121,1414,146],{"emptyLinePlaceholder":145},[121,1416,1417,1419,1421],{"class":123,"line":298},[121,1418,192],{"class":134},[121,1420,971],{"class":182},[121,1422,1159],{"class":138},[121,1424,1425,1427,1429,1431,1433,1435,1438,1440],{"class":123,"line":304},[121,1426,1020],{"class":134},[121,1428,1023],{"class":138},[121,1430,1026],{"class":134},[121,1432,1029],{"class":138},[121,1434,1032],{"class":134},[121,1436,1437],{"class":138}," load_targets() ",[121,1439,333],{"class":134},[121,1441,1040],{"class":138},[121,1443,1444],{"class":123,"line":325},[121,1445,146],{"emptyLinePlaceholder":145},[121,1447,1448,1450],{"class":123,"line":330},[121,1449,728],{"class":182},[121,1451,186],{"class":138},[121,1453,1454,1456,1458,1460,1462,1464,1466,1468,1470,1472,1474],{"class":123,"line":347},[121,1455,499],{"class":182},[121,1457,502],{"class":138},[121,1459,505],{"class":167},[121,1461,508],{"class":138},[121,1463,948],{"class":161},[121,1465,155],{"class":134},[121,1467,1067],{"class":138},[121,1469,511],{"class":161},[121,1471,155],{"class":134},[121,1473,516],{"class":201},[121,1475,171],{"class":138},[121,1477,1479,1481,1483,1485,1487,1489,1491],{"class":123,"line":1478},18,[121,1480,192],{"class":134},[121,1482,195],{"class":182},[121,1484,198],{"class":138},[121,1486,202],{"class":201},[121,1488,217],{"class":138},[121,1490,220],{"class":201},[121,1492,223],{"class":138},[121,1494,1496,1498,1500,1502,1504,1506,1508,1510],{"class":123,"line":1495},19,[121,1497,544],{"class":138},[121,1499,238],{"class":134},[121,1501,241],{"class":167},[121,1503,244],{"class":201},[121,1505,247],{"class":138},[121,1507,250],{"class":201},[121,1509,320],{"class":167},[121,1511,171],{"class":138},[10,1513,1514,1515,1518,1519,1522],{},"Two things make this production-grade. First, the callback ",[36,1516,1517],{},"fails closed",": any read or parse error returns an empty list, so a malformed config never wedges the user's Tab key. Second, it is ",[36,1520,1521],{},"cheap"," — reading a small local file per keypress is fine, but if this were a network call you would cache the result (for example in a short-lived file under the user's cache dir) so completion stays instant.",[10,1524,1525,1526,1529,1530,1533],{},"You can also read earlier arguments from ",[41,1527,1528],{},"ctx.params"," to make a completion depend on what the user has already typed — for example, only suggesting regions valid for the ",[41,1531,1532],{},"--env"," already on the line.",[25,1535,1537],{"id":1536},"the-same-thing-in-typer-autocompletion","The same thing in Typer: autocompletion",[10,1539,1540,1541,1544,1545,1548,1549,1551],{},"Typer exposes the identical capability through the ",[41,1542,1543],{},"autocompletion"," argument. The callback takes the incomplete string and returns strings, ",[41,1546,1547],{},"(value, help)"," tuples, or ",[41,1550,71],{},"s:",[112,1553,1555],{"className":114,"code":1554,"language":116,"meta":117,"style":117},"import typer\n\ndef complete_env(incomplete: str) -> list[tuple[str, str]]:\n    targets = {\"staging\": \"shared pre-prod\", \"prod-eu\": \"Frankfurt\", \"prod-us\": \"N. Virginia\"}\n    return [(name, desc) for name, desc in targets.items() if name.startswith(incomplete)]\n\napp = typer.Typer()\n\n@app.command()\ndef deploy(\n    env: str = typer.Option(..., autocompletion=complete_env),\n) -> None:\n    typer.echo(f\"Deploying to {env}\")\n",[41,1556,1557,1563,1567,1590,1624,1645,1649,1657,1661,1667,1676,1700,1708],{"__ignoreMap":117},[121,1558,1559,1561],{"class":123,"line":124},[121,1560,135],{"class":134},[121,1562,139],{"class":138},[121,1564,1565],{"class":123,"line":131},[121,1566,146],{"emptyLinePlaceholder":145},[121,1568,1569,1571,1573,1576,1578,1581,1583,1585,1587],{"class":123,"line":142},[121,1570,192],{"class":134},[121,1572,971],{"class":182},[121,1574,1575],{"class":138},"(incomplete: ",[121,1577,202],{"class":201},[121,1579,1580],{"class":138},") -> list[tuple[",[121,1582,202],{"class":201},[121,1584,508],{"class":138},[121,1586,202],{"class":201},[121,1588,1589],{"class":138},"]]:\n",[121,1591,1592,1594,1596,1599,1601,1603,1605,1607,1609,1611,1613,1615,1617,1619,1621],{"class":123,"line":149},[121,1593,1164],{"class":138},[121,1595,155],{"class":134},[121,1597,1598],{"class":138}," {",[121,1600,997],{"class":167},[121,1602,1177],{"class":138},[121,1604,1180],{"class":167},[121,1606,508],{"class":138},[121,1608,1002],{"class":167},[121,1610,1177],{"class":138},[121,1612,1193],{"class":167},[121,1614,508],{"class":138},[121,1616,1007],{"class":167},[121,1618,1177],{"class":138},[121,1620,1205],{"class":167},[121,1622,1623],{"class":138},"}\n",[121,1625,1626,1628,1631,1633,1635,1637,1640,1642],{"class":123,"line":174},[121,1627,1020],{"class":134},[121,1629,1630],{"class":138}," [(name, desc) ",[121,1632,1026],{"class":134},[121,1634,1239],{"class":138},[121,1636,1032],{"class":134},[121,1638,1639],{"class":138}," targets.items() ",[121,1641,333],{"class":134},[121,1643,1644],{"class":138}," name.startswith(incomplete)]\n",[121,1646,1647],{"class":123,"line":179},[121,1648,146],{"emptyLinePlaceholder":145},[121,1650,1651,1653,1655],{"class":123,"line":189},[121,1652,152],{"class":138},[121,1654,155],{"class":134},[121,1656,891],{"class":138},[121,1658,1659],{"class":123,"line":226},[121,1660,146],{"emptyLinePlaceholder":145},[121,1662,1663,1665],{"class":123,"line":232},[121,1664,183],{"class":182},[121,1666,186],{"class":138},[121,1668,1669,1671,1673],{"class":123,"line":268},[121,1670,192],{"class":134},[121,1672,195],{"class":182},[121,1674,1675],{"class":138},"(\n",[121,1677,1678,1681,1683,1685,1688,1691,1693,1695,1697],{"class":123,"line":273},[121,1679,1680],{"class":138},"    env: ",[121,1682,202],{"class":201},[121,1684,211],{"class":134},[121,1686,1687],{"class":138}," typer.Option(",[121,1689,1690],{"class":201},"...",[121,1692,508],{"class":138},[121,1694,1543],{"class":161},[121,1696,155],{"class":134},[121,1698,1699],{"class":138},"complete_env),\n",[121,1701,1702,1704,1706],{"class":123,"line":280},[121,1703,217],{"class":138},[121,1705,220],{"class":201},[121,1707,223],{"class":138},[121,1709,1710,1712,1714,1716,1718,1720,1722,1724],{"class":123,"line":298},[121,1711,235],{"class":138},[121,1713,238],{"class":134},[121,1715,241],{"class":167},[121,1717,244],{"class":201},[121,1719,247],{"class":138},[121,1721,250],{"class":201},[121,1723,320],{"class":167},[121,1725,171],{"class":138},[10,1727,1728,1729,85,1731,1733,1734,1736,1737,1740,1741,1744],{},"Because Typer is Click underneath, the runtime behaviour is the same; you just declare the callback on the ",[41,1730,84],{},[41,1732,88],{}," instead of passing ",[41,1735,948],{},". If you are still on the older ",[41,1738,1739],{},"autocompletion="," name in Click, migrate to ",[41,1742,1743],{},"shell_complete="," — it replaced the former in Click 8 and is the API these examples use.",[25,1746,1748],{"id":1747},"verify-completion-without-a-shell","Verify completion without a shell",[10,1750,1751],{},"You do not need to install anything into your shell to know your callback works. Drive Click's completion protocol directly and read the candidates it prints:",[112,1753,1755],{"className":353,"code":1754,"language":355,"meta":117,"style":117},"$ _YOURCLI_COMPLETE=bash_complete COMP_WORDS=\"yourcli deploy --env prod-\" COMP_CWORD=3 yourcli\nplain,prod-eu\nplain,prod-us\nplain,prod-ap\n",[41,1756,1757,1776,1781,1786],{"__ignoreMap":117},[121,1758,1759,1761,1764,1767,1770,1773],{"class":123,"line":124},[121,1760,362],{"class":182},[121,1762,1763],{"class":167}," _YOURCLI_COMPLETE=bash_complete",[121,1765,1766],{"class":167}," COMP_WORDS=\"yourcli deploy --env prod-\"",[121,1768,1769],{"class":167}," COMP_CWORD=",[121,1771,1772],{"class":201},"3",[121,1774,1775],{"class":167}," yourcli\n",[121,1777,1778],{"class":123,"line":131},[121,1779,1780],{"class":182},"plain,prod-eu\n",[121,1782,1783],{"class":123,"line":142},[121,1784,1785],{"class":182},"plain,prod-us\n",[121,1787,1788],{"class":123,"line":149},[121,1789,1790],{"class":182},"plain,prod-ap\n",[10,1792,1793,1794,1797,1798,1800],{},"Each line is ",[41,1795,1796],{},"type,value",". If this prints the values you expect, your Python side is correct and any remaining problem is in the install step covered by the ",[19,1799,22],{"href":21},". This is also the ideal hook for a unit test — invoke your CLI with those environment variables set and assert on stdout.",[25,1802,1804],{"id":1803},"production-notes","Production notes",[30,1806,1807,1817,1826,1835,1848],{},[33,1808,1809,1812,1813,89],{},[36,1810,1811],{},"Completion imports your whole program."," Every Tab re-runs your module up to the callback, so heavy top-level imports make suggestions feel sluggish. Keep imports lazy; see ",[19,1814,1816],{"href":1815},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI startup performance and lazy loading",[33,1818,1819,1822,1823,1825],{},[36,1820,1821],{},"Never write to stdout during completion."," With ",[41,1824,604],{}," set, anything on stdout is parsed as a candidate. Send diagnostics to stderr and guard any banner or logging.",[33,1827,1828,1831,1832,89],{},[36,1829,1830],{},"Bound network calls in callbacks."," A slow or hanging API turns Tab into a hang. Add a short timeout and cache; on failure, return ",[41,1833,1834],{},"[]",[33,1836,1837,1840,1841,1843,1844,1847],{},[36,1838,1839],{},"Match the trigger to the installed name."," ",[41,1842,604],{}," is derived from the console-script name — keep it in sync with your ",[19,1845,1846],{"href":427},"entry point",", and regenerate the script if you rename the command.",[33,1849,1850,1840,1853,1855,1856,1859,1860,89],{},[36,1851,1852],{},"Pin versions.",[41,1854,948],{}," is Click ≥8.0; Typer's install flags stabilised around 0.12. Pin ",[41,1857,1858],{},"click>=8.1"," \u002F ",[41,1861,1862],{},"typer>=0.12",[25,1864,1866],{"id":1865},"related","Related",[30,1868,1869,1875,1882,1889],{},[33,1870,1871,1874],{},[19,1872,1873],{"href":21},"Installing shell completion for bash, zsh, fish"," — the sibling guide: put the generated script where each shell loads it.",[33,1876,1877,1881],{},[19,1878,1880],{"href":1879},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F","Shell completion for Python CLIs"," — the overview tying the Python and shell sides together.",[33,1883,1884,1888],{},[19,1885,1887],{"href":1886},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click: when to use each"," — the framework trade-off behind these two APIs.",[33,1890,1891,1894],{},[19,1892,1893],{"href":1270},"Handling config files and env vars in CLIs"," — where dynamic completion often gets its data.",[1896,1897,1898],"style",{},"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}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}",{"title":117,"searchDepth":131,"depth":131,"links":1900},[1901,1902,1903,1904,1905,1906,1907,1908,1909,1910],{"id":27,"depth":131,"text":28},{"id":106,"depth":131,"text":107},{"id":432,"depth":131,"text":433},{"id":700,"depth":131,"text":701},{"id":941,"depth":131,"text":942},{"id":1263,"depth":131,"text":1264},{"id":1536,"depth":131,"text":1537},{"id":1747,"depth":131,"text":1748},{"id":1803,"depth":131,"text":1804},{"id":1865,"depth":131,"text":1866},"2026-07-05","Turn on shell completion in Click and Typer, add dynamic completions for arguments and options, and ship a one-command install-completion setup.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer",{"title":5,"description":1912},"advanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer\u002Findex",[1921,1922,1923,1924,1925],"completion","click","typer","cli","context","5bmIp02aBBENZbPMfzyFVc9bj54PxGjXMSCjtPzfQJc",[1928,1931,1934,1937,1940,1943,1946,1949,1952,1955,1958,1961,1964,1965,1968,1971,1974,1977,1980,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],{"path":1929,"title":1930},"\u002Fabout","About Python CLI Toolcraft",{"path":1932,"title":1933},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1935,"title":1936},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1938,"title":1939},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1941,"title":1942},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1944,"title":1945},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1947,"title":1948},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1950,"title":1951},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1953,"title":1954},"\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":1956,"title":1957},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1959,"title":1960},"\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":1962,"title":1963},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1917,"title":5},{"path":1966,"title":1967},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1969,"title":1970},"\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":1972,"title":1973},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1975,"title":1976},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1978,"title":1979},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":85,"title":1981},"Python CLI Toolcraft",{"path":1983,"title":1984},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1986,"title":1987},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1989,"title":1990},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1992,"title":1993},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1995,"title":1996},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1998,"title":1999},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2001,"title":2002},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2004,"title":2005},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2007,"title":2008},"\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":2010,"title":2011},"\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":2013,"title":2014},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2016,"title":2017},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2019,"title":2020},"\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":2022,"title":2023},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2025,"title":2026},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2028,"title":2029},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2031,"title":2032},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2034,"title":2035},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2037,"title":2038},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2040,"title":2041},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2043,"title":2044},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2046,"title":2047},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2049,"title":2050},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2052,"title":2053},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2055,"title":2056},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2058,"title":2059},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2061,"title":2062},"\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":2064,"title":2065},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2067,"title":2068},"\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":2070,"title":2071},"\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":2073,"title":2074},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2076,"title":2077},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867196]