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