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