[{"data":1,"prerenderedAt":2748},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy\u002F":3,"content-directory":2201},{"id":4,"title":5,"body":6,"date":2186,"description":2187,"difficulty":2188,"draft":2189,"extension":2190,"meta":2191,"navigation":205,"path":2192,"seo":2193,"stem":2194,"tags":2195,"updated":2186,"__hash__":2200},"content\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy\u002Findex.md","Type-Checking Click and Typer Code with mypy",{"type":7,"value":8,"toc":2165},"minimark",[9,44,49,67,71,74,78,90,94,255,272,276,282,1099,1102,1137,1161,1188,1191,1195,1206,1465,1487,1491,1494,1539,1542,1546,1553,1992,2000,2004,2026,2030,2039,2058,2065,2080,2084,2095,2103,2114,2118,2126,2130,2161],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37,38,43],"p",{},"A command-line tool is where untyped data enters your program: every argument starts as a string in ",[14,15,16],"code",{},"sys.argv",". Typer and Click convert those strings into Python values, and from that point on a type checker can verify that each value is used correctly — that the optional ",[14,19,20],{},"--out"," path is checked for ",[14,23,24],{},"None"," before use, that a ",[14,27,28],{},"--retries"," count is not concatenated to a string, that the settings object shared between commands has the attributes you think it has. In practice, CLI codebases often leak ",[14,31,32],{},"Any"," at exactly the framework boundary and lose most of that protection. This guide sets up mypy for a Typer or Click project and closes the usual gaps: parameters annotated with ",[14,35,36],{},"Annotated",", typed Click commands, a typed context object, and Enums instead of bare strings for choices. It is part of the ",[39,40,42],"a",{"href":41},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002F","linting and type-checking topic",".",[45,46,48],"h2",{"id":47},"prerequisites","Prerequisites",[50,51,52,60],"ul",{},[53,54,55,56,59],"li",{},"Python 3.10+, Typer 0.12+ or Click 8.1+, and mypy as a pinned dev dependency (",[14,57,58],{},"uv add --dev mypy",").",[53,61,62,63,66],{},"A project in ",[14,64,65],{},"src\u002F"," layout. Pyright works equally well with everything below; only the configuration syntax differs.",[45,68,70],{"id":69},"how-types-flow-through-a-cli","How types flow through a CLI",[10,72,73],{},"Values cross three stages. The shell passes strings; the framework converts them according to the parameter declarations; your command function receives Python objects and passes them to the rest of the program.",[75,76],"inline-diagram",{"name":77},"lint-mypy-flow",[10,79,80,81,85,86,89],{},"In Typer the declaration ",[82,83,84],"strong",{},"is"," the type annotation, so the conversion rules and the static types cannot drift apart. In Click, declarations live in decorators (",[14,87,88],{},"@click.option(\"--retries\", type=int)",") and the function parameters are separate — the type checker sees only the function signature, so it is your job to annotate it consistently with the decorator.",[45,91,93],{"id":92},"the-recipe-mypy-configuration","The recipe: mypy configuration",[95,96,101],"pre",{"className":97,"code":98,"language":99,"meta":100,"style":100},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.mypy]\npython_version = \"3.10\"       # your oldest supported Python\nfiles = [\"src\", \"tests\"]\nstrict = true\nwarn_unreachable = true\nenable_error_code = [\"ignore-without-code\", \"redundant-expr\", \"truthy-bool\"]\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\ndisallow_incomplete_defs = false\n","toml","",[14,102,103,112,131,144,161,171,179,200,207,227,238,247],{"__ignoreMap":100},[104,105,108],"span",{"class":106,"line":107},"line",1,[104,109,111],{"class":110},"sJ8bj","# pyproject.toml\n",[104,113,115,119,123,125,128],{"class":106,"line":114},2,[104,116,118],{"class":117},"sVt8B","[",[104,120,122],{"class":121},"sScJk","tool",[104,124,43],{"class":117},[104,126,127],{"class":121},"mypy",[104,129,130],{"class":117},"]\n",[104,132,134,137,141],{"class":106,"line":133},3,[104,135,136],{"class":117},"python_version = ",[104,138,140],{"class":139},"sZZnC","\"3.10\"",[104,142,143],{"class":110},"       # your oldest supported Python\n",[104,145,147,150,153,156,159],{"class":106,"line":146},4,[104,148,149],{"class":117},"files = [",[104,151,152],{"class":139},"\"src\"",[104,154,155],{"class":117},", ",[104,157,158],{"class":139},"\"tests\"",[104,160,130],{"class":117},[104,162,164,167],{"class":106,"line":163},5,[104,165,166],{"class":117},"strict = ",[104,168,170],{"class":169},"sj4cs","true\n",[104,172,174,177],{"class":106,"line":173},6,[104,175,176],{"class":117},"warn_unreachable = ",[104,178,170],{"class":169},[104,180,182,185,188,190,193,195,198],{"class":106,"line":181},7,[104,183,184],{"class":117},"enable_error_code = [",[104,186,187],{"class":139},"\"ignore-without-code\"",[104,189,155],{"class":117},[104,191,192],{"class":139},"\"redundant-expr\"",[104,194,155],{"class":117},[104,196,197],{"class":139},"\"truthy-bool\"",[104,199,130],{"class":117},[104,201,203],{"class":106,"line":202},8,[104,204,206],{"emptyLinePlaceholder":205},true,"\n",[104,208,210,213,215,217,219,221,224],{"class":106,"line":209},9,[104,211,212],{"class":117},"[[",[104,214,122],{"class":121},[104,216,43],{"class":117},[104,218,127],{"class":121},[104,220,43],{"class":117},[104,222,223],{"class":121},"overrides",[104,225,226],{"class":117},"]]\n",[104,228,230,233,236],{"class":106,"line":229},10,[104,231,232],{"class":117},"module = [",[104,234,235],{"class":139},"\"tests.*\"",[104,237,130],{"class":117},[104,239,241,244],{"class":106,"line":240},11,[104,242,243],{"class":117},"disallow_untyped_defs = ",[104,245,246],{"class":169},"false\n",[104,248,250,253],{"class":106,"line":249},12,[104,251,252],{"class":117},"disallow_incomplete_defs = ",[104,254,246],{"class":169},[10,256,257,260,261,264,265,267,268,271],{},[14,258,259],{},"strict"," enables the checks that matter — no untyped definitions, no implicit ",[14,262,263],{},"Optional",", no returning ",[14,266,32],{}," from typed functions — and ",[14,269,270],{},"python_version"," makes mypy flag standard-library APIs newer than your oldest supported interpreter. Tests are checked, but not required to be fully annotated, so calls into your code are still verified without annotating every fixture.",[45,273,275],{"id":274},"the-recipe-typed-typer-commands","The recipe: typed Typer commands",[10,277,278,279,281],{},"Use ",[14,280,36],{}," for every parameter. It keeps the real default in the place Python (and mypy) expects, and puts Typer's metadata alongside the type:",[95,283,287],{"className":284,"code":285,"language":286,"meta":100,"style":100},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fcli.py\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\napp = typer.Typer()\n\n\nclass Format(str, Enum):\n    table = \"table\"\n    json = \"json\"\n\n\n@dataclass\nclass State:\n    verbose: bool\n    config_path: Path\n\n\ndef state(ctx: typer.Context) -> State:\n    \"\"\"The one place ctx.obj (typed Any by Click) becomes a real type.\"\"\"\n    obj = ctx.find_object(State)\n    if obj is None:\n        raise RuntimeError(\"State not initialised by the app callback\")\n    return obj\n\n\n@app.callback()\ndef main(\n    ctx: typer.Context,\n    verbose: Annotated[bool, typer.Option(\"--verbose\", \"-v\")] = False,\n    config: Annotated[Path, typer.Option(envvar=\"MYTOOL_CONFIG\")] = Path(\"mytool.toml\"),\n) -> None:\n    \"\"\"Report tools.\"\"\"\n    ctx.obj = State(verbose=verbose, config_path=config)\n\n\ndef render(rows: list[dict[str, str]], fmt: Format, out: Path) -> int:\n    ...\n    return len(rows)\n\n\n@app.command()\ndef report(\n    ctx: typer.Context,\n    source: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],\n    fmt: Annotated[Format, typer.Option(\"--format\", \"-f\")] = Format.table,\n    out: Annotated[Path | None, typer.Option(\"--out\", \"-o\")] = None,\n    limit: Annotated[int, typer.Option(min=1)] = 50,\n) -> None:\n    \"\"\"Render a report from SOURCE.\"\"\"\n    st = state(ctx)\n    rows = [{\"line\": line} for line in source.read_text(encoding=\"utf-8\").splitlines()[:limit]]\n    target = out if out is not None else source.with_suffix(f\".{fmt.value}\")\n    written = render(rows, fmt, target)\n    if st.verbose:\n        typer.echo(f\"wrote {written} rows to {target}\", err=True)\n\n\nif __name__ == \"__main__\":\n    app()\n","python",[14,288,289,294,309,313,326,338,350,362,366,373,377,388,392,397,420,431,442,447,452,458,469,478,484,489,494,506,512,523,539,556,565,570,575,584,595,601,632,660,670,676,703,708,713,738,744,755,760,765,773,783,788,815,836,865,892,901,907,918,958,1007,1018,1026,1067,1072,1077,1093],{"__ignoreMap":100},[104,290,291],{"class":106,"line":107},[104,292,293],{"class":110},"# src\u002Fmytool\u002Fcli.py\n",[104,295,296,300,303,306],{"class":106,"line":114},[104,297,299],{"class":298},"szBVR","from",[104,301,302],{"class":169}," __future__",[104,304,305],{"class":298}," import",[104,307,308],{"class":117}," annotations\n",[104,310,311],{"class":106,"line":133},[104,312,206],{"emptyLinePlaceholder":205},[104,314,315,317,320,323],{"class":106,"line":146},[104,316,299],{"class":298},[104,318,319],{"class":117}," dataclasses ",[104,321,322],{"class":298},"import",[104,324,325],{"class":117}," dataclass\n",[104,327,328,330,333,335],{"class":106,"line":163},[104,329,299],{"class":298},[104,331,332],{"class":117}," enum ",[104,334,322],{"class":298},[104,336,337],{"class":117}," Enum\n",[104,339,340,342,345,347],{"class":106,"line":173},[104,341,299],{"class":298},[104,343,344],{"class":117}," pathlib ",[104,346,322],{"class":298},[104,348,349],{"class":117}," Path\n",[104,351,352,354,357,359],{"class":106,"line":181},[104,353,299],{"class":298},[104,355,356],{"class":117}," typing ",[104,358,322],{"class":298},[104,360,361],{"class":117}," Annotated\n",[104,363,364],{"class":106,"line":202},[104,365,206],{"emptyLinePlaceholder":205},[104,367,368,370],{"class":106,"line":209},[104,369,322],{"class":298},[104,371,372],{"class":117}," typer\n",[104,374,375],{"class":106,"line":229},[104,376,206],{"emptyLinePlaceholder":205},[104,378,379,382,385],{"class":106,"line":240},[104,380,381],{"class":117},"app ",[104,383,384],{"class":298},"=",[104,386,387],{"class":117}," typer.Typer()\n",[104,389,390],{"class":106,"line":249},[104,391,206],{"emptyLinePlaceholder":205},[104,393,395],{"class":106,"line":394},13,[104,396,206],{"emptyLinePlaceholder":205},[104,398,400,403,406,409,412,414,417],{"class":106,"line":399},14,[104,401,402],{"class":298},"class",[104,404,405],{"class":121}," Format",[104,407,408],{"class":117},"(",[104,410,411],{"class":169},"str",[104,413,155],{"class":117},[104,415,416],{"class":121},"Enum",[104,418,419],{"class":117},"):\n",[104,421,423,426,428],{"class":106,"line":422},15,[104,424,425],{"class":117},"    table ",[104,427,384],{"class":298},[104,429,430],{"class":139}," \"table\"\n",[104,432,434,437,439],{"class":106,"line":433},16,[104,435,436],{"class":117},"    json ",[104,438,384],{"class":298},[104,440,441],{"class":139}," \"json\"\n",[104,443,445],{"class":106,"line":444},17,[104,446,206],{"emptyLinePlaceholder":205},[104,448,450],{"class":106,"line":449},18,[104,451,206],{"emptyLinePlaceholder":205},[104,453,455],{"class":106,"line":454},19,[104,456,457],{"class":121},"@dataclass\n",[104,459,461,463,466],{"class":106,"line":460},20,[104,462,402],{"class":298},[104,464,465],{"class":121}," State",[104,467,468],{"class":117},":\n",[104,470,472,475],{"class":106,"line":471},21,[104,473,474],{"class":117},"    verbose: ",[104,476,477],{"class":169},"bool\n",[104,479,481],{"class":106,"line":480},22,[104,482,483],{"class":117},"    config_path: Path\n",[104,485,487],{"class":106,"line":486},23,[104,488,206],{"emptyLinePlaceholder":205},[104,490,492],{"class":106,"line":491},24,[104,493,206],{"emptyLinePlaceholder":205},[104,495,497,500,503],{"class":106,"line":496},25,[104,498,499],{"class":298},"def",[104,501,502],{"class":121}," state",[104,504,505],{"class":117},"(ctx: typer.Context) -> State:\n",[104,507,509],{"class":106,"line":508},26,[104,510,511],{"class":139},"    \"\"\"The one place ctx.obj (typed Any by Click) becomes a real type.\"\"\"\n",[104,513,515,518,520],{"class":106,"line":514},27,[104,516,517],{"class":117},"    obj ",[104,519,384],{"class":298},[104,521,522],{"class":117}," ctx.find_object(State)\n",[104,524,526,529,532,534,537],{"class":106,"line":525},28,[104,527,528],{"class":298},"    if",[104,530,531],{"class":117}," obj ",[104,533,84],{"class":298},[104,535,536],{"class":169}," None",[104,538,468],{"class":117},[104,540,542,545,548,550,553],{"class":106,"line":541},29,[104,543,544],{"class":298},"        raise",[104,546,547],{"class":169}," RuntimeError",[104,549,408],{"class":117},[104,551,552],{"class":139},"\"State not initialised by the app callback\"",[104,554,555],{"class":117},")\n",[104,557,559,562],{"class":106,"line":558},30,[104,560,561],{"class":298},"    return",[104,563,564],{"class":117}," obj\n",[104,566,568],{"class":106,"line":567},31,[104,569,206],{"emptyLinePlaceholder":205},[104,571,573],{"class":106,"line":572},32,[104,574,206],{"emptyLinePlaceholder":205},[104,576,578,581],{"class":106,"line":577},33,[104,579,580],{"class":121},"@app.callback",[104,582,583],{"class":117},"()\n",[104,585,587,589,592],{"class":106,"line":586},34,[104,588,499],{"class":298},[104,590,591],{"class":121}," main",[104,593,594],{"class":117},"(\n",[104,596,598],{"class":106,"line":597},35,[104,599,600],{"class":117},"    ctx: typer.Context,\n",[104,602,604,607,610,613,616,618,621,624,626,629],{"class":106,"line":603},36,[104,605,606],{"class":117},"    verbose: Annotated[",[104,608,609],{"class":169},"bool",[104,611,612],{"class":117},", typer.Option(",[104,614,615],{"class":139},"\"--verbose\"",[104,617,155],{"class":117},[104,619,620],{"class":139},"\"-v\"",[104,622,623],{"class":117},")] ",[104,625,384],{"class":298},[104,627,628],{"class":169}," False",[104,630,631],{"class":117},",\n",[104,633,635,638,642,644,647,649,651,654,657],{"class":106,"line":634},37,[104,636,637],{"class":117},"    config: Annotated[Path, typer.Option(",[104,639,641],{"class":640},"s4XuR","envvar",[104,643,384],{"class":298},[104,645,646],{"class":139},"\"MYTOOL_CONFIG\"",[104,648,623],{"class":117},[104,650,384],{"class":298},[104,652,653],{"class":117}," Path(",[104,655,656],{"class":139},"\"mytool.toml\"",[104,658,659],{"class":117},"),\n",[104,661,663,666,668],{"class":106,"line":662},38,[104,664,665],{"class":117},") -> ",[104,667,24],{"class":169},[104,669,468],{"class":117},[104,671,673],{"class":106,"line":672},39,[104,674,675],{"class":139},"    \"\"\"Report tools.\"\"\"\n",[104,677,679,682,684,687,690,692,695,698,700],{"class":106,"line":678},40,[104,680,681],{"class":117},"    ctx.obj ",[104,683,384],{"class":298},[104,685,686],{"class":117}," State(",[104,688,689],{"class":640},"verbose",[104,691,384],{"class":298},[104,693,694],{"class":117},"verbose, ",[104,696,697],{"class":640},"config_path",[104,699,384],{"class":298},[104,701,702],{"class":117},"config)\n",[104,704,706],{"class":106,"line":705},41,[104,707,206],{"emptyLinePlaceholder":205},[104,709,711],{"class":106,"line":710},42,[104,712,206],{"emptyLinePlaceholder":205},[104,714,716,718,721,724,726,728,730,733,736],{"class":106,"line":715},43,[104,717,499],{"class":298},[104,719,720],{"class":121}," render",[104,722,723],{"class":117},"(rows: list[dict[",[104,725,411],{"class":169},[104,727,155],{"class":117},[104,729,411],{"class":169},[104,731,732],{"class":117},"]], fmt: Format, out: Path) -> ",[104,734,735],{"class":169},"int",[104,737,468],{"class":117},[104,739,741],{"class":106,"line":740},44,[104,742,743],{"class":169},"    ...\n",[104,745,747,749,752],{"class":106,"line":746},45,[104,748,561],{"class":298},[104,750,751],{"class":169}," len",[104,753,754],{"class":117},"(rows)\n",[104,756,758],{"class":106,"line":757},46,[104,759,206],{"emptyLinePlaceholder":205},[104,761,763],{"class":106,"line":762},47,[104,764,206],{"emptyLinePlaceholder":205},[104,766,768,771],{"class":106,"line":767},48,[104,769,770],{"class":121},"@app.command",[104,772,583],{"class":117},[104,774,776,778,781],{"class":106,"line":775},49,[104,777,499],{"class":298},[104,779,780],{"class":121}," report",[104,782,594],{"class":117},[104,784,786],{"class":106,"line":785},50,[104,787,600],{"class":117},[104,789,791,794,797,799,802,804,807,809,812],{"class":106,"line":790},51,[104,792,793],{"class":117},"    source: Annotated[Path, typer.Argument(",[104,795,796],{"class":640},"exists",[104,798,384],{"class":298},[104,800,801],{"class":169},"True",[104,803,155],{"class":117},[104,805,806],{"class":640},"dir_okay",[104,808,384],{"class":298},[104,810,811],{"class":169},"False",[104,813,814],{"class":117},")],\n",[104,816,818,821,824,826,829,831,833],{"class":106,"line":817},52,[104,819,820],{"class":117},"    fmt: Annotated[Format, typer.Option(",[104,822,823],{"class":139},"\"--format\"",[104,825,155],{"class":117},[104,827,828],{"class":139},"\"-f\"",[104,830,623],{"class":117},[104,832,384],{"class":298},[104,834,835],{"class":117}," Format.table,\n",[104,837,839,842,845,847,849,852,854,857,859,861,863],{"class":106,"line":838},53,[104,840,841],{"class":117},"    out: Annotated[Path ",[104,843,844],{"class":298},"|",[104,846,536],{"class":169},[104,848,612],{"class":117},[104,850,851],{"class":139},"\"--out\"",[104,853,155],{"class":117},[104,855,856],{"class":139},"\"-o\"",[104,858,623],{"class":117},[104,860,384],{"class":298},[104,862,536],{"class":169},[104,864,631],{"class":117},[104,866,868,871,873,875,878,880,883,885,887,890],{"class":106,"line":867},54,[104,869,870],{"class":117},"    limit: Annotated[",[104,872,735],{"class":169},[104,874,612],{"class":117},[104,876,877],{"class":640},"min",[104,879,384],{"class":298},[104,881,882],{"class":169},"1",[104,884,623],{"class":117},[104,886,384],{"class":298},[104,888,889],{"class":169}," 50",[104,891,631],{"class":117},[104,893,895,897,899],{"class":106,"line":894},55,[104,896,665],{"class":117},[104,898,24],{"class":169},[104,900,468],{"class":117},[104,902,904],{"class":106,"line":903},56,[104,905,906],{"class":139},"    \"\"\"Render a report from SOURCE.\"\"\"\n",[104,908,910,913,915],{"class":106,"line":909},57,[104,911,912],{"class":117},"    st ",[104,914,384],{"class":298},[104,916,917],{"class":117}," state(ctx)\n",[104,919,921,924,926,929,932,935,938,941,944,947,950,952,955],{"class":106,"line":920},58,[104,922,923],{"class":117},"    rows ",[104,925,384],{"class":298},[104,927,928],{"class":117}," [{",[104,930,931],{"class":139},"\"line\"",[104,933,934],{"class":117},": line} ",[104,936,937],{"class":298},"for",[104,939,940],{"class":117}," line ",[104,942,943],{"class":298},"in",[104,945,946],{"class":117}," source.read_text(",[104,948,949],{"class":640},"encoding",[104,951,384],{"class":298},[104,953,954],{"class":139},"\"utf-8\"",[104,956,957],{"class":117},").splitlines()[:limit]]\n",[104,959,961,964,966,969,972,974,976,979,981,984,987,990,993,996,999,1002,1005],{"class":106,"line":960},59,[104,962,963],{"class":117},"    target ",[104,965,384],{"class":298},[104,967,968],{"class":117}," out ",[104,970,971],{"class":298},"if",[104,973,968],{"class":117},[104,975,84],{"class":298},[104,977,978],{"class":298}," not",[104,980,536],{"class":169},[104,982,983],{"class":298}," else",[104,985,986],{"class":117}," source.with_suffix(",[104,988,989],{"class":298},"f",[104,991,992],{"class":139},"\".",[104,994,995],{"class":169},"{",[104,997,998],{"class":117},"fmt.value",[104,1000,1001],{"class":169},"}",[104,1003,1004],{"class":139},"\"",[104,1006,555],{"class":117},[104,1008,1010,1013,1015],{"class":106,"line":1009},60,[104,1011,1012],{"class":117},"    written ",[104,1014,384],{"class":298},[104,1016,1017],{"class":117}," render(rows, fmt, target)\n",[104,1019,1021,1023],{"class":106,"line":1020},61,[104,1022,528],{"class":298},[104,1024,1025],{"class":117}," st.verbose:\n",[104,1027,1029,1032,1034,1037,1039,1042,1044,1047,1049,1052,1054,1056,1058,1061,1063,1065],{"class":106,"line":1028},62,[104,1030,1031],{"class":117},"        typer.echo(",[104,1033,989],{"class":298},[104,1035,1036],{"class":139},"\"wrote ",[104,1038,995],{"class":169},[104,1040,1041],{"class":117},"written",[104,1043,1001],{"class":169},[104,1045,1046],{"class":139}," rows to ",[104,1048,995],{"class":169},[104,1050,1051],{"class":117},"target",[104,1053,1001],{"class":169},[104,1055,1004],{"class":139},[104,1057,155],{"class":117},[104,1059,1060],{"class":640},"err",[104,1062,384],{"class":298},[104,1064,801],{"class":169},[104,1066,555],{"class":117},[104,1068,1070],{"class":106,"line":1069},63,[104,1071,206],{"emptyLinePlaceholder":205},[104,1073,1075],{"class":106,"line":1074},64,[104,1076,206],{"emptyLinePlaceholder":205},[104,1078,1080,1082,1085,1088,1091],{"class":106,"line":1079},65,[104,1081,971],{"class":298},[104,1083,1084],{"class":169}," __name__",[104,1086,1087],{"class":298}," ==",[104,1089,1090],{"class":139}," \"__main__\"",[104,1092,468],{"class":117},[104,1094,1096],{"class":106,"line":1095},66,[104,1097,1098],{"class":117},"    app()\n",[10,1100,1101],{},"Three details carry the type safety:",[10,1103,1104,1110,1111,1114,1115,1118,1119,1121,1122,1124,1125,1127,1128,1130,1131,1134,1135,43],{},[82,1105,1106,1109],{},[14,1107,1108],{},"Path | None"," for optional options."," mypy then refuses ",[14,1112,1113],{},"render(rows, fmt, out)"," directly, because ",[14,1116,1117],{},"out"," might be ",[14,1120,24],{},"; you must decide what ",[14,1123,24],{}," means, as the ",[14,1126,1051],{}," line does. Without the annotation — or with a default of ",[14,1129,24],{}," on a parameter typed ",[14,1132,1133],{},"Path"," — the bug ships and appears only when a user omits ",[14,1136,20],{},[10,1138,1139,1145,1146,1148,1149,1152,1153,1156,1157,1160],{},[82,1140,1141,1142,1144],{},"An ",[14,1143,416],{}," for choices."," Typer turns an ",[14,1147,416],{}," parameter into a choice option with the members as allowed values, and your code receives ",[14,1150,1151],{},"Format.json"," instead of ",[14,1154,1155],{},"\"json\"",". mypy can then check exhaustive handling, and a typo in a comparison (",[14,1158,1159],{},"fmt == \"jsno\"",") becomes impossible.",[10,1162,1163,1169,1170,1172,1173,1175,1176,1179,1180,1182,1183,1187],{},[82,1164,1165,1166,43],{},"A typed accessor for ",[14,1167,1168],{},"ctx.obj"," Click types the context object as ",[14,1171,32],{},", and ",[14,1174,32],{}," spreads silently: every attribute access on it is unchecked. Funnel all access through one function that returns a concrete type. ",[14,1177,1178],{},"ctx.find_object(State)"," walks up the context chain and returns an instance of that class or ",[14,1181,24],{},", so the accessor works from nested subcommands too — the pattern from ",[39,1184,1186],{"href":1185},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects\u002F","sharing state with Click context objects",", made type-safe.",[75,1189],{"name":1190},"lint-click-typing",[45,1192,1194],{"id":1193},"the-recipe-typed-click-commands","The recipe: typed Click commands",[10,1196,1197,1198,1201,1202,1205],{},"Click's decorators return an untyped ",[14,1199,1200],{},"Command"," object, so mypy checks the ",[82,1203,1204],{},"body"," of your function against its own annotations. Annotate every parameter to match the decorator:",[95,1207,1209],{"className":284,"code":1208,"language":286,"meta":100,"style":100},"# src\u002Fmytool\u002Fclick_cli.py\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nimport click\n\n\n@click.command()\n@click.argument(\"source\", type=click.Path(exists=True, dir_okay=False, path_type=Path))\n@click.option(\"--retries\", type=int, default=3, show_default=True)\n@click.option(\"--out\", type=click.Path(path_type=Path), default=None)\ndef fetch(source: Path, retries: int, out: Path | None) -> None:\n    \"\"\"Fetch items listed in SOURCE.\"\"\"\n    target = out or source.with_suffix(\".out\")\n    click.echo(f\"{retries} retries -> {target}\", err=True)\n",[14,1210,1211,1216,1226,1230,1240,1244,1251,1255,1259,1266,1310,1349,1380,1405,1410,1428],{"__ignoreMap":100},[104,1212,1213],{"class":106,"line":107},[104,1214,1215],{"class":110},"# src\u002Fmytool\u002Fclick_cli.py\n",[104,1217,1218,1220,1222,1224],{"class":106,"line":114},[104,1219,299],{"class":298},[104,1221,302],{"class":169},[104,1223,305],{"class":298},[104,1225,308],{"class":117},[104,1227,1228],{"class":106,"line":133},[104,1229,206],{"emptyLinePlaceholder":205},[104,1231,1232,1234,1236,1238],{"class":106,"line":146},[104,1233,299],{"class":298},[104,1235,344],{"class":117},[104,1237,322],{"class":298},[104,1239,349],{"class":117},[104,1241,1242],{"class":106,"line":163},[104,1243,206],{"emptyLinePlaceholder":205},[104,1245,1246,1248],{"class":106,"line":173},[104,1247,322],{"class":298},[104,1249,1250],{"class":117}," click\n",[104,1252,1253],{"class":106,"line":181},[104,1254,206],{"emptyLinePlaceholder":205},[104,1256,1257],{"class":106,"line":202},[104,1258,206],{"emptyLinePlaceholder":205},[104,1260,1261,1264],{"class":106,"line":209},[104,1262,1263],{"class":121},"@click.command",[104,1265,583],{"class":117},[104,1267,1268,1271,1273,1276,1278,1281,1283,1286,1288,1290,1292,1294,1296,1298,1300,1302,1305,1307],{"class":106,"line":229},[104,1269,1270],{"class":121},"@click.argument",[104,1272,408],{"class":117},[104,1274,1275],{"class":139},"\"source\"",[104,1277,155],{"class":117},[104,1279,1280],{"class":640},"type",[104,1282,384],{"class":298},[104,1284,1285],{"class":117},"click.Path(",[104,1287,796],{"class":640},[104,1289,384],{"class":298},[104,1291,801],{"class":169},[104,1293,155],{"class":117},[104,1295,806],{"class":640},[104,1297,384],{"class":298},[104,1299,811],{"class":169},[104,1301,155],{"class":117},[104,1303,1304],{"class":640},"path_type",[104,1306,384],{"class":298},[104,1308,1309],{"class":117},"Path))\n",[104,1311,1312,1315,1317,1320,1322,1324,1326,1328,1330,1333,1335,1338,1340,1343,1345,1347],{"class":106,"line":240},[104,1313,1314],{"class":121},"@click.option",[104,1316,408],{"class":117},[104,1318,1319],{"class":139},"\"--retries\"",[104,1321,155],{"class":117},[104,1323,1280],{"class":640},[104,1325,384],{"class":298},[104,1327,735],{"class":169},[104,1329,155],{"class":117},[104,1331,1332],{"class":640},"default",[104,1334,384],{"class":298},[104,1336,1337],{"class":169},"3",[104,1339,155],{"class":117},[104,1341,1342],{"class":640},"show_default",[104,1344,384],{"class":298},[104,1346,801],{"class":169},[104,1348,555],{"class":117},[104,1350,1351,1353,1355,1357,1359,1361,1363,1365,1367,1369,1372,1374,1376,1378],{"class":106,"line":249},[104,1352,1314],{"class":121},[104,1354,408],{"class":117},[104,1356,851],{"class":139},[104,1358,155],{"class":117},[104,1360,1280],{"class":640},[104,1362,384],{"class":298},[104,1364,1285],{"class":117},[104,1366,1304],{"class":640},[104,1368,384],{"class":298},[104,1370,1371],{"class":117},"Path), ",[104,1373,1332],{"class":640},[104,1375,384],{"class":298},[104,1377,24],{"class":169},[104,1379,555],{"class":117},[104,1381,1382,1384,1387,1390,1392,1395,1397,1399,1401,1403],{"class":106,"line":394},[104,1383,499],{"class":298},[104,1385,1386],{"class":121}," fetch",[104,1388,1389],{"class":117},"(source: Path, retries: ",[104,1391,735],{"class":169},[104,1393,1394],{"class":117},", out: Path ",[104,1396,844],{"class":298},[104,1398,536],{"class":169},[104,1400,665],{"class":117},[104,1402,24],{"class":169},[104,1404,468],{"class":117},[104,1406,1407],{"class":106,"line":399},[104,1408,1409],{"class":139},"    \"\"\"Fetch items listed in SOURCE.\"\"\"\n",[104,1411,1412,1414,1416,1418,1421,1423,1426],{"class":106,"line":422},[104,1413,963],{"class":117},[104,1415,384],{"class":298},[104,1417,968],{"class":117},[104,1419,1420],{"class":298},"or",[104,1422,986],{"class":117},[104,1424,1425],{"class":139},"\".out\"",[104,1427,555],{"class":117},[104,1429,1430,1433,1435,1437,1439,1442,1444,1447,1449,1451,1453,1455,1457,1459,1461,1463],{"class":106,"line":433},[104,1431,1432],{"class":117},"    click.echo(",[104,1434,989],{"class":298},[104,1436,1004],{"class":139},[104,1438,995],{"class":169},[104,1440,1441],{"class":117},"retries",[104,1443,1001],{"class":169},[104,1445,1446],{"class":139}," retries -> ",[104,1448,995],{"class":169},[104,1450,1051],{"class":117},[104,1452,1001],{"class":169},[104,1454,1004],{"class":139},[104,1456,155],{"class":117},[104,1458,1060],{"class":640},[104,1460,384],{"class":298},[104,1462,801],{"class":169},[104,1464,555],{"class":117},[10,1466,1467,1470,1471,1473,1474,1476,1477,1480,1481,1483,1484,1486],{},[14,1468,1469],{},"path_type=Path"," makes Click actually pass a ",[14,1472,1133],{}," (without it, you get a ",[14,1475,411],{}," despite the annotation — a mismatch mypy cannot see). The same care applies to ",[14,1478,1479],{},"type=click.Choice([...])",", which passes a ",[14,1482,411],{},"; annotate it as ",[14,1485,411],{},", or convert it to an Enum on the first line. Keeping annotations honest to the decorators is exactly what the tests below are for.",[45,1488,1490],{"id":1489},"ux-considerations","UX considerations",[10,1492,1493],{},"Types are for developers, but they have direct effects on users:",[50,1495,1496,1505,1515,1525],{},[53,1497,1498,1501,1502,1504],{},[82,1499,1500],{},"Optional flags handled correctly."," The most common crash in untyped CLI code is a ",[14,1503,24],{}," from an omitted option; strict optional checking prevents it.",[53,1506,1507,1510,1511,1514],{},[82,1508,1509],{},"Help text from Enums."," Enum-typed options list their allowed values in ",[14,1512,1513],{},"--help"," automatically, and invalid values produce a clear \"Invalid value for '--format'\" error rather than a failure deep inside the code.",[53,1516,1517,1520,1521,1524],{},[82,1518,1519],{},"Refactors that do not break commands."," Renaming a field on the shared ",[14,1522,1523],{},"State"," object is caught in every command at once instead of at runtime in the one command nobody tested.",[53,1526,1527,1530,1531,1534,1535,1538],{},[82,1528,1529],{},"Clear mypy errors, not suppressions."," When mypy complains about framework code, prefer a precise ",[14,1532,1533],{},"cast"," in the accessor or a narrowly scoped ",[14,1536,1537],{},"# type: ignore[code]"," over loosening global settings.",[75,1540],{"name":1541},"lint-mypy-terminal",[45,1543,1545],{"id":1544},"testing-the-behaviour","Testing the behaviour",[10,1547,1548,1549,1552],{},"Two kinds of test keep the types honest. mypy itself runs in CI (",[14,1550,1551],{},"uv run mypy","). And a small runtime test verifies that what the framework actually passes matches the annotations — the thing mypy cannot see through Click decorators:",[95,1554,1556],{"className":284,"code":1555,"language":286,"meta":100,"style":100},"# tests\u002Ftest_types.py\nfrom pathlib import Path\n\nfrom click.testing import CliRunner\nfrom typer.testing import CliRunner as TyperRunner\n\nfrom mytool import cli\nfrom mytool.click_cli import fetch\n\n\ndef test_click_passes_what_we_annotated(tmp_path, monkeypatch):\n    src = tmp_path \u002F \"items.txt\"\n    src.write_text(\"a\\n\")\n    seen = {}\n    monkeypatch.setattr(fetch, \"callback\",\n                        lambda source, retries, out: seen.update(source=source, retries=retries, out=out))\n    CliRunner().invoke(fetch, [str(src), \"--retries\", \"5\"])\n    assert isinstance(seen[\"source\"], Path)\n    assert isinstance(seen[\"retries\"], int)\n    assert seen[\"out\"] is None\n\n\ndef test_typer_enum_choice(tmp_path):\n    src = tmp_path \u002F \"in.txt\"\n    src.write_text(\"x\\ny\\n\")\n    result = TyperRunner().invoke(cli.app, [\"-v\", \"report\", str(src), \"--format\", \"json\"])\n    assert result.exit_code == 0, result.output\n    assert \"in.json\" in result.output\n\n\ndef test_invalid_choice_is_a_usage_error(tmp_path):\n    src = tmp_path \u002F \"in.txt\"\n    src.write_text(\"x\\n\")\n    result = TyperRunner().invoke(cli.app, [\"report\", str(src), \"--format\", \"xml\"])\n    assert result.exit_code == 2\n",[14,1557,1558,1563,1573,1577,1589,1607,1611,1623,1635,1639,1643,1653,1669,1684,1694,1704,1734,1754,1770,1788,1806,1810,1814,1824,1837,1855,1886,1902,1915,1919,1923,1932,1944,1956,1981],{"__ignoreMap":100},[104,1559,1560],{"class":106,"line":107},[104,1561,1562],{"class":110},"# tests\u002Ftest_types.py\n",[104,1564,1565,1567,1569,1571],{"class":106,"line":114},[104,1566,299],{"class":298},[104,1568,344],{"class":117},[104,1570,322],{"class":298},[104,1572,349],{"class":117},[104,1574,1575],{"class":106,"line":133},[104,1576,206],{"emptyLinePlaceholder":205},[104,1578,1579,1581,1584,1586],{"class":106,"line":146},[104,1580,299],{"class":298},[104,1582,1583],{"class":117}," click.testing ",[104,1585,322],{"class":298},[104,1587,1588],{"class":117}," CliRunner\n",[104,1590,1591,1593,1596,1598,1601,1604],{"class":106,"line":163},[104,1592,299],{"class":298},[104,1594,1595],{"class":117}," typer.testing ",[104,1597,322],{"class":298},[104,1599,1600],{"class":117}," CliRunner ",[104,1602,1603],{"class":298},"as",[104,1605,1606],{"class":117}," TyperRunner\n",[104,1608,1609],{"class":106,"line":173},[104,1610,206],{"emptyLinePlaceholder":205},[104,1612,1613,1615,1618,1620],{"class":106,"line":181},[104,1614,299],{"class":298},[104,1616,1617],{"class":117}," mytool ",[104,1619,322],{"class":298},[104,1621,1622],{"class":117}," cli\n",[104,1624,1625,1627,1630,1632],{"class":106,"line":202},[104,1626,299],{"class":298},[104,1628,1629],{"class":117}," mytool.click_cli ",[104,1631,322],{"class":298},[104,1633,1634],{"class":117}," fetch\n",[104,1636,1637],{"class":106,"line":209},[104,1638,206],{"emptyLinePlaceholder":205},[104,1640,1641],{"class":106,"line":229},[104,1642,206],{"emptyLinePlaceholder":205},[104,1644,1645,1647,1650],{"class":106,"line":240},[104,1646,499],{"class":298},[104,1648,1649],{"class":121}," test_click_passes_what_we_annotated",[104,1651,1652],{"class":117},"(tmp_path, monkeypatch):\n",[104,1654,1655,1658,1660,1663,1666],{"class":106,"line":249},[104,1656,1657],{"class":117},"    src ",[104,1659,384],{"class":298},[104,1661,1662],{"class":117}," tmp_path ",[104,1664,1665],{"class":298},"\u002F",[104,1667,1668],{"class":139}," \"items.txt\"\n",[104,1670,1671,1674,1677,1680,1682],{"class":106,"line":394},[104,1672,1673],{"class":117},"    src.write_text(",[104,1675,1676],{"class":139},"\"a",[104,1678,1679],{"class":169},"\\n",[104,1681,1004],{"class":139},[104,1683,555],{"class":117},[104,1685,1686,1689,1691],{"class":106,"line":399},[104,1687,1688],{"class":117},"    seen ",[104,1690,384],{"class":298},[104,1692,1693],{"class":117}," {}\n",[104,1695,1696,1699,1702],{"class":106,"line":422},[104,1697,1698],{"class":117},"    monkeypatch.setattr(fetch, ",[104,1700,1701],{"class":139},"\"callback\"",[104,1703,631],{"class":117},[104,1705,1706,1709,1712,1715,1717,1720,1722,1724,1727,1729,1731],{"class":106,"line":433},[104,1707,1708],{"class":298},"                        lambda",[104,1710,1711],{"class":117}," source, retries, out: seen.update(",[104,1713,1714],{"class":640},"source",[104,1716,384],{"class":298},[104,1718,1719],{"class":117},"source, ",[104,1721,1441],{"class":640},[104,1723,384],{"class":298},[104,1725,1726],{"class":117},"retries, ",[104,1728,1117],{"class":640},[104,1730,384],{"class":298},[104,1732,1733],{"class":117},"out))\n",[104,1735,1736,1739,1741,1744,1746,1748,1751],{"class":106,"line":444},[104,1737,1738],{"class":117},"    CliRunner().invoke(fetch, [",[104,1740,411],{"class":169},[104,1742,1743],{"class":117},"(src), ",[104,1745,1319],{"class":139},[104,1747,155],{"class":117},[104,1749,1750],{"class":139},"\"5\"",[104,1752,1753],{"class":117},"])\n",[104,1755,1756,1759,1762,1765,1767],{"class":106,"line":449},[104,1757,1758],{"class":298},"    assert",[104,1760,1761],{"class":169}," isinstance",[104,1763,1764],{"class":117},"(seen[",[104,1766,1275],{"class":139},[104,1768,1769],{"class":117},"], Path)\n",[104,1771,1772,1774,1776,1778,1781,1784,1786],{"class":106,"line":454},[104,1773,1758],{"class":298},[104,1775,1761],{"class":169},[104,1777,1764],{"class":117},[104,1779,1780],{"class":139},"\"retries\"",[104,1782,1783],{"class":117},"], ",[104,1785,735],{"class":169},[104,1787,555],{"class":117},[104,1789,1790,1792,1795,1798,1801,1803],{"class":106,"line":460},[104,1791,1758],{"class":298},[104,1793,1794],{"class":117}," seen[",[104,1796,1797],{"class":139},"\"out\"",[104,1799,1800],{"class":117},"] ",[104,1802,84],{"class":298},[104,1804,1805],{"class":169}," None\n",[104,1807,1808],{"class":106,"line":471},[104,1809,206],{"emptyLinePlaceholder":205},[104,1811,1812],{"class":106,"line":480},[104,1813,206],{"emptyLinePlaceholder":205},[104,1815,1816,1818,1821],{"class":106,"line":486},[104,1817,499],{"class":298},[104,1819,1820],{"class":121}," test_typer_enum_choice",[104,1822,1823],{"class":117},"(tmp_path):\n",[104,1825,1826,1828,1830,1832,1834],{"class":106,"line":491},[104,1827,1657],{"class":117},[104,1829,384],{"class":298},[104,1831,1662],{"class":117},[104,1833,1665],{"class":298},[104,1835,1836],{"class":139}," \"in.txt\"\n",[104,1838,1839,1841,1844,1846,1849,1851,1853],{"class":106,"line":496},[104,1840,1673],{"class":117},[104,1842,1843],{"class":139},"\"x",[104,1845,1679],{"class":169},[104,1847,1848],{"class":139},"y",[104,1850,1679],{"class":169},[104,1852,1004],{"class":139},[104,1854,555],{"class":117},[104,1856,1857,1860,1862,1865,1867,1869,1872,1874,1876,1878,1880,1882,1884],{"class":106,"line":508},[104,1858,1859],{"class":117},"    result ",[104,1861,384],{"class":298},[104,1863,1864],{"class":117}," TyperRunner().invoke(cli.app, [",[104,1866,620],{"class":139},[104,1868,155],{"class":117},[104,1870,1871],{"class":139},"\"report\"",[104,1873,155],{"class":117},[104,1875,411],{"class":169},[104,1877,1743],{"class":117},[104,1879,823],{"class":139},[104,1881,155],{"class":117},[104,1883,1155],{"class":139},[104,1885,1753],{"class":117},[104,1887,1888,1890,1893,1896,1899],{"class":106,"line":514},[104,1889,1758],{"class":298},[104,1891,1892],{"class":117}," result.exit_code ",[104,1894,1895],{"class":298},"==",[104,1897,1898],{"class":169}," 0",[104,1900,1901],{"class":117},", result.output\n",[104,1903,1904,1906,1909,1912],{"class":106,"line":525},[104,1905,1758],{"class":298},[104,1907,1908],{"class":139}," \"in.json\"",[104,1910,1911],{"class":298}," in",[104,1913,1914],{"class":117}," result.output\n",[104,1916,1917],{"class":106,"line":541},[104,1918,206],{"emptyLinePlaceholder":205},[104,1920,1921],{"class":106,"line":558},[104,1922,206],{"emptyLinePlaceholder":205},[104,1924,1925,1927,1930],{"class":106,"line":567},[104,1926,499],{"class":298},[104,1928,1929],{"class":121}," test_invalid_choice_is_a_usage_error",[104,1931,1823],{"class":117},[104,1933,1934,1936,1938,1940,1942],{"class":106,"line":572},[104,1935,1657],{"class":117},[104,1937,384],{"class":298},[104,1939,1662],{"class":117},[104,1941,1665],{"class":298},[104,1943,1836],{"class":139},[104,1945,1946,1948,1950,1952,1954],{"class":106,"line":577},[104,1947,1673],{"class":117},[104,1949,1843],{"class":139},[104,1951,1679],{"class":169},[104,1953,1004],{"class":139},[104,1955,555],{"class":117},[104,1957,1958,1960,1962,1964,1966,1968,1970,1972,1974,1976,1979],{"class":106,"line":586},[104,1959,1859],{"class":117},[104,1961,384],{"class":298},[104,1963,1864],{"class":117},[104,1965,1871],{"class":139},[104,1967,155],{"class":117},[104,1969,411],{"class":169},[104,1971,1743],{"class":117},[104,1973,823],{"class":139},[104,1975,155],{"class":117},[104,1977,1978],{"class":139},"\"xml\"",[104,1980,1753],{"class":117},[104,1982,1983,1985,1987,1989],{"class":106,"line":597},[104,1984,1758],{"class":298},[104,1986,1892],{"class":117},[104,1988,1895],{"class":298},[104,1990,1991],{"class":169}," 2\n",[10,1993,1994,1995,1997,1998,43],{},"The Click test replaces the command's callback with a recorder and asserts on the runtime types — it would fail immediately if someone removed ",[14,1996,1469],{}," while the annotation still said ",[14,1999,1133],{},[45,2001,2003],{"id":2002},"conclusion","Conclusion",[10,2005,2006,2007,2009,2010,2013,2014,2016,2017,2019,2020,2022,2023,2025],{},"A CLI can be as well-typed as any other Python code once the framework boundary is handled deliberately. Turn on strict mypy, annotate Typer parameters with ",[14,2008,36],{},", give optional options ",[14,2011,2012],{},"X | None"," types and handle the ",[14,2015,24],{},", use Enums for choices, annotate Click commands to match their decorators (with ",[14,2018,1469],{},"), and route ",[14,2021,1168],{}," through one typed accessor. The result is that most bugs involving the shape of user input are found by ",[14,2024,127],{}," before anyone runs the command.",[45,2027,2029],{"id":2028},"frequently-asked-questions","Frequently asked questions",[2031,2032,2034,2035,2038],"h3",{"id":2033},"does-typers-older-param-int-typeroption3-style-type-check","Does Typer's older ",[14,2036,2037],{},"param: int = typer.Option(3)"," style type-check?",[10,2040,2041,2042,2045,2046,2048,2049,2051,2052,2054,2055,2057],{},"mypy sees the default as whatever ",[14,2043,2044],{},"typer.Option"," returns, which is typed as ",[14,2047,32],{},", so it does not complain — but it also cannot tell you that a parameter with default ",[14,2050,24],{}," should be ",[14,2053,263],{},". ",[14,2056,36],{}," makes the default explicit and checkable, which is why it is the recommended style.",[2031,2059,2061,2062,2064],{"id":2060},"how-do-i-type-a-click-groups-ctxobj-without-a-dataclass","How do I type a Click group's ",[14,2063,1168],{}," without a dataclass?",[10,2066,2067,2068,2071,2072,2075,2076,2079],{},"A ",[14,2069,2070],{},"TypedDict"," works if you prefer dictionaries: define ",[14,2073,2074],{},"class Obj(TypedDict): verbose: bool"," and have the accessor return ",[14,2077,2078],{},"cast(Obj, ctx.obj)",". A dataclass is usually clearer and catches misspelt attributes at construction.",[2031,2081,2083],{"id":2082},"mypy-says-a-click-decorator-makes-my-function-untyped-what-now","mypy says a Click decorator makes my function untyped. What now?",[10,2085,2086,2087,2090,2091,2094],{},"With ",[14,2088,2089],{},"disallow_untyped_decorators",", mypy complains that Click's decorators are not fully typed in some versions. Keep the strict setting and add a targeted override for your CLI modules (",[14,2092,2093],{},"disallow_untyped_decorators = false",") rather than disabling it project-wide.",[2031,2096,2098,2099,2102],{"id":2097},"does-from-__future__-import-annotations-break-typer","Does ",[14,2100,2101],{},"from __future__ import annotations"," break Typer?",[10,2104,2105,2106,2109,2110,2113],{},"Not with current Typer releases, which resolve string annotations with ",[14,2107,2108],{},"typing.get_type_hints",". The one trap is annotating with a name that only exists under ",[14,2111,2112],{},"if TYPE_CHECKING:"," — Typer needs the real type at runtime to build the parameter, so import anything used in a command signature normally.",[2031,2115,2117],{"id":2116},"should-i-use-pydantic-for-option-validation","Should I use pydantic for option validation?",[10,2119,2120,2121,2125],{},"For complex, nested configuration — a config file, JSON arguments — pydantic models add validation and good error messages, as covered in ",[39,2122,2124],{"href":2123},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings\u002F","typed settings with pydantic-settings",". For individual flags, Typer's own types and callbacks are usually enough.",[45,2127,2129],{"id":2128},"related","Related",[50,2131,2132,2138,2144,2150,2156],{},[53,2133,2134,2135],{},"Up: ",[39,2136,2137],{"href":41},"Linting and type-checking CLI code",[53,2139,2140],{},[39,2141,2143],{"href":2142},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project\u002F","Configuring Ruff for a CLI project",[53,2145,2146],{},[39,2147,2149],{"href":2148},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer\u002F","Using Annotated options in Typer",[53,2151,2152],{},[39,2153,2155],{"href":2154},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types\u002F","Writing custom Click parameter types",[53,2157,2158],{},[39,2159,2160],{"href":1185},"Sharing state with Click context objects",[2162,2163,2164],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":100,"searchDepth":114,"depth":114,"links":2166},[2167,2168,2169,2170,2171,2172,2173,2174,2175,2185],{"id":47,"depth":114,"text":48},{"id":69,"depth":114,"text":70},{"id":92,"depth":114,"text":93},{"id":274,"depth":114,"text":275},{"id":1193,"depth":114,"text":1194},{"id":1489,"depth":114,"text":1490},{"id":1544,"depth":114,"text":1545},{"id":2002,"depth":114,"text":2003},{"id":2028,"depth":114,"text":2029,"children":2176},[2177,2179,2181,2182,2184],{"id":2033,"depth":133,"text":2178},"Does Typer's older param: int = typer.Option(3) style type-check?",{"id":2060,"depth":133,"text":2180},"How do I type a Click group's ctx.obj without a dataclass?",{"id":2082,"depth":133,"text":2083},{"id":2097,"depth":133,"text":2183},"Does from __future__ import annotations break Typer?",{"id":2116,"depth":133,"text":2117},{"id":2128,"depth":114,"text":2129},"2026-09-18","Get real type safety in Python CLIs: mypy strict settings, Annotated Typer parameters, typed Click commands, a typed ctx.obj, Enums for choices and tests.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy",{"title":5,"description":2187},"project-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy\u002Findex",[127,2196,2197,2198,2199],"typing","typer","click","type-checking","1AjXN5aGjpjj4J1NAwqX-eFtwfulYVpK4gkkKjDBLJY",[2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283,2286,2289,2292,2295,2298,2301,2304,2307,2310,2313,2316,2319,2322,2325,2328,2331,2334,2337,2340,2343,2346,2349,2352,2355,2358,2361,2364,2367,2370,2373,2376,2379,2382,2385,2388,2391,2394,2397,2400,2403,2406,2409,2412,2415,2418,2421,2424,2427,2430,2433,2436,2439,2442,2445,2448,2451,2454,2457,2460,2463,2466,2469,2472,2475,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600,2603,2606,2609,2612,2615,2618,2621,2624,2627,2630,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660,2663,2664,2667,2670,2673,2676,2679,2682,2685,2688,2691,2694,2697,2700,2703,2706,2709,2712,2715,2718,2721,2724,2727,2730,2733,2736,2739,2742,2745],{"path":2203,"title":2204},"\u002Fabout","About Python CLI Toolcraft",{"path":2206,"title":2207},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2209,"title":2210},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2212,"title":2213},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2215,"title":2216},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2218,"title":2219},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2221,"title":2222},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":2224,"title":2225},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":2227,"title":2228},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2230,"title":2231},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2233,"title":2234},"\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":2236,"title":2237},"\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":2239,"title":2240},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2242,"title":2243},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2245,"title":2246},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2248,"title":2249},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2251,"title":2252},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2254,"title":2255},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":2257,"title":2258},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2260,"title":2261},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":2263,"title":2264},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2266,"title":2267},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":2269,"title":2270},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2272,"title":2273},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2275,"title":2276},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2278,"title":2279},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":2281,"title":2282},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2284,"title":2285},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":2287,"title":2288},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2290,"title":2291},"\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":2293,"title":2294},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2296,"title":2297},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2299,"title":2300},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2302,"title":2303},"\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":2305,"title":2306},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2308,"title":2309},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2311,"title":2312},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2314,"title":2315},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2317,"title":2318},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2320,"title":2321},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2323,"title":2324},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":2326,"title":2327},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2329,"title":2330},"\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":2332,"title":2333},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2335,"title":2336},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2338,"title":2339},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2341,"title":2342},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2344,"title":2345},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2347,"title":2348},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2350,"title":2351},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2353,"title":2354},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2356,"title":2357},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2359,"title":2360},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2362,"title":2363},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2365,"title":2366},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2368,"title":2369},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2371,"title":2372},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2374,"title":2375},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2377,"title":2378},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2380,"title":2381},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2383,"title":2384},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2386,"title":2387},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2389,"title":2390},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2392,"title":2393},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2395,"title":2396},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2398,"title":2399},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2401,"title":2402},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2404,"title":2405},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2407,"title":2408},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2410,"title":2411},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2413,"title":2414},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2416,"title":2417},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2419,"title":2420},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2422,"title":2423},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2425,"title":2426},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2428,"title":2429},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2431,"title":2432},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2434,"title":2435},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2437,"title":2438},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2440,"title":2441},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2443,"title":2444},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2446,"title":2447},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2449,"title":2450},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2452,"title":2453},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2455,"title":2456},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2458,"title":2459},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2461,"title":2462},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2464,"title":2465},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2467,"title":2468},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2470,"title":2471},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2473,"title":2474},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1665,"title":2476},"Python CLI Toolcraft",{"path":2478,"title":2479},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2481,"title":2482},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2484,"title":2485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2487,"title":2488},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2490,"title":2491},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2493,"title":2494},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2496,"title":2497},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2499,"title":2500},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2502,"title":2503},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2505,"title":2506},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2508,"title":2509},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2511,"title":2512},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2514,"title":2515},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2517,"title":2518},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2520,"title":2521},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2523,"title":2524},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2526,"title":2527},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2529,"title":2530},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2532,"title":2533},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2535,"title":2536},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2538,"title":2539},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2541,"title":2542},"\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":2544,"title":2545},"\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":2547,"title":2548},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2550,"title":2551},"\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":2553,"title":2554},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2556,"title":2557},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2559,"title":2560},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2562,"title":2563},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2565,"title":2566},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2568,"title":2569},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2571,"title":2572},"\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":2574,"title":2575},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2577,"title":2578},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2580,"title":2581},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2583,"title":2584},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2586,"title":2587},"\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":2589,"title":2590},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2592,"title":2593},"\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":2595,"title":2596},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2598,"title":2599},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2601,"title":2602},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2604,"title":2605},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2607,"title":2608},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2610,"title":2611},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2613,"title":2614},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2616,"title":2617},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2619,"title":2620},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2622,"title":2623},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2625,"title":2626},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2628,"title":2629},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2631,"title":2632},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2634,"title":2635},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2637,"title":2638},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2640,"title":2641},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2643,"title":2644},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2646,"title":2647},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2649,"title":2650},"\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":2652,"title":2653},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2655,"title":2656},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2658,"title":2659},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2661,"title":2662},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2192,"title":5},{"path":2665,"title":2666},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2668,"title":2669},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2671,"title":2672},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2674,"title":2675},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2677,"title":2678},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2680,"title":2681},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2683,"title":2684},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2686,"title":2687},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2689,"title":2690},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2692,"title":2693},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2695,"title":2696},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2698,"title":2699},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2701,"title":2702},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2704,"title":2705},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2707,"title":2708},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2710,"title":2711},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2713,"title":2714},"\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":2716,"title":2717},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2719,"title":2720},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2722,"title":2723},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2725,"title":2726},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2728,"title":2729},"\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":2731,"title":2732},"\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":2734,"title":2735},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2737,"title":2738},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2740,"title":2741},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2743,"title":2744},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2746,"title":2747},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907493]