[{"data":1,"prerenderedAt":2144},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects\u002F":3,"content-directory":1897},{"id":4,"title":5,"body":6,"date":1881,"description":1882,"difficulty":1883,"draft":1884,"extension":1885,"meta":1886,"navigation":151,"path":1887,"seo":1888,"stem":1889,"tags":1890,"updated":1881,"__hash__":1896},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects\u002Findex.md","Sharing State with Click Context Objects",{"type":7,"value":8,"toc":1868},"minimark",[9,26,31,97,101,114,118,124,260,286,289,293,302,621,635,639,652,724,755,759,772,775,862,1049,1059,1063,1079,1118,1183,1214,1218,1233,1236,1377,1400,1404,1414,1500,1514,1518,1528,1736,1754,1758,1834,1838,1864],[10,11,12,13,17,18,22,23,25],"p",{},"A grouped Click CLI needs a way to hand shared state — resolved config, an open database\nhandle, an API client — from the top-level group down to whichever subcommand runs. Click's\nanswer is the ",[14,15,16],"strong",{},"Context",": an object threaded through every invocation, with a free-form\n",[19,20,21],"code",{},"ctx.obj"," slot you own. Done well, it gives every command a typed handle on shared services\nwhile keeping each one testable in isolation. Done carelessly, ",[19,24,21],{}," becomes a stringly-typed\ngrab bag. This guide shows the disciplined version.",[27,28,30],"h2",{"id":29},"tldr","TL;DR",[32,33,34,48,58,68,90],"ul",{},[35,36,37,38,40,41,44,45,47],"li",{},"Click passes a ",[19,39,16],{}," to any command decorated with ",[19,42,43],{},"@click.pass_context","; its ",[19,46,21],{},"\nattribute is yours to fill with shared state.",[35,49,50,51,54,55,57],{},"Load config once in the ",[14,52,53],{},"group callback",", store it on ",[19,56,21],{},", and every subcommand can\nread it.",[35,59,60,61,64,65,67],{},"Use ",[19,62,63],{},"ctx.ensure_object(dict)"," so ",[19,66,21],{}," exists even when a subcommand is invoked directly\nin a test.",[35,69,70,71,74,75,77,78,81,82,85,86,89],{},"Prefer a ",[14,72,73],{},"dataclass"," on ",[19,76,21],{}," over a bare dict, and inject it with ",[19,79,80],{},"@click.pass_obj","\nor a custom ",[19,83,84],{},"pass_config"," decorator built from ",[19,87,88],{},"make_pass_decorator",".",[35,91,92,93,96],{},"Test commands with ",[19,94,95],{},"CliRunner(...).invoke(cli, [...], obj=...)"," to inject state directly.",[27,98,100],{"id":99},"the-context-and-ctxobj","The Context and ctx.obj",[10,102,103,104,106,107,110,111,113],{},"Every time Click runs a command it builds a ",[19,105,16],{},". Groups create a context, and each\nsubcommand gets a child context whose ",[19,108,109],{},"obj"," is inherited from the parent by default. That\ninheritance is the whole mechanism: set ",[19,112,21],{}," in the group, read it in the child.",[115,116],"inline-diagram",{"name":117},"click-context-chain",[10,119,120,121,123],{},"Grab the context with ",[19,122,43],{},", which injects it as the first parameter:",[125,126,131],"pre",{"className":127,"code":128,"language":129,"meta":130,"style":130},"language-python shiki shiki-themes github-light github-dark","import click\n\n@click.group()\n@click.pass_context\ndef cli(ctx: click.Context) -> None:\n    ctx.obj = {\"user\": \"martin\"}          # available to every subcommand\n\n@cli.command()\n@click.pass_context\ndef whoami(ctx: click.Context) -> None:\n    click.echo(ctx.obj[\"user\"])\n","python","",[19,132,133,146,153,163,169,188,217,222,230,235,249],{"__ignoreMap":130},[134,135,138,142],"span",{"class":136,"line":137},"line",1,[134,139,141],{"class":140},"szBVR","import",[134,143,145],{"class":144},"sVt8B"," click\n",[134,147,149],{"class":136,"line":148},2,[134,150,152],{"emptyLinePlaceholder":151},true,"\n",[134,154,156,160],{"class":136,"line":155},3,[134,157,159],{"class":158},"sScJk","@click.group",[134,161,162],{"class":144},"()\n",[134,164,166],{"class":136,"line":165},4,[134,167,168],{"class":158},"@click.pass_context\n",[134,170,172,175,178,181,185],{"class":136,"line":171},5,[134,173,174],{"class":140},"def",[134,176,177],{"class":158}," cli",[134,179,180],{"class":144},"(ctx: click.Context) -> ",[134,182,184],{"class":183},"sj4cs","None",[134,186,187],{"class":144},":\n",[134,189,191,194,197,200,204,207,210,213],{"class":136,"line":190},6,[134,192,193],{"class":144},"    ctx.obj ",[134,195,196],{"class":140},"=",[134,198,199],{"class":144}," {",[134,201,203],{"class":202},"sZZnC","\"user\"",[134,205,206],{"class":144},": ",[134,208,209],{"class":202},"\"martin\"",[134,211,212],{"class":144},"}          ",[134,214,216],{"class":215},"sJ8bj","# available to every subcommand\n",[134,218,220],{"class":136,"line":219},7,[134,221,152],{"emptyLinePlaceholder":151},[134,223,225,228],{"class":136,"line":224},8,[134,226,227],{"class":158},"@cli.command",[134,229,162],{"class":144},[134,231,233],{"class":136,"line":232},9,[134,234,168],{"class":158},[134,236,238,240,243,245,247],{"class":136,"line":237},10,[134,239,174],{"class":140},[134,241,242],{"class":158}," whoami",[134,244,180],{"class":144},[134,246,184],{"class":183},[134,248,187],{"class":144},[134,250,252,255,257],{"class":136,"line":251},11,[134,253,254],{"class":144},"    click.echo(ctx.obj[",[134,256,203],{"class":202},[134,258,259],{"class":144},"])\n",[125,261,265],{"className":262,"code":263,"language":264,"meta":130,"style":130},"language-bash shiki shiki-themes github-light github-dark","$ python app.py whoami\nmartin\n","bash",[19,266,267,281],{"__ignoreMap":130},[134,268,269,272,275,278],{"class":136,"line":137},[134,270,271],{"class":158},"$",[134,273,274],{"class":202}," python",[134,276,277],{"class":202}," app.py",[134,279,280],{"class":202}," whoami\n",[134,282,283],{"class":136,"line":148},[134,284,285],{"class":158},"martin\n",[10,287,288],{},"The group body runs before the chosen subcommand, so it is the correct place to populate\nshared state. The subcommand reads it back off the inherited context.",[27,290,292],{"id":291},"a-group-callback-that-loads-config","A group callback that loads config",[10,294,295,296,298,299,301],{},"The realistic version: the group parses global options, loads a config file, layers\nenvironment variables and flags on top, and stashes the result so no subcommand re-reads the\nconfig. ",[19,297,63],{}," creates ",[19,300,21],{}," as a dict if nothing set it yet — which\nis what makes a subcommand safe to invoke on its own.",[125,303,305],{"className":127,"code":304,"language":129,"meta":130,"style":130},"# app\u002Fcli.py\nimport os\nimport click\n\n@click.group()\n@click.option(\"--config\", type=click.Path(dir_okay=False), default=\"config.toml\")\n@click.option(\"-v\", \"--verbose\", is_flag=True)\n@click.pass_context\ndef cli(ctx: click.Context, config: str, verbose: bool) -> None:\n    \"\"\"app — resolves config once, shares it with every command.\"\"\"\n    ctx.ensure_object(dict)\n    ctx.obj[\"verbose\"] = verbose\n    # Precedence: explicit flag > env var > config file default.\n    ctx.obj[\"region\"] = os.environ.get(\"APP_REGION\", \"us-east-1\")\n    ctx.obj[\"config_path\"] = config\n\n@cli.command()\n@click.pass_context\ndef deploy(ctx: click.Context) -> None:\n    \"\"\"Deploy using the shared, resolved config.\"\"\"\n    if ctx.obj[\"verbose\"]:\n        click.echo(f\"[verbose] region={ctx.obj['region']}\")\n    click.echo(f\"Deploying to {ctx.obj['region']}\")\n",[19,306,307,312,319,325,329,335,380,406,410,435,440,450,467,473,498,513,518,525,530,544,550,564,596],{"__ignoreMap":130},[134,308,309],{"class":136,"line":137},[134,310,311],{"class":215},"# app\u002Fcli.py\n",[134,313,314,316],{"class":136,"line":148},[134,315,141],{"class":140},[134,317,318],{"class":144}," os\n",[134,320,321,323],{"class":136,"line":155},[134,322,141],{"class":140},[134,324,145],{"class":144},[134,326,327],{"class":136,"line":165},[134,328,152],{"emptyLinePlaceholder":151},[134,330,331,333],{"class":136,"line":171},[134,332,159],{"class":158},[134,334,162],{"class":144},[134,336,337,340,343,346,349,353,355,358,361,363,366,369,372,374,377],{"class":136,"line":190},[134,338,339],{"class":158},"@click.option",[134,341,342],{"class":144},"(",[134,344,345],{"class":202},"\"--config\"",[134,347,348],{"class":144},", ",[134,350,352],{"class":351},"s4XuR","type",[134,354,196],{"class":140},[134,356,357],{"class":144},"click.Path(",[134,359,360],{"class":351},"dir_okay",[134,362,196],{"class":140},[134,364,365],{"class":183},"False",[134,367,368],{"class":144},"), ",[134,370,371],{"class":351},"default",[134,373,196],{"class":140},[134,375,376],{"class":202},"\"config.toml\"",[134,378,379],{"class":144},")\n",[134,381,382,384,386,389,391,394,396,399,401,404],{"class":136,"line":219},[134,383,339],{"class":158},[134,385,342],{"class":144},[134,387,388],{"class":202},"\"-v\"",[134,390,348],{"class":144},[134,392,393],{"class":202},"\"--verbose\"",[134,395,348],{"class":144},[134,397,398],{"class":351},"is_flag",[134,400,196],{"class":140},[134,402,403],{"class":183},"True",[134,405,379],{"class":144},[134,407,408],{"class":136,"line":224},[134,409,168],{"class":158},[134,411,412,414,416,419,422,425,428,431,433],{"class":136,"line":232},[134,413,174],{"class":140},[134,415,177],{"class":158},[134,417,418],{"class":144},"(ctx: click.Context, config: ",[134,420,421],{"class":183},"str",[134,423,424],{"class":144},", verbose: ",[134,426,427],{"class":183},"bool",[134,429,430],{"class":144},") -> ",[134,432,184],{"class":183},[134,434,187],{"class":144},[134,436,437],{"class":136,"line":237},[134,438,439],{"class":202},"    \"\"\"app — resolves config once, shares it with every command.\"\"\"\n",[134,441,442,445,448],{"class":136,"line":251},[134,443,444],{"class":144},"    ctx.ensure_object(",[134,446,447],{"class":183},"dict",[134,449,379],{"class":144},[134,451,453,456,459,462,464],{"class":136,"line":452},12,[134,454,455],{"class":144},"    ctx.obj[",[134,457,458],{"class":202},"\"verbose\"",[134,460,461],{"class":144},"] ",[134,463,196],{"class":140},[134,465,466],{"class":144}," verbose\n",[134,468,470],{"class":136,"line":469},13,[134,471,472],{"class":215},"    # Precedence: explicit flag > env var > config file default.\n",[134,474,476,478,481,483,485,488,491,493,496],{"class":136,"line":475},14,[134,477,455],{"class":144},[134,479,480],{"class":202},"\"region\"",[134,482,461],{"class":144},[134,484,196],{"class":140},[134,486,487],{"class":144}," os.environ.get(",[134,489,490],{"class":202},"\"APP_REGION\"",[134,492,348],{"class":144},[134,494,495],{"class":202},"\"us-east-1\"",[134,497,379],{"class":144},[134,499,501,503,506,508,510],{"class":136,"line":500},15,[134,502,455],{"class":144},[134,504,505],{"class":202},"\"config_path\"",[134,507,461],{"class":144},[134,509,196],{"class":140},[134,511,512],{"class":144}," config\n",[134,514,516],{"class":136,"line":515},16,[134,517,152],{"emptyLinePlaceholder":151},[134,519,521,523],{"class":136,"line":520},17,[134,522,227],{"class":158},[134,524,162],{"class":144},[134,526,528],{"class":136,"line":527},18,[134,529,168],{"class":158},[134,531,533,535,538,540,542],{"class":136,"line":532},19,[134,534,174],{"class":140},[134,536,537],{"class":158}," deploy",[134,539,180],{"class":144},[134,541,184],{"class":183},[134,543,187],{"class":144},[134,545,547],{"class":136,"line":546},20,[134,548,549],{"class":202},"    \"\"\"Deploy using the shared, resolved config.\"\"\"\n",[134,551,553,556,559,561],{"class":136,"line":552},21,[134,554,555],{"class":140},"    if",[134,557,558],{"class":144}," ctx.obj[",[134,560,458],{"class":202},[134,562,563],{"class":144},"]:\n",[134,565,567,570,573,576,579,582,585,588,591,594],{"class":136,"line":566},22,[134,568,569],{"class":144},"        click.echo(",[134,571,572],{"class":140},"f",[134,574,575],{"class":202},"\"[verbose] region=",[134,577,578],{"class":183},"{",[134,580,581],{"class":144},"ctx.obj[",[134,583,584],{"class":202},"'region'",[134,586,587],{"class":144},"]",[134,589,590],{"class":183},"}",[134,592,593],{"class":202},"\"",[134,595,379],{"class":144},[134,597,599,602,604,607,609,611,613,615,617,619],{"class":136,"line":598},23,[134,600,601],{"class":144},"    click.echo(",[134,603,572],{"class":140},[134,605,606],{"class":202},"\"Deploying to ",[134,608,578],{"class":183},[134,610,581],{"class":144},[134,612,584],{"class":202},[134,614,587],{"class":144},[134,616,590],{"class":183},[134,618,593],{"class":202},[134,620,379],{"class":144},[10,622,623,624,629,630,634],{},"For the full precedence story — flags over environment over files over defaults — see\n",[625,626,628],"a",{"href":627},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults\u002F","config precedence: flags, env, files, defaults",".\nThe point here is ",[631,632,633],"em",{},"where"," it happens: once, in the callback, not scattered across commands.",[27,636,638],{"id":637},"pass_obj-skip-the-ceremony","pass_obj: skip the ceremony",[10,640,641,642,644,645,647,648,651],{},"When a command only needs ",[19,643,21],{}," and not the whole context, ",[19,646,80],{}," injects the\nobject directly, so you drop the ",[19,649,650],{},"ctx."," prefix everywhere:",[125,653,655],{"className":127,"code":654,"language":129,"meta":130,"style":130},"@cli.command()\n@click.pass_obj\ndef status(obj: dict) -> None:\n    click.echo(f\"region={obj['region']} verbose={obj['verbose']}\")\n",[19,656,657,663,668,686],{"__ignoreMap":130},[134,658,659,661],{"class":136,"line":137},[134,660,227],{"class":158},[134,662,162],{"class":144},[134,664,665],{"class":136,"line":148},[134,666,667],{"class":158},"@click.pass_obj\n",[134,669,670,672,675,678,680,682,684],{"class":136,"line":155},[134,671,174],{"class":140},[134,673,674],{"class":158}," status",[134,676,677],{"class":144},"(obj: ",[134,679,447],{"class":183},[134,681,430],{"class":144},[134,683,184],{"class":183},[134,685,187],{"class":144},[134,687,688,690,692,695,697,700,702,704,706,709,711,713,716,718,720,722],{"class":136,"line":165},[134,689,601],{"class":144},[134,691,572],{"class":140},[134,693,694],{"class":202},"\"region=",[134,696,578],{"class":183},[134,698,699],{"class":144},"obj[",[134,701,584],{"class":202},[134,703,587],{"class":144},[134,705,590],{"class":183},[134,707,708],{"class":202}," verbose=",[134,710,578],{"class":183},[134,712,699],{"class":144},[134,714,715],{"class":202},"'verbose'",[134,717,587],{"class":144},[134,719,590],{"class":183},[134,721,593],{"class":202},[134,723,379],{"class":144},[10,725,726,727,729,730,733,734,737,738,740,741,744,745,747,748,348,751,754],{},"This is the same ",[19,728,21],{}," you set in the group — ",[19,731,732],{},"pass_obj"," is just ",[19,735,736],{},"pass_context"," that\nhands you ",[19,739,21],{}," instead of ",[19,742,743],{},"ctx",". Reach for it in the common case; keep ",[19,746,736],{},"\nfor commands that also need ",[19,749,750],{},"ctx.exit()",[19,752,753],{},"ctx.call_on_close()",", or sub-context work.",[27,756,758],{"id":757},"the-dataclass-pattern","The dataclass pattern",[10,760,761,762,764,765,768,769,771],{},"A bare dict on ",[19,763,21],{}," invites bugs: a mistyped ",[19,766,767],{},"ctx.obj[\"reigon\"]"," fails at runtime, and\nyour editor can't help. For anything past a flag or two, store a ",[14,770,73],{},". You get\nattribute access, autocompletion, and a mypy-checked shape.",[115,773],{"name":774},"ctx-obj-dataclass-flow",[125,776,778],{"className":127,"code":777,"language":129,"meta":130,"style":130},"# app\u002Fstate.py\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass AppConfig:\n    region: str\n    verbose: bool = False\n    tags: list[str] = field(default_factory=list)\n",[19,779,780,785,798,802,807,817,825,838],{"__ignoreMap":130},[134,781,782],{"class":136,"line":137},[134,783,784],{"class":215},"# app\u002Fstate.py\n",[134,786,787,790,793,795],{"class":136,"line":148},[134,788,789],{"class":140},"from",[134,791,792],{"class":144}," dataclasses ",[134,794,141],{"class":140},[134,796,797],{"class":144}," dataclass, field\n",[134,799,800],{"class":136,"line":155},[134,801,152],{"emptyLinePlaceholder":151},[134,803,804],{"class":136,"line":165},[134,805,806],{"class":158},"@dataclass\n",[134,808,809,812,815],{"class":136,"line":171},[134,810,811],{"class":140},"class",[134,813,814],{"class":158}," AppConfig",[134,816,187],{"class":144},[134,818,819,822],{"class":136,"line":190},[134,820,821],{"class":144},"    region: ",[134,823,824],{"class":183},"str\n",[134,826,827,830,832,835],{"class":136,"line":219},[134,828,829],{"class":144},"    verbose: ",[134,831,427],{"class":183},[134,833,834],{"class":140}," =",[134,836,837],{"class":183}," False\n",[134,839,840,843,845,847,849,852,855,857,860],{"class":136,"line":224},[134,841,842],{"class":144},"    tags: list[",[134,844,421],{"class":183},[134,846,461],{"class":144},[134,848,196],{"class":140},[134,850,851],{"class":144}," field(",[134,853,854],{"class":351},"default_factory",[134,856,196],{"class":140},[134,858,859],{"class":183},"list",[134,861,379],{"class":144},[125,863,865],{"className":127,"code":864,"language":129,"meta":130,"style":130},"# app\u002Fcli.py\nimport click\nfrom app.state import AppConfig\n\n@click.group()\n@click.option(\"--region\", default=\"us-east-1\")\n@click.option(\"-v\", \"--verbose\", is_flag=True)\n@click.pass_context\ndef cli(ctx: click.Context, region: str, verbose: bool) -> None:\n    ctx.obj = AppConfig(region=region, verbose=verbose)\n\n@cli.command()\n@click.pass_obj\ndef status(cfg: AppConfig) -> None:          # typed, autocompleted\n    click.echo(f\"region={cfg.region} verbose={cfg.verbose}\")\n",[19,866,867,871,877,889,893,899,918,940,944,965,990,994,1000,1004,1021],{"__ignoreMap":130},[134,868,869],{"class":136,"line":137},[134,870,311],{"class":215},[134,872,873,875],{"class":136,"line":148},[134,874,141],{"class":140},[134,876,145],{"class":144},[134,878,879,881,884,886],{"class":136,"line":155},[134,880,789],{"class":140},[134,882,883],{"class":144}," app.state ",[134,885,141],{"class":140},[134,887,888],{"class":144}," AppConfig\n",[134,890,891],{"class":136,"line":165},[134,892,152],{"emptyLinePlaceholder":151},[134,894,895,897],{"class":136,"line":171},[134,896,159],{"class":158},[134,898,162],{"class":144},[134,900,901,903,905,908,910,912,914,916],{"class":136,"line":190},[134,902,339],{"class":158},[134,904,342],{"class":144},[134,906,907],{"class":202},"\"--region\"",[134,909,348],{"class":144},[134,911,371],{"class":351},[134,913,196],{"class":140},[134,915,495],{"class":202},[134,917,379],{"class":144},[134,919,920,922,924,926,928,930,932,934,936,938],{"class":136,"line":219},[134,921,339],{"class":158},[134,923,342],{"class":144},[134,925,388],{"class":202},[134,927,348],{"class":144},[134,929,393],{"class":202},[134,931,348],{"class":144},[134,933,398],{"class":351},[134,935,196],{"class":140},[134,937,403],{"class":183},[134,939,379],{"class":144},[134,941,942],{"class":136,"line":224},[134,943,168],{"class":158},[134,945,946,948,950,953,955,957,959,961,963],{"class":136,"line":232},[134,947,174],{"class":140},[134,949,177],{"class":158},[134,951,952],{"class":144},"(ctx: click.Context, region: ",[134,954,421],{"class":183},[134,956,424],{"class":144},[134,958,427],{"class":183},[134,960,430],{"class":144},[134,962,184],{"class":183},[134,964,187],{"class":144},[134,966,967,969,971,974,977,979,982,985,987],{"class":136,"line":237},[134,968,193],{"class":144},[134,970,196],{"class":140},[134,972,973],{"class":144}," AppConfig(",[134,975,976],{"class":351},"region",[134,978,196],{"class":140},[134,980,981],{"class":144},"region, ",[134,983,984],{"class":351},"verbose",[134,986,196],{"class":140},[134,988,989],{"class":144},"verbose)\n",[134,991,992],{"class":136,"line":251},[134,993,152],{"emptyLinePlaceholder":151},[134,995,996,998],{"class":136,"line":452},[134,997,227],{"class":158},[134,999,162],{"class":144},[134,1001,1002],{"class":136,"line":469},[134,1003,667],{"class":158},[134,1005,1006,1008,1010,1013,1015,1018],{"class":136,"line":475},[134,1007,174],{"class":140},[134,1009,674],{"class":158},[134,1011,1012],{"class":144},"(cfg: AppConfig) -> ",[134,1014,184],{"class":183},[134,1016,1017],{"class":144},":          ",[134,1019,1020],{"class":215},"# typed, autocompleted\n",[134,1022,1023,1025,1027,1029,1031,1034,1036,1038,1040,1043,1045,1047],{"class":136,"line":500},[134,1024,601],{"class":144},[134,1026,572],{"class":140},[134,1028,694],{"class":202},[134,1030,578],{"class":183},[134,1032,1033],{"class":144},"cfg.region",[134,1035,590],{"class":183},[134,1037,708],{"class":202},[134,1039,578],{"class":183},[134,1041,1042],{"class":144},"cfg.verbose",[134,1044,590],{"class":183},[134,1046,593],{"class":202},[134,1048,379],{"class":144},[10,1050,1051,1053,1054,1058],{},[19,1052,1033],{}," is now checked at author time; a typo is a type error, not a 2 a.m. traceback.\nThis mirrors the typed-state approach in\n",[625,1055,1057],{"href":1056},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click\u002F","building a CLI with subcommands in Click",",\nscaled up to a real config object.",[27,1060,1062],{"id":1061},"a-custom-pass_config-decorator","A custom pass_config decorator",[10,1064,1065,1067,1068,1070,1071,1074,1075,1078],{},[19,1066,80],{}," is untyped — every command annotates the parameter itself and trusts that\n",[19,1069,21],{}," really is an ",[19,1072,1073],{},"AppConfig",". ",[19,1076,1077],{},"click.make_pass_decorator"," builds a decorator bound to a\nspecific type, giving you one named, self-documenting injector:",[125,1080,1082],{"className":127,"code":1081,"language":129,"meta":130,"style":130},"# app\u002Fstate.py (continued)\nimport click\n\npass_config = click.make_pass_decorator(AppConfig, ensure=True)\n",[19,1083,1084,1089,1095,1099],{"__ignoreMap":130},[134,1085,1086],{"class":136,"line":137},[134,1087,1088],{"class":215},"# app\u002Fstate.py (continued)\n",[134,1090,1091,1093],{"class":136,"line":148},[134,1092,141],{"class":140},[134,1094,145],{"class":144},[134,1096,1097],{"class":136,"line":155},[134,1098,152],{"emptyLinePlaceholder":151},[134,1100,1101,1104,1106,1109,1112,1114,1116],{"class":136,"line":165},[134,1102,1103],{"class":144},"pass_config ",[134,1105,196],{"class":140},[134,1107,1108],{"class":144}," click.make_pass_decorator(AppConfig, ",[134,1110,1111],{"class":351},"ensure",[134,1113,196],{"class":140},[134,1115,403],{"class":183},[134,1117,379],{"class":144},[125,1119,1121],{"className":127,"code":1120,"language":129,"meta":130,"style":130},"# app\u002Fcli.py\nfrom app.state import AppConfig, pass_config\n\n@cli.command()\n@pass_config\ndef deploy(cfg: AppConfig) -> None:\n    click.echo(f\"Deploying to {cfg.region}\")\n",[19,1122,1123,1127,1138,1142,1148,1153,1165],{"__ignoreMap":130},[134,1124,1125],{"class":136,"line":137},[134,1126,311],{"class":215},[134,1128,1129,1131,1133,1135],{"class":136,"line":148},[134,1130,789],{"class":140},[134,1132,883],{"class":144},[134,1134,141],{"class":140},[134,1136,1137],{"class":144}," AppConfig, pass_config\n",[134,1139,1140],{"class":136,"line":155},[134,1141,152],{"emptyLinePlaceholder":151},[134,1143,1144,1146],{"class":136,"line":165},[134,1145,227],{"class":158},[134,1147,162],{"class":144},[134,1149,1150],{"class":136,"line":171},[134,1151,1152],{"class":158},"@pass_config\n",[134,1154,1155,1157,1159,1161,1163],{"class":136,"line":190},[134,1156,174],{"class":140},[134,1158,537],{"class":158},[134,1160,1012],{"class":144},[134,1162,184],{"class":183},[134,1164,187],{"class":144},[134,1166,1167,1169,1171,1173,1175,1177,1179,1181],{"class":136,"line":219},[134,1168,601],{"class":144},[134,1170,572],{"class":140},[134,1172,606],{"class":202},[134,1174,578],{"class":183},[134,1176,1033],{"class":144},[134,1178,590],{"class":183},[134,1180,593],{"class":202},[134,1182,379],{"class":144},[10,1184,1185,1188,1189,1191,1192,1195,1196,1199,1200,1203,1204,1206,1207,1209,1210,1213],{},[19,1186,1187],{},"make_pass_decorator(AppConfig)"," walks up the context chain and finds the nearest ",[19,1190,1073],{},"\ninstance, so it works even with nested groups. ",[19,1193,1194],{},"ensure=True"," tells it to ",[631,1197,1198],{},"create"," an\n",[19,1201,1202],{},"AppConfig()"," if none exists yet — handy when a subcommand can run standalone, though it\nrequires your dataclass to be constructible with no arguments (give fields defaults, or drop\n",[19,1205,1111],{}," and set ",[19,1208,21],{}," in the group). The payoff is readability: ",[19,1211,1212],{},"@pass_config"," says\nexactly what it injects, and there is one place to change if the state type evolves.",[27,1215,1217],{"id":1216},"nested-groups-and-context-walking","Nested groups and context walking",[10,1219,1220,1222,1223,1225,1226,1228,1229,1232],{},[19,1221,21],{}," inheritance shines with nested groups. A child group gets a child context whose\n",[19,1224,109],{}," points at the parent's, so state set at the root reaches an arbitrarily deep leaf\nwithout re-passing it. ",[19,1227,88],{}," walks ",[631,1230,1231],{},"up"," the chain to find the nearest\ninstance of the requested type, which means an inner group can layer its own state on top:",[115,1234],{"name":1235},"click-context-walk",[125,1237,1239],{"className":127,"code":1238,"language":129,"meta":130,"style":130},"@cli.group()\n@click.option(\"--namespace\", default=\"default\")\n@click.pass_obj\ndef db(cfg: AppConfig, namespace: str) -> None:\n    \"\"\"Database subcommands, scoped to a namespace.\"\"\"\n    cfg.tags.append(f\"ns:{namespace}\")     # mutate the inherited config\n\n@db.command()\n@pass_config\ndef migrate(cfg: AppConfig) -> None:\n    click.echo(f\"Migrating region={cfg.region} tags={cfg.tags}\")\n",[19,1240,1241,1248,1268,1272,1290,1295,1320,1324,1331,1335,1348],{"__ignoreMap":130},[134,1242,1243,1246],{"class":136,"line":137},[134,1244,1245],{"class":158},"@cli.group",[134,1247,162],{"class":144},[134,1249,1250,1252,1254,1257,1259,1261,1263,1266],{"class":136,"line":148},[134,1251,339],{"class":158},[134,1253,342],{"class":144},[134,1255,1256],{"class":202},"\"--namespace\"",[134,1258,348],{"class":144},[134,1260,371],{"class":351},[134,1262,196],{"class":140},[134,1264,1265],{"class":202},"\"default\"",[134,1267,379],{"class":144},[134,1269,1270],{"class":136,"line":155},[134,1271,667],{"class":158},[134,1273,1274,1276,1279,1282,1284,1286,1288],{"class":136,"line":165},[134,1275,174],{"class":140},[134,1277,1278],{"class":158}," db",[134,1280,1281],{"class":144},"(cfg: AppConfig, namespace: ",[134,1283,421],{"class":183},[134,1285,430],{"class":144},[134,1287,184],{"class":183},[134,1289,187],{"class":144},[134,1291,1292],{"class":136,"line":171},[134,1293,1294],{"class":202},"    \"\"\"Database subcommands, scoped to a namespace.\"\"\"\n",[134,1296,1297,1300,1302,1305,1307,1310,1312,1314,1317],{"class":136,"line":190},[134,1298,1299],{"class":144},"    cfg.tags.append(",[134,1301,572],{"class":140},[134,1303,1304],{"class":202},"\"ns:",[134,1306,578],{"class":183},[134,1308,1309],{"class":144},"namespace",[134,1311,590],{"class":183},[134,1313,593],{"class":202},[134,1315,1316],{"class":144},")     ",[134,1318,1319],{"class":215},"# mutate the inherited config\n",[134,1321,1322],{"class":136,"line":219},[134,1323,152],{"emptyLinePlaceholder":151},[134,1325,1326,1329],{"class":136,"line":224},[134,1327,1328],{"class":158},"@db.command",[134,1330,162],{"class":144},[134,1332,1333],{"class":136,"line":232},[134,1334,1152],{"class":158},[134,1336,1337,1339,1342,1344,1346],{"class":136,"line":237},[134,1338,174],{"class":140},[134,1340,1341],{"class":158}," migrate",[134,1343,1012],{"class":144},[134,1345,184],{"class":183},[134,1347,187],{"class":144},[134,1349,1350,1352,1354,1357,1359,1361,1363,1366,1368,1371,1373,1375],{"class":136,"line":251},[134,1351,601],{"class":144},[134,1353,572],{"class":140},[134,1355,1356],{"class":202},"\"Migrating region=",[134,1358,578],{"class":183},[134,1360,1033],{"class":144},[134,1362,590],{"class":183},[134,1364,1365],{"class":202}," tags=",[134,1367,578],{"class":183},[134,1369,1370],{"class":144},"cfg.tags",[134,1372,590],{"class":183},[134,1374,593],{"class":202},[134,1376,379],{"class":144},[10,1378,1379,1382,1383,1385,1386,1389,1390,1393,1394,1396,1397,1399],{},[19,1380,1381],{},"app --region eu-west-1 db --namespace staging migrate"," flows the root's ",[19,1384,976],{}," and the\n",[19,1387,1388],{},"db"," group's namespace into one ",[19,1391,1392],{},"migrate"," call. Because both groups share the same ",[19,1395,1073],{},"\ninstance on ",[19,1398,21],{},", the child sees the parent's fields and its own additions. Keep the\nmutation deliberate — sharing a mutable object across levels is powerful but means an inner\ngroup can surprise a sibling if it rewrites shared fields.",[27,1401,1403],{"id":1402},"supplying-defaults-through-the-context","Supplying defaults through the context",[10,1405,1406,1407,1409,1410,1413],{},"Beyond ",[19,1408,21],{},", the context carries a ",[19,1411,1412],{},"default_map"," that lets a group feed default values\ninto its subcommands' options — useful when a config file should override an option's\nbuilt-in default without the command knowing where the value came from:",[125,1415,1417],{"className":127,"code":1416,"language":129,"meta":130,"style":130},"@click.group()\n@click.option(\"--config\", type=click.Path(dir_okay=False), default=\"config.toml\")\n@click.pass_context\ndef cli(ctx: click.Context, config: str) -> None:\n    ctx.ensure_object(dict)\n    # e.g. {\"deploy\": {\"region\": \"eu-west-1\"}} loaded from the config file\n    ctx.default_map = load_defaults(config)\n",[19,1418,1419,1425,1457,1461,1477,1485,1490],{"__ignoreMap":130},[134,1420,1421,1423],{"class":136,"line":137},[134,1422,159],{"class":158},[134,1424,162],{"class":144},[134,1426,1427,1429,1431,1433,1435,1437,1439,1441,1443,1445,1447,1449,1451,1453,1455],{"class":136,"line":148},[134,1428,339],{"class":158},[134,1430,342],{"class":144},[134,1432,345],{"class":202},[134,1434,348],{"class":144},[134,1436,352],{"class":351},[134,1438,196],{"class":140},[134,1440,357],{"class":144},[134,1442,360],{"class":351},[134,1444,196],{"class":140},[134,1446,365],{"class":183},[134,1448,368],{"class":144},[134,1450,371],{"class":351},[134,1452,196],{"class":140},[134,1454,376],{"class":202},[134,1456,379],{"class":144},[134,1458,1459],{"class":136,"line":155},[134,1460,168],{"class":158},[134,1462,1463,1465,1467,1469,1471,1473,1475],{"class":136,"line":165},[134,1464,174],{"class":140},[134,1466,177],{"class":158},[134,1468,418],{"class":144},[134,1470,421],{"class":183},[134,1472,430],{"class":144},[134,1474,184],{"class":183},[134,1476,187],{"class":144},[134,1478,1479,1481,1483],{"class":136,"line":171},[134,1480,444],{"class":144},[134,1482,447],{"class":183},[134,1484,379],{"class":144},[134,1486,1487],{"class":136,"line":190},[134,1488,1489],{"class":215},"    # e.g. {\"deploy\": {\"region\": \"eu-west-1\"}} loaded from the config file\n",[134,1491,1492,1495,1497],{"class":136,"line":219},[134,1493,1494],{"class":144},"    ctx.default_map ",[134,1496,196],{"class":140},[134,1498,1499],{"class":144}," load_defaults(config)\n",[10,1501,1502,1503,1506,1507,1510,1511,1513],{},"With that in place, ",[19,1504,1505],{},"deploy","'s ",[19,1508,1509],{},"--region"," option falls back to the config-provided value\ninstead of its hardcoded default, while an explicit ",[19,1512,1509],{}," on the command line still\nwins. This keeps the precedence rules — flag beats config beats default — enforced by Click\nrather than by hand in every command body.",[27,1515,1517],{"id":1516},"testing-with-clirunner-and-obj-injection","Testing with CliRunner and obj injection",[10,1519,1520,1521,1524,1525,1527],{},"The reason to keep state on the context rather than in module globals is testability. Click's\n",[19,1522,1523],{},"CliRunner"," lets you inject ",[19,1526,21],{}," directly, so you can exercise a subcommand with a known\nconfig without running the group's config-loading at all.",[125,1529,1531],{"className":127,"code":1530,"language":129,"meta":130,"style":130},"# tests\u002Ftest_cli.py\nfrom click.testing import CliRunner\nfrom app.cli import cli, deploy\nfrom app.state import AppConfig\n\ndef test_deploy_uses_injected_config() -> None:\n    runner = CliRunner()\n    # Invoke the subcommand directly, injecting the shared object.\n    result = runner.invoke(deploy, obj=AppConfig(region=\"eu-west-1\"), standalone_mode=False)\n    assert result.exit_code == 0\n    assert \"eu-west-1\" in result.output\n\ndef test_group_resolves_config_end_to_end() -> None:\n    runner = CliRunner()\n    result = runner.invoke(cli, [\"--region\", \"ap-south-1\", \"deploy\"])\n    assert result.exit_code == 0\n    assert \"ap-south-1\" in result.output\n",[19,1532,1533,1538,1550,1562,1572,1576,1590,1600,1605,1640,1654,1667,1671,1684,1692,1715,1725],{"__ignoreMap":130},[134,1534,1535],{"class":136,"line":137},[134,1536,1537],{"class":215},"# tests\u002Ftest_cli.py\n",[134,1539,1540,1542,1545,1547],{"class":136,"line":148},[134,1541,789],{"class":140},[134,1543,1544],{"class":144}," click.testing ",[134,1546,141],{"class":140},[134,1548,1549],{"class":144}," CliRunner\n",[134,1551,1552,1554,1557,1559],{"class":136,"line":155},[134,1553,789],{"class":140},[134,1555,1556],{"class":144}," app.cli ",[134,1558,141],{"class":140},[134,1560,1561],{"class":144}," cli, deploy\n",[134,1563,1564,1566,1568,1570],{"class":136,"line":165},[134,1565,789],{"class":140},[134,1567,883],{"class":144},[134,1569,141],{"class":140},[134,1571,888],{"class":144},[134,1573,1574],{"class":136,"line":171},[134,1575,152],{"emptyLinePlaceholder":151},[134,1577,1578,1580,1583,1586,1588],{"class":136,"line":190},[134,1579,174],{"class":140},[134,1581,1582],{"class":158}," test_deploy_uses_injected_config",[134,1584,1585],{"class":144},"() -> ",[134,1587,184],{"class":183},[134,1589,187],{"class":144},[134,1591,1592,1595,1597],{"class":136,"line":219},[134,1593,1594],{"class":144},"    runner ",[134,1596,196],{"class":140},[134,1598,1599],{"class":144}," CliRunner()\n",[134,1601,1602],{"class":136,"line":224},[134,1603,1604],{"class":215},"    # Invoke the subcommand directly, injecting the shared object.\n",[134,1606,1607,1610,1612,1615,1617,1619,1622,1624,1626,1629,1631,1634,1636,1638],{"class":136,"line":232},[134,1608,1609],{"class":144},"    result ",[134,1611,196],{"class":140},[134,1613,1614],{"class":144}," runner.invoke(deploy, ",[134,1616,109],{"class":351},[134,1618,196],{"class":140},[134,1620,1621],{"class":144},"AppConfig(",[134,1623,976],{"class":351},[134,1625,196],{"class":140},[134,1627,1628],{"class":202},"\"eu-west-1\"",[134,1630,368],{"class":144},[134,1632,1633],{"class":351},"standalone_mode",[134,1635,196],{"class":140},[134,1637,365],{"class":183},[134,1639,379],{"class":144},[134,1641,1642,1645,1648,1651],{"class":136,"line":237},[134,1643,1644],{"class":140},"    assert",[134,1646,1647],{"class":144}," result.exit_code ",[134,1649,1650],{"class":140},"==",[134,1652,1653],{"class":183}," 0\n",[134,1655,1656,1658,1661,1664],{"class":136,"line":251},[134,1657,1644],{"class":140},[134,1659,1660],{"class":202}," \"eu-west-1\"",[134,1662,1663],{"class":140}," in",[134,1665,1666],{"class":144}," result.output\n",[134,1668,1669],{"class":136,"line":452},[134,1670,152],{"emptyLinePlaceholder":151},[134,1672,1673,1675,1678,1680,1682],{"class":136,"line":469},[134,1674,174],{"class":140},[134,1676,1677],{"class":158}," test_group_resolves_config_end_to_end",[134,1679,1585],{"class":144},[134,1681,184],{"class":183},[134,1683,187],{"class":144},[134,1685,1686,1688,1690],{"class":136,"line":475},[134,1687,1594],{"class":144},[134,1689,196],{"class":140},[134,1691,1599],{"class":144},[134,1693,1694,1696,1698,1701,1703,1705,1708,1710,1713],{"class":136,"line":500},[134,1695,1609],{"class":144},[134,1697,196],{"class":140},[134,1699,1700],{"class":144}," runner.invoke(cli, [",[134,1702,907],{"class":202},[134,1704,348],{"class":144},[134,1706,1707],{"class":202},"\"ap-south-1\"",[134,1709,348],{"class":144},[134,1711,1712],{"class":202},"\"deploy\"",[134,1714,259],{"class":144},[134,1716,1717,1719,1721,1723],{"class":136,"line":515},[134,1718,1644],{"class":140},[134,1720,1647],{"class":144},[134,1722,1650],{"class":140},[134,1724,1653],{"class":183},[134,1726,1727,1729,1732,1734],{"class":136,"line":520},[134,1728,1644],{"class":140},[134,1730,1731],{"class":202}," \"ap-south-1\"",[134,1733,1663],{"class":140},[134,1735,1666],{"class":144},[10,1737,1738,1739,1742,1743,1745,1746,1749,1750,89],{},"Two complementary tests: one injects ",[19,1740,1741],{},"obj="," to test the command in isolation (fast, no config\nplumbing), the other runs the group end to end to confirm the callback wires state correctly.\nPassing ",[19,1744,1741],{}," to ",[19,1747,1748],{},"invoke"," is what decouples the two — each command stays independently\ntestable, which is the entire argument for this pattern over global state. This layering is a\nconcrete case of the discipline in\n",[625,1751,1753],{"href":1752},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","structuring multi-command Python CLIs",[27,1755,1757],{"id":1756},"production-notes","Production notes",[32,1759,1760,1783,1796,1805,1822],{},[35,1761,1762,1768,1769,1771,1772,1775,1776,1778,1779,1782],{},[14,1763,1764,1767],{},[19,1765,1766],{},"ensure_object"," vs direct assignment."," ",[19,1770,63],{}," is idempotent and\nsafe when a child might run before the parent set anything; ",[19,1773,1774],{},"ctx.obj = AppConfig(...)","\noverwrites unconditionally. Use ",[19,1777,1766],{}," for dicts, direct assignment (or\n",[19,1780,1781],{},"make_pass_decorator(..., ensure=True)",") for dataclasses.",[35,1784,1785,1788,1789,1792,1793,1795],{},[14,1786,1787],{},"Don't smuggle globals."," The temptation is a module-level ",[19,1790,1791],{},"CONFIG"," singleton. It makes\ntests order-dependent and breaks under Click's ",[19,1794,1523],{},", which reuses the process. Keep\nstate on the context.",[35,1797,1798,1801,1802,1804],{},[14,1799,1800],{},"Context objects and lazy loading."," If the group callback opens an expensive resource\n(DB, network client), consider deferring it — build a factory on ",[19,1803,21],{}," and connect on\nfirst use, so commands that don't need it stay fast. This dovetails with lazy subcommand\nloading covered under CLI startup performance.",[35,1806,1807,1813,1814,1817,1818,1821],{},[14,1808,1809,1812],{},[19,1810,1811],{},"call_on_close"," for teardown."," Resources you open in the group (files, connections)\nshould be released with ",[19,1815,1816],{},"ctx.call_on_close(handle.close)",", which fires when the context\nexits even on error — cleaner than a ",[19,1819,1820],{},"try\u002Ffinally"," around dispatch.",[35,1823,1824,1827,1828,1830,1831,1833],{},[14,1825,1826],{},"Pin Click ≥8.1"," for the current ",[19,1829,88],{}," and ",[19,1832,16],{}," typing behaviour.",[27,1835,1837],{"id":1836},"related","Related",[32,1839,1840,1846,1852,1858],{},[35,1841,1842,1843],{},"Up: ",[625,1844,1845],{"href":1752},"Structuring multi-command Python CLIs",[35,1847,1848,1849],{},"Sideways: ",[625,1850,1851],{"href":1056},"Building a CLI with subcommands in Click",[35,1853,1848,1854],{},[625,1855,1857],{"href":1856},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click: when to use each",[35,1859,1860,1861],{},"Related: ",[625,1862,1863],{"href":627},"Config precedence: flags, env, files, defaults",[1865,1866,1867],"style",{},"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 .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 pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":130,"searchDepth":148,"depth":148,"links":1869},[1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880],{"id":29,"depth":148,"text":30},{"id":99,"depth":148,"text":100},{"id":291,"depth":148,"text":292},{"id":637,"depth":148,"text":638},{"id":757,"depth":148,"text":758},{"id":1061,"depth":148,"text":1062},{"id":1216,"depth":148,"text":1217},{"id":1402,"depth":148,"text":1403},{"id":1516,"depth":148,"text":1517},{"id":1756,"depth":148,"text":1757},{"id":1836,"depth":148,"text":1837},"2026-07-05","Pass configuration and shared services between Click commands with ctx.obj and pass_context, set defaults in a group callback, and keep commands testable.","advanced",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects",{"title":5,"description":1882},"modern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects\u002Findex",[1891,1892,1893,1894,1895],"click","context","structure","config","testing","wOTecp59uLJM7Pp5EkhNii3KKAgfLQwBt0qj9BMqP8g",[1898,1901,1904,1907,1910,1913,1916,1919,1922,1925,1928,1931,1934,1937,1940,1943,1946,1949,1952,1955,1958,1961,1964,1967,1970,1973,1976,1979,1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2039,2040,2043,2046,2049,2052,2055,2058,2060,2063,2066,2069,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141],{"path":1899,"title":1900},"\u002Fabout","About Python CLI Toolcraft",{"path":1902,"title":1903},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1905,"title":1906},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1908,"title":1909},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1911,"title":1912},"\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":1914,"title":1915},"\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":1917,"title":1918},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1920,"title":1921},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1923,"title":1924},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1926,"title":1927},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1929,"title":1930},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1932,"title":1933},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1935,"title":1936},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1938,"title":1939},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1941,"title":1942},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1944,"title":1945},"\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":1947,"title":1948},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1950,"title":1951},"\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":1953,"title":1954},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1956,"title":1957},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1959,"title":1960},"\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":1962,"title":1963},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1965,"title":1966},"\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":1968,"title":1969},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1971,"title":1972},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1974,"title":1975},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1977,"title":1978},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1980,"title":1981},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1983,"title":1984},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1986,"title":1987},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1989,"title":1990},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1992,"title":1993},"\u002F","Python CLI Toolcraft",{"path":1995,"title":1996},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1998,"title":1999},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2001,"title":2002},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2004,"title":2005},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2007,"title":2008},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2010,"title":2011},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2013,"title":2014},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2016,"title":2017},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2019,"title":2020},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2022,"title":2023},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2025,"title":2026},"\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":2028,"title":2029},"\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":2031,"title":2032},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2034,"title":2035},"\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":2037,"title":2038},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1887,"title":5},{"path":2041,"title":2042},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2044,"title":2045},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2047,"title":2048},"\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":2050,"title":2051},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2053,"title":2054},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2056,"title":2057},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2059,"title":1851},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click",{"path":2061,"title":2062},"\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":2064,"title":2065},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2067,"title":2068},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2070,"title":2071},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2073,"title":2074},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2076,"title":2077},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2079,"title":2080},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2082,"title":2083},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2085,"title":2086},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2088,"title":2089},"\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":2091,"title":2092},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2094,"title":2095},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2097,"title":2098},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2100,"title":2101},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2103,"title":2104},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2106,"title":2107},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2109,"title":2110},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2112,"title":2113},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2115,"title":2116},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2118,"title":2119},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2121,"title":2122},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2124,"title":2125},"\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":2127,"title":2128},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2130,"title":2131},"\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":2133,"title":2134},"\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":2136,"title":2137},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2139,"title":2140},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2142,"title":2143},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690032]