[{"data":1,"prerenderedAt":1978},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags\u002F":3,"content-directory":1829},{"id":4,"title":5,"body":6,"date":1815,"description":1816,"difficulty":1817,"draft":1818,"extension":1819,"meta":1820,"navigation":166,"path":1821,"seo":1822,"stem":1823,"tags":1824,"updated":1815,"__hash__":1828},"content\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags\u002Findex.md","Adding Verbose and Quiet Logging Flags",{"type":7,"value":8,"toc":1804},"minimark",[9,42,47,113,117,123,304,322,326,346,708,796,812,816,825,969,984,1088,1103,1107,1120,1155,1169,1231,1259,1263,1275,1485,1503,1507,1514,1683,1698,1702,1771,1775,1800],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37,38,41],"p",{},"Users want to control how much your CLI says without editing config or setting env vars. The convention they already know is ",[14,15,16],"code",{},"-v"," for more detail (",[14,19,20],{},"-vv"," for even more) and ",[14,23,24],{},"-q","\u002F",[14,27,28],{},"--quiet"," for near-silence. This page shows how to map those flags onto Python ",[14,31,32],{},"logging"," levels with a single small function, wire them into a Click or Typer callback, make verbose and quiet mutually exclusive, and keep everything on ",[14,35,36],{},"stderr"," so ",[14,39,40],{},"stdout"," stays pipe-safe.",[43,44,46],"h2",{"id":45},"tldr","TL;DR",[48,49,50,79,88,91,100],"ul",{},[51,52,53,54,56,57,60,61,63,64,60,67,63,69,72,73,63,75,78],"li",{},"Count ",[14,55,16],{},": none → ",[14,58,59],{},"WARNING",", ",[14,62,16],{}," → ",[14,65,66],{},"INFO",[14,68,20],{},[14,70,71],{},"DEBUG",". ",[14,74,28],{},[14,76,77],{},"ERROR",".",[51,80,81,82,84,85,87],{},"Default to ",[14,83,59],{},", not ",[14,86,66],{}," — a quiet tool that only speaks up when something's wrong is the right default.",[51,89,90],{},"Put the mapping in one pure function so it's trivial to unit-test.",[51,92,93,94,96,97,99],{},"Route logs to ",[14,95,36],{},"; never let verbosity touch ",[14,98,40],{},", which belongs to the result.",[51,101,102,103,105,106,108,109,112],{},"Reject ",[14,104,16],{}," and ",[14,107,24],{}," used together, and disable color when ",[14,110,111],{},"NO_COLOR"," is set or output isn't a TTY.",[43,114,116],{"id":115},"the-level-mapping-function","The level-mapping function",[10,118,119,120,122],{},"Keep the policy in one pure function that turns \"verbose count\" and \"quiet flag\" into a ",[14,121,32],{}," level. No I\u002FO, no globals — just arithmetic on the level constants, which makes it a joy to test:",[124,125,130],"pre",{"className":126,"code":127,"language":128,"meta":129,"style":129},"language-python shiki shiki-themes github-light github-dark","from __future__ import annotations\nimport logging\n\ndef resolve_level(verbose: int = 0, quiet: bool = False) -> int:\n    \"\"\"Map -v\u002F-vv and --quiet onto a logging level.\n\n    default -> WARNING, -v -> INFO, -vv -> DEBUG, --quiet -> ERROR.\n    \"\"\"\n    if quiet:\n        return logging.ERROR\n    return {\n        0: logging.WARNING,\n        1: logging.INFO,\n    }.get(verbose, logging.DEBUG)   # 2 or more -> DEBUG\n","python","",[14,131,132,152,161,168,209,216,221,227,233,242,254,263,277,289],{"__ignoreMap":129},[133,134,137,141,145,148],"span",{"class":135,"line":136},"line",1,[133,138,140],{"class":139},"szBVR","from",[133,142,144],{"class":143},"sj4cs"," __future__",[133,146,147],{"class":139}," import",[133,149,151],{"class":150},"sVt8B"," annotations\n",[133,153,155,158],{"class":135,"line":154},2,[133,156,157],{"class":139},"import",[133,159,160],{"class":150}," logging\n",[133,162,164],{"class":135,"line":163},3,[133,165,167],{"emptyLinePlaceholder":166},true,"\n",[133,169,171,174,178,181,184,187,190,193,196,198,201,204,206],{"class":135,"line":170},4,[133,172,173],{"class":139},"def",[133,175,177],{"class":176},"sScJk"," resolve_level",[133,179,180],{"class":150},"(verbose: ",[133,182,183],{"class":143},"int",[133,185,186],{"class":139}," =",[133,188,189],{"class":143}," 0",[133,191,192],{"class":150},", quiet: ",[133,194,195],{"class":143},"bool",[133,197,186],{"class":139},[133,199,200],{"class":143}," False",[133,202,203],{"class":150},") -> ",[133,205,183],{"class":143},[133,207,208],{"class":150},":\n",[133,210,212],{"class":135,"line":211},5,[133,213,215],{"class":214},"sZZnC","    \"\"\"Map -v\u002F-vv and --quiet onto a logging level.\n",[133,217,219],{"class":135,"line":218},6,[133,220,167],{"emptyLinePlaceholder":166},[133,222,224],{"class":135,"line":223},7,[133,225,226],{"class":214},"    default -> WARNING, -v -> INFO, -vv -> DEBUG, --quiet -> ERROR.\n",[133,228,230],{"class":135,"line":229},8,[133,231,232],{"class":214},"    \"\"\"\n",[133,234,236,239],{"class":135,"line":235},9,[133,237,238],{"class":139},"    if",[133,240,241],{"class":150}," quiet:\n",[133,243,245,248,251],{"class":135,"line":244},10,[133,246,247],{"class":139},"        return",[133,249,250],{"class":150}," logging.",[133,252,253],{"class":143},"ERROR\n",[133,255,257,260],{"class":135,"line":256},11,[133,258,259],{"class":139},"    return",[133,261,262],{"class":150}," {\n",[133,264,266,269,272,274],{"class":135,"line":265},12,[133,267,268],{"class":143},"        0",[133,270,271],{"class":150},": logging.",[133,273,59],{"class":143},[133,275,276],{"class":150},",\n",[133,278,280,283,285,287],{"class":135,"line":279},13,[133,281,282],{"class":143},"        1",[133,284,271],{"class":150},[133,286,66],{"class":143},[133,288,276],{"class":150},[133,290,292,295,297,300],{"class":135,"line":291},14,[133,293,294],{"class":150},"    }.get(verbose, logging.",[133,296,71],{"class":143},[133,298,299],{"class":150},")   ",[133,301,303],{"class":302},"sJ8bj","# 2 or more -> DEBUG\n",[10,305,306,307,309,310,312,313,315,316,318,319,321],{},"The default of ",[14,308,59],{}," is deliberate. A CLI that prints an ",[14,311,66],{}," line for every step is noisy out of the box; users learn to ignore it, which defeats the point. Start quiet, let ",[14,314,16],{}," opt into the narrative, and reserve the default channel for warnings and errors that actually need attention. ",[14,317,20],{}," bottoms out at ",[14,320,71],{},"; there's no level below it, so any higher count stays there.",[43,323,325],{"id":324},"wiring-it-into-a-click-callback","Wiring it into a Click callback",[10,327,328,329,332,333,336,337,340,341,345],{},"Click counts repeated flags with ",[14,330,331],{},"count=True",", so ",[14,334,335],{},"-vvv"," arrives as ",[14,338,339],{},"verbose=3",". Configure logging in the group callback ",[342,343,344],"em",{},"before"," any subcommand runs, so every command inherits the chosen level:",[124,347,349],{"className":126,"code":348,"language":128,"meta":129,"style":129},"import logging\nimport sys\nimport click\n\ndef setup_logging(level: int) -> None:\n    handler = logging.StreamHandler(sys.stderr)          # logs -> stderr\n    handler.setFormatter(logging.Formatter(\"%(levelname)s: %(message)s\"))\n    root = logging.getLogger()\n    root.handlers.clear()\n    root.addHandler(handler)\n    root.setLevel(level)\n\n@click.group()\n@click.option(\"-v\", \"--verbose\", count=True, help=\"Increase verbosity (-v, -vv).\")\n@click.option(\"-q\", \"--quiet\", is_flag=True, help=\"Only show errors.\")\n@click.pass_context\ndef cli(ctx: click.Context, verbose: int, quiet: bool) -> None:\n    if verbose and quiet:\n        raise click.UsageError(\"Pass either -v or --quiet, not both.\")\n    setup_logging(resolve_level(verbose, quiet))\n\n@cli.command()\ndef sync() -> None:\n    log = logging.getLogger(\"mycli.sync\")\n    log.debug(\"connecting\")          # shown only at -vv\n    log.info(\"syncing\")              # shown at -v and -vv\n    log.warning(\"slow response\")     # shown by default\n    click.echo(\"done\")               # result -> stdout, always\n",[14,350,351,357,364,371,375,394,408,430,440,445,450,455,459,467,507,542,548,571,584,598,604,609,617,632,648,663,678,693],{"__ignoreMap":129},[133,352,353,355],{"class":135,"line":136},[133,354,157],{"class":139},[133,356,160],{"class":150},[133,358,359,361],{"class":135,"line":154},[133,360,157],{"class":139},[133,362,363],{"class":150}," sys\n",[133,365,366,368],{"class":135,"line":163},[133,367,157],{"class":139},[133,369,370],{"class":150}," click\n",[133,372,373],{"class":135,"line":170},[133,374,167],{"emptyLinePlaceholder":166},[133,376,377,379,382,385,387,389,392],{"class":135,"line":211},[133,378,173],{"class":139},[133,380,381],{"class":176}," setup_logging",[133,383,384],{"class":150},"(level: ",[133,386,183],{"class":143},[133,388,203],{"class":150},[133,390,391],{"class":143},"None",[133,393,208],{"class":150},[133,395,396,399,402,405],{"class":135,"line":218},[133,397,398],{"class":150},"    handler ",[133,400,401],{"class":139},"=",[133,403,404],{"class":150}," logging.StreamHandler(sys.stderr)          ",[133,406,407],{"class":302},"# logs -> stderr\n",[133,409,410,413,416,419,422,425,427],{"class":135,"line":223},[133,411,412],{"class":150},"    handler.setFormatter(logging.Formatter(",[133,414,415],{"class":214},"\"",[133,417,418],{"class":143},"%(levelname)s",[133,420,421],{"class":214},": ",[133,423,424],{"class":143},"%(message)s",[133,426,415],{"class":214},[133,428,429],{"class":150},"))\n",[133,431,432,435,437],{"class":135,"line":229},[133,433,434],{"class":150},"    root ",[133,436,401],{"class":139},[133,438,439],{"class":150}," logging.getLogger()\n",[133,441,442],{"class":135,"line":235},[133,443,444],{"class":150},"    root.handlers.clear()\n",[133,446,447],{"class":135,"line":244},[133,448,449],{"class":150},"    root.addHandler(handler)\n",[133,451,452],{"class":135,"line":256},[133,453,454],{"class":150},"    root.setLevel(level)\n",[133,456,457],{"class":135,"line":265},[133,458,167],{"emptyLinePlaceholder":166},[133,460,461,464],{"class":135,"line":279},[133,462,463],{"class":176},"@click.group",[133,465,466],{"class":150},"()\n",[133,468,469,472,475,478,480,483,485,489,491,494,496,499,501,504],{"class":135,"line":291},[133,470,471],{"class":176},"@click.option",[133,473,474],{"class":150},"(",[133,476,477],{"class":214},"\"-v\"",[133,479,60],{"class":150},[133,481,482],{"class":214},"\"--verbose\"",[133,484,60],{"class":150},[133,486,488],{"class":487},"s4XuR","count",[133,490,401],{"class":139},[133,492,493],{"class":143},"True",[133,495,60],{"class":150},[133,497,498],{"class":487},"help",[133,500,401],{"class":139},[133,502,503],{"class":214},"\"Increase verbosity (-v, -vv).\"",[133,505,506],{"class":150},")\n",[133,508,510,512,514,517,519,522,524,527,529,531,533,535,537,540],{"class":135,"line":509},15,[133,511,471],{"class":176},[133,513,474],{"class":150},[133,515,516],{"class":214},"\"-q\"",[133,518,60],{"class":150},[133,520,521],{"class":214},"\"--quiet\"",[133,523,60],{"class":150},[133,525,526],{"class":487},"is_flag",[133,528,401],{"class":139},[133,530,493],{"class":143},[133,532,60],{"class":150},[133,534,498],{"class":487},[133,536,401],{"class":139},[133,538,539],{"class":214},"\"Only show errors.\"",[133,541,506],{"class":150},[133,543,545],{"class":135,"line":544},16,[133,546,547],{"class":176},"@click.pass_context\n",[133,549,551,553,556,559,561,563,565,567,569],{"class":135,"line":550},17,[133,552,173],{"class":139},[133,554,555],{"class":176}," cli",[133,557,558],{"class":150},"(ctx: click.Context, verbose: ",[133,560,183],{"class":143},[133,562,192],{"class":150},[133,564,195],{"class":143},[133,566,203],{"class":150},[133,568,391],{"class":143},[133,570,208],{"class":150},[133,572,574,576,579,582],{"class":135,"line":573},18,[133,575,238],{"class":139},[133,577,578],{"class":150}," verbose ",[133,580,581],{"class":139},"and",[133,583,241],{"class":150},[133,585,587,590,593,596],{"class":135,"line":586},19,[133,588,589],{"class":139},"        raise",[133,591,592],{"class":150}," click.UsageError(",[133,594,595],{"class":214},"\"Pass either -v or --quiet, not both.\"",[133,597,506],{"class":150},[133,599,601],{"class":135,"line":600},20,[133,602,603],{"class":150},"    setup_logging(resolve_level(verbose, quiet))\n",[133,605,607],{"class":135,"line":606},21,[133,608,167],{"emptyLinePlaceholder":166},[133,610,612,615],{"class":135,"line":611},22,[133,613,614],{"class":176},"@cli.command",[133,616,466],{"class":150},[133,618,620,622,625,628,630],{"class":135,"line":619},23,[133,621,173],{"class":139},[133,623,624],{"class":176}," sync",[133,626,627],{"class":150},"() -> ",[133,629,391],{"class":143},[133,631,208],{"class":150},[133,633,635,638,640,643,646],{"class":135,"line":634},24,[133,636,637],{"class":150},"    log ",[133,639,401],{"class":139},[133,641,642],{"class":150}," logging.getLogger(",[133,644,645],{"class":214},"\"mycli.sync\"",[133,647,506],{"class":150},[133,649,651,654,657,660],{"class":135,"line":650},25,[133,652,653],{"class":150},"    log.debug(",[133,655,656],{"class":214},"\"connecting\"",[133,658,659],{"class":150},")          ",[133,661,662],{"class":302},"# shown only at -vv\n",[133,664,666,669,672,675],{"class":135,"line":665},26,[133,667,668],{"class":150},"    log.info(",[133,670,671],{"class":214},"\"syncing\"",[133,673,674],{"class":150},")              ",[133,676,677],{"class":302},"# shown at -v and -vv\n",[133,679,681,684,687,690],{"class":135,"line":680},27,[133,682,683],{"class":150},"    log.warning(",[133,685,686],{"class":214},"\"slow response\"",[133,688,689],{"class":150},")     ",[133,691,692],{"class":302},"# shown by default\n",[133,694,696,699,702,705],{"class":135,"line":695},28,[133,697,698],{"class":150},"    click.echo(",[133,700,701],{"class":214},"\"done\"",[133,703,704],{"class":150},")               ",[133,706,707],{"class":302},"# result -> stdout, always\n",[124,709,713],{"className":710,"code":711,"language":712,"meta":129,"style":129},"language-bash shiki shiki-themes github-light github-dark","$ mycli sync                 # default: WARNING and up\nWARNING: slow response\ndone\n$ mycli -v sync              # INFO and up\nINFO: syncing\nWARNING: slow response\ndone\n$ mycli --quiet sync         # errors only\ndone\n","bash",[14,714,715,728,739,744,758,766,774,778,792],{"__ignoreMap":129},[133,716,717,720,723,725],{"class":135,"line":136},[133,718,719],{"class":176},"$",[133,721,722],{"class":214}," mycli",[133,724,624],{"class":214},[133,726,727],{"class":302},"                 # default: WARNING and up\n",[133,729,730,733,736],{"class":135,"line":154},[133,731,732],{"class":176},"WARNING:",[133,734,735],{"class":214}," slow",[133,737,738],{"class":214}," response\n",[133,740,741],{"class":135,"line":163},[133,742,743],{"class":139},"done\n",[133,745,746,748,750,753,755],{"class":135,"line":170},[133,747,719],{"class":176},[133,749,722],{"class":214},[133,751,752],{"class":143}," -v",[133,754,624],{"class":214},[133,756,757],{"class":302},"              # INFO and up\n",[133,759,760,763],{"class":135,"line":211},[133,761,762],{"class":176},"INFO:",[133,764,765],{"class":214}," syncing\n",[133,767,768,770,772],{"class":135,"line":218},[133,769,732],{"class":176},[133,771,735],{"class":214},[133,773,738],{"class":214},[133,775,776],{"class":135,"line":223},[133,777,743],{"class":139},[133,779,780,782,784,787,789],{"class":135,"line":229},[133,781,719],{"class":176},[133,783,722],{"class":214},[133,785,786],{"class":143}," --quiet",[133,788,624],{"class":214},[133,790,791],{"class":302},"         # errors only\n",[133,793,794],{"class":135,"line":235},[133,795,743],{"class":139},[10,797,798,799,802,803,806,807,78],{},"Setting the level once on the root logger means ",[14,800,801],{},"logging.getLogger(\"mycli.sync\")"," — and every other module logger — inherits it. You never thread a verbosity value into your business logic; you set the threshold and log unconditionally. This is the same single-",[14,804,805],{},"setup_logging"," discipline described in the ",[808,809,811],"a",{"href":810},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002F","structured logging overview",[43,813,815],{"id":814},"the-same-thing-in-typer-and-argparse","The same thing in Typer and argparse",[10,817,818,819,821,822,824],{},"Typer exposes the count with ",[14,820,331],{}," on an ",[14,823,183],{}," option and gives you a callback the same way:",[124,826,828],{"className":126,"code":827,"language":128,"meta":129,"style":129},"import logging\nimport typer\n\napp = typer.Typer()\n\n@app.callback()\ndef main(\n    verbose: int = typer.Option(0, \"-v\", \"--verbose\", count=True),\n    quiet: bool = typer.Option(False, \"-q\", \"--quiet\"),\n) -> None:\n    if verbose and quiet:\n        raise typer.BadParameter(\"Pass either -v or --quiet, not both.\")\n    setup_logging(resolve_level(verbose, quiet))\n",[14,829,830,836,843,847,857,861,868,878,912,936,944,954,965],{"__ignoreMap":129},[133,831,832,834],{"class":135,"line":136},[133,833,157],{"class":139},[133,835,160],{"class":150},[133,837,838,840],{"class":135,"line":154},[133,839,157],{"class":139},[133,841,842],{"class":150}," typer\n",[133,844,845],{"class":135,"line":163},[133,846,167],{"emptyLinePlaceholder":166},[133,848,849,852,854],{"class":135,"line":170},[133,850,851],{"class":150},"app ",[133,853,401],{"class":139},[133,855,856],{"class":150}," typer.Typer()\n",[133,858,859],{"class":135,"line":211},[133,860,167],{"emptyLinePlaceholder":166},[133,862,863,866],{"class":135,"line":218},[133,864,865],{"class":176},"@app.callback",[133,867,466],{"class":150},[133,869,870,872,875],{"class":135,"line":223},[133,871,173],{"class":139},[133,873,874],{"class":176}," main",[133,876,877],{"class":150},"(\n",[133,879,880,883,885,887,890,893,895,897,899,901,903,905,907,909],{"class":135,"line":229},[133,881,882],{"class":150},"    verbose: ",[133,884,183],{"class":143},[133,886,186],{"class":139},[133,888,889],{"class":150}," typer.Option(",[133,891,892],{"class":143},"0",[133,894,60],{"class":150},[133,896,477],{"class":214},[133,898,60],{"class":150},[133,900,482],{"class":214},[133,902,60],{"class":150},[133,904,488],{"class":487},[133,906,401],{"class":139},[133,908,493],{"class":143},[133,910,911],{"class":150},"),\n",[133,913,914,917,919,921,923,926,928,930,932,934],{"class":135,"line":235},[133,915,916],{"class":150},"    quiet: ",[133,918,195],{"class":143},[133,920,186],{"class":139},[133,922,889],{"class":150},[133,924,925],{"class":143},"False",[133,927,60],{"class":150},[133,929,516],{"class":214},[133,931,60],{"class":150},[133,933,521],{"class":214},[133,935,911],{"class":150},[133,937,938,940,942],{"class":135,"line":244},[133,939,203],{"class":150},[133,941,391],{"class":143},[133,943,208],{"class":150},[133,945,946,948,950,952],{"class":135,"line":256},[133,947,238],{"class":139},[133,949,578],{"class":150},[133,951,581],{"class":139},[133,953,241],{"class":150},[133,955,956,958,961,963],{"class":135,"line":265},[133,957,589],{"class":139},[133,959,960],{"class":150}," typer.BadParameter(",[133,962,595],{"class":214},[133,964,506],{"class":150},[133,966,967],{"class":135,"line":279},[133,968,603],{"class":150},[10,970,971,972,975,976,979,980,983],{},"With ",[14,973,974],{},"argparse",", count with ",[14,977,978],{},"action=\"count\""," and reject the combination with a mutually exclusive group so the parser rejects ",[14,981,982],{},"-v -q"," before your code runs:",[124,985,987],{"className":126,"code":986,"language":128,"meta":129,"style":129},"import argparse\n\nparser = argparse.ArgumentParser()\ngroup = parser.add_mutually_exclusive_group()\ngroup.add_argument(\"-v\", \"--verbose\", action=\"count\", default=0)\ngroup.add_argument(\"-q\", \"--quiet\", action=\"store_true\")\nargs = parser.parse_args()\nsetup_logging(resolve_level(args.verbose, args.quiet))\n",[14,988,989,996,1000,1010,1020,1052,1073,1083],{"__ignoreMap":129},[133,990,991,993],{"class":135,"line":136},[133,992,157],{"class":139},[133,994,995],{"class":150}," argparse\n",[133,997,998],{"class":135,"line":154},[133,999,167],{"emptyLinePlaceholder":166},[133,1001,1002,1005,1007],{"class":135,"line":163},[133,1003,1004],{"class":150},"parser ",[133,1006,401],{"class":139},[133,1008,1009],{"class":150}," argparse.ArgumentParser()\n",[133,1011,1012,1015,1017],{"class":135,"line":170},[133,1013,1014],{"class":150},"group ",[133,1016,401],{"class":139},[133,1018,1019],{"class":150}," parser.add_mutually_exclusive_group()\n",[133,1021,1022,1025,1027,1029,1031,1033,1036,1038,1041,1043,1046,1048,1050],{"class":135,"line":211},[133,1023,1024],{"class":150},"group.add_argument(",[133,1026,477],{"class":214},[133,1028,60],{"class":150},[133,1030,482],{"class":214},[133,1032,60],{"class":150},[133,1034,1035],{"class":487},"action",[133,1037,401],{"class":139},[133,1039,1040],{"class":214},"\"count\"",[133,1042,60],{"class":150},[133,1044,1045],{"class":487},"default",[133,1047,401],{"class":139},[133,1049,892],{"class":143},[133,1051,506],{"class":150},[133,1053,1054,1056,1058,1060,1062,1064,1066,1068,1071],{"class":135,"line":218},[133,1055,1024],{"class":150},[133,1057,516],{"class":214},[133,1059,60],{"class":150},[133,1061,521],{"class":214},[133,1063,60],{"class":150},[133,1065,1035],{"class":487},[133,1067,401],{"class":139},[133,1069,1070],{"class":214},"\"store_true\"",[133,1072,506],{"class":150},[133,1074,1075,1078,1080],{"class":135,"line":223},[133,1076,1077],{"class":150},"args ",[133,1079,401],{"class":139},[133,1081,1082],{"class":150}," parser.parse_args()\n",[133,1084,1085],{"class":135,"line":229},[133,1086,1087],{"class":150},"setup_logging(resolve_level(args.verbose, args.quiet))\n",[10,1089,1090,1093,1094,1097,1098,1102],{},[14,1091,1092],{},"add_mutually_exclusive_group()"," is the cleanest guard of the three: argparse emits its own usage error if both are passed, so you delete the hand-written ",[14,1095,1096],{},"if verbose and quiet"," check. If you're weighing these frameworks against each other, the ",[808,1099,1101],{"href":1100},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click comparison"," covers where each fits.",[43,1104,1106],{"id":1105},"keep-stdout-clean-and-respect-no_color","Keep stdout clean and respect NO_COLOR",[10,1108,1109,1110,1112,1113,1115,1116,1119],{},"Verbosity must never leak into ",[14,1111,40],{},". The whole reason to build this is so a user can crank ",[14,1114,20],{}," while debugging and still pipe the tool's real output somewhere. The handler above targets ",[14,1117,1118],{},"sys.stderr"," for exactly that reason:",[124,1121,1123],{"className":710,"code":1122,"language":712,"meta":129,"style":129},"$ mycli -vv export 2>debug.log | jq '.'   # DEBUG noise in debug.log, clean JSON to jq\n",[14,1124,1125],{"__ignoreMap":129},[133,1126,1127,1129,1131,1134,1137,1140,1143,1146,1149,1152],{"class":135,"line":136},[133,1128,719],{"class":176},[133,1130,722],{"class":214},[133,1132,1133],{"class":143}," -vv",[133,1135,1136],{"class":214}," export",[133,1138,1139],{"class":139}," 2>",[133,1141,1142],{"class":214},"debug.log",[133,1144,1145],{"class":139}," |",[133,1147,1148],{"class":176}," jq",[133,1150,1151],{"class":214}," '.'",[133,1153,1154],{"class":302},"   # DEBUG noise in debug.log, clean JSON to jq\n",[10,1156,1157,1158,1165,1166,1168],{},"Two more environment signals matter for well-behaved output. Honor ",[808,1159,1163],{"href":1160,"rel":1161},"https:\u002F\u002Fno-color.org\u002F",[1162],"nofollow",[14,1164,111],{}," — if it's set, don't colorize, period. And when ",[14,1167,36],{}," isn't a TTY (piped, CI, a log file), drop color and decoration so log files don't fill with escape sequences:",[124,1170,1172],{"className":126,"code":1171,"language":128,"meta":129,"style":129},"import os\nimport sys\n\ndef use_color() -> bool:\n    if os.environ.get(\"NO_COLOR\"):\n        return False\n    return sys.stderr.isatty()\n",[14,1173,1174,1181,1187,1191,1204,1217,1224],{"__ignoreMap":129},[133,1175,1176,1178],{"class":135,"line":136},[133,1177,157],{"class":139},[133,1179,1180],{"class":150}," os\n",[133,1182,1183,1185],{"class":135,"line":154},[133,1184,157],{"class":139},[133,1186,363],{"class":150},[133,1188,1189],{"class":135,"line":163},[133,1190,167],{"emptyLinePlaceholder":166},[133,1192,1193,1195,1198,1200,1202],{"class":135,"line":170},[133,1194,173],{"class":139},[133,1196,1197],{"class":176}," use_color",[133,1199,627],{"class":150},[133,1201,195],{"class":143},[133,1203,208],{"class":150},[133,1205,1206,1208,1211,1214],{"class":135,"line":211},[133,1207,238],{"class":139},[133,1209,1210],{"class":150}," os.environ.get(",[133,1212,1213],{"class":214},"\"NO_COLOR\"",[133,1215,1216],{"class":150},"):\n",[133,1218,1219,1221],{"class":135,"line":218},[133,1220,247],{"class":139},[133,1222,1223],{"class":143}," False\n",[133,1225,1226,1228],{"class":135,"line":223},[133,1227,259],{"class":139},[133,1229,1230],{"class":150}," sys.stderr.isatty()\n",[10,1232,1233,1234,1237,1238,1241,1242,1244,1245,1241,1248,1250,1251,1254,1255,78],{},"Feed ",[14,1235,1236],{},"use_color()"," into whichever handler you build — a plain ",[14,1239,1240],{},"Formatter"," when it's ",[14,1243,925],{},", or a ",[14,1246,1247],{},"RichHandler",[14,1249,493],{},". This is a rendering decision layered on top of the level; the two are independent, just like the ",[14,1252,1253],{},"--log-format"," switch in the ",[808,1256,1258],{"href":1257},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis\u002F","JSON logging guide",[43,1260,1262],{"id":1261},"also-accepting-an-explicit-log-level","Also accepting an explicit --log-level",[10,1264,1265,1266,1269,1270,25,1272,1274],{},"Counting flags is the ergonomic default, but power users and CI configs often want to name a level directly: ",[14,1267,1268],{},"--log-level DEBUG",". Offer both, and let the explicit name win when it's given, falling back to the ",[14,1271,16],{},[14,1273,24],{}," count otherwise:",[124,1276,1278],{"className":126,"code":1277,"language":128,"meta":129,"style":129},"import logging\nimport click\n\n@click.option(\"-v\", \"--verbose\", count=True)\n@click.option(\"-q\", \"--quiet\", is_flag=True)\n@click.option(\"--log-level\", type=click.Choice(\n    [\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\"], case_sensitive=False), default=None)\ndef cli(verbose: int, quiet: bool, log_level: str | None) -> None:\n    if log_level is not None:\n        level = getattr(logging, log_level.upper())\n    else:\n        level = resolve_level(verbose, quiet)\n    setup_logging(level)\n",[14,1279,1280,1286,1292,1296,1318,1340,1359,1403,1434,1451,1464,1471,1480],{"__ignoreMap":129},[133,1281,1282,1284],{"class":135,"line":136},[133,1283,157],{"class":139},[133,1285,160],{"class":150},[133,1287,1288,1290],{"class":135,"line":154},[133,1289,157],{"class":139},[133,1291,370],{"class":150},[133,1293,1294],{"class":135,"line":163},[133,1295,167],{"emptyLinePlaceholder":166},[133,1297,1298,1300,1302,1304,1306,1308,1310,1312,1314,1316],{"class":135,"line":170},[133,1299,471],{"class":176},[133,1301,474],{"class":150},[133,1303,477],{"class":214},[133,1305,60],{"class":150},[133,1307,482],{"class":214},[133,1309,60],{"class":150},[133,1311,488],{"class":487},[133,1313,401],{"class":139},[133,1315,493],{"class":143},[133,1317,506],{"class":150},[133,1319,1320,1322,1324,1326,1328,1330,1332,1334,1336,1338],{"class":135,"line":211},[133,1321,471],{"class":176},[133,1323,474],{"class":150},[133,1325,516],{"class":214},[133,1327,60],{"class":150},[133,1329,521],{"class":214},[133,1331,60],{"class":150},[133,1333,526],{"class":487},[133,1335,401],{"class":139},[133,1337,493],{"class":143},[133,1339,506],{"class":150},[133,1341,1342,1344,1346,1349,1351,1354,1356],{"class":135,"line":218},[133,1343,471],{"class":176},[133,1345,474],{"class":150},[133,1347,1348],{"class":214},"\"--log-level\"",[133,1350,60],{"class":150},[133,1352,1353],{"class":487},"type",[133,1355,401],{"class":139},[133,1357,1358],{"class":150},"click.Choice(\n",[133,1360,1361,1364,1367,1369,1372,1374,1377,1379,1382,1385,1388,1390,1392,1395,1397,1399,1401],{"class":135,"line":223},[133,1362,1363],{"class":150},"    [",[133,1365,1366],{"class":214},"\"DEBUG\"",[133,1368,60],{"class":150},[133,1370,1371],{"class":214},"\"INFO\"",[133,1373,60],{"class":150},[133,1375,1376],{"class":214},"\"WARNING\"",[133,1378,60],{"class":150},[133,1380,1381],{"class":214},"\"ERROR\"",[133,1383,1384],{"class":150},"], ",[133,1386,1387],{"class":487},"case_sensitive",[133,1389,401],{"class":139},[133,1391,925],{"class":143},[133,1393,1394],{"class":150},"), ",[133,1396,1045],{"class":487},[133,1398,401],{"class":139},[133,1400,391],{"class":143},[133,1402,506],{"class":150},[133,1404,1405,1407,1409,1411,1413,1415,1417,1420,1423,1425,1428,1430,1432],{"class":135,"line":229},[133,1406,173],{"class":139},[133,1408,555],{"class":176},[133,1410,180],{"class":150},[133,1412,183],{"class":143},[133,1414,192],{"class":150},[133,1416,195],{"class":143},[133,1418,1419],{"class":150},", log_level: ",[133,1421,1422],{"class":143},"str",[133,1424,1145],{"class":139},[133,1426,1427],{"class":143}," None",[133,1429,203],{"class":150},[133,1431,391],{"class":143},[133,1433,208],{"class":150},[133,1435,1436,1438,1441,1444,1447,1449],{"class":135,"line":235},[133,1437,238],{"class":139},[133,1439,1440],{"class":150}," log_level ",[133,1442,1443],{"class":139},"is",[133,1445,1446],{"class":139}," not",[133,1448,1427],{"class":143},[133,1450,208],{"class":150},[133,1452,1453,1456,1458,1461],{"class":135,"line":244},[133,1454,1455],{"class":150},"        level ",[133,1457,401],{"class":139},[133,1459,1460],{"class":143}," getattr",[133,1462,1463],{"class":150},"(logging, log_level.upper())\n",[133,1465,1466,1469],{"class":135,"line":256},[133,1467,1468],{"class":139},"    else",[133,1470,208],{"class":150},[133,1472,1473,1475,1477],{"class":135,"line":265},[133,1474,1455],{"class":150},[133,1476,401],{"class":139},[133,1478,1479],{"class":150}," resolve_level(verbose, quiet)\n",[133,1481,1482],{"class":135,"line":279},[133,1483,1484],{"class":150},"    setup_logging(level)\n",[10,1486,1487,1490,1491,1494,1495,1499,1500,1502],{},[14,1488,1489],{},"getattr(logging, \"DEBUG\")"," resolves the name to the numeric constant — the stdlib exposes each level as a module attribute, so you don't maintain a second lookup table. Keeping ",[14,1492,1493],{},"--log-level"," as an explicit override that beats the counted flags mirrors the general rule that a more specific input wins, the same principle behind ",[808,1496,1498],{"href":1497},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults\u002F","config precedence",". Don't offer three ways to say the same thing without deciding which wins — document that ",[14,1501,1493],{}," takes priority so a user passing both isn't surprised.",[43,1504,1506],{"id":1505},"testing-the-level-mapping","Testing the level mapping",[10,1508,1509,1510,1513],{},"Because ",[14,1511,1512],{},"resolve_level"," is pure, testing the whole policy is a one-liner per case. Parametrize the table and you've locked the behavior:",[124,1515,1517],{"className":126,"code":1516,"language":128,"meta":129,"style":129},"import logging\nimport pytest\nfrom myapp.logging import resolve_level\n\n@pytest.mark.parametrize(\"verbose, quiet, expected\", [\n    (0, False, logging.WARNING),\n    (1, False, logging.INFO),\n    (2, False, logging.DEBUG),\n    (3, False, logging.DEBUG),   # caps at DEBUG\n    (0, True,  logging.ERROR),   # quiet wins\n])\ndef test_resolve_level(verbose, quiet, expected):\n    assert resolve_level(verbose, quiet) == expected\n",[14,1518,1519,1525,1532,1544,1548,1561,1579,1596,1613,1634,1654,1659,1669],{"__ignoreMap":129},[133,1520,1521,1523],{"class":135,"line":136},[133,1522,157],{"class":139},[133,1524,160],{"class":150},[133,1526,1527,1529],{"class":135,"line":154},[133,1528,157],{"class":139},[133,1530,1531],{"class":150}," pytest\n",[133,1533,1534,1536,1539,1541],{"class":135,"line":163},[133,1535,140],{"class":139},[133,1537,1538],{"class":150}," myapp.logging ",[133,1540,157],{"class":139},[133,1542,1543],{"class":150}," resolve_level\n",[133,1545,1546],{"class":135,"line":170},[133,1547,167],{"emptyLinePlaceholder":166},[133,1549,1550,1553,1555,1558],{"class":135,"line":211},[133,1551,1552],{"class":176},"@pytest.mark.parametrize",[133,1554,474],{"class":150},[133,1556,1557],{"class":214},"\"verbose, quiet, expected\"",[133,1559,1560],{"class":150},", [\n",[133,1562,1563,1566,1568,1570,1572,1575,1577],{"class":135,"line":218},[133,1564,1565],{"class":150},"    (",[133,1567,892],{"class":143},[133,1569,60],{"class":150},[133,1571,925],{"class":143},[133,1573,1574],{"class":150},", logging.",[133,1576,59],{"class":143},[133,1578,911],{"class":150},[133,1580,1581,1583,1586,1588,1590,1592,1594],{"class":135,"line":223},[133,1582,1565],{"class":150},[133,1584,1585],{"class":143},"1",[133,1587,60],{"class":150},[133,1589,925],{"class":143},[133,1591,1574],{"class":150},[133,1593,66],{"class":143},[133,1595,911],{"class":150},[133,1597,1598,1600,1603,1605,1607,1609,1611],{"class":135,"line":229},[133,1599,1565],{"class":150},[133,1601,1602],{"class":143},"2",[133,1604,60],{"class":150},[133,1606,925],{"class":143},[133,1608,1574],{"class":150},[133,1610,71],{"class":143},[133,1612,911],{"class":150},[133,1614,1615,1617,1620,1622,1624,1626,1628,1631],{"class":135,"line":235},[133,1616,1565],{"class":150},[133,1618,1619],{"class":143},"3",[133,1621,60],{"class":150},[133,1623,925],{"class":143},[133,1625,1574],{"class":150},[133,1627,71],{"class":143},[133,1629,1630],{"class":150},"),   ",[133,1632,1633],{"class":302},"# caps at DEBUG\n",[133,1635,1636,1638,1640,1642,1644,1647,1649,1651],{"class":135,"line":244},[133,1637,1565],{"class":150},[133,1639,892],{"class":143},[133,1641,60],{"class":150},[133,1643,493],{"class":143},[133,1645,1646],{"class":150},",  logging.",[133,1648,77],{"class":143},[133,1650,1630],{"class":150},[133,1652,1653],{"class":302},"# quiet wins\n",[133,1655,1656],{"class":135,"line":256},[133,1657,1658],{"class":150},"])\n",[133,1660,1661,1663,1666],{"class":135,"line":265},[133,1662,173],{"class":139},[133,1664,1665],{"class":176}," test_resolve_level",[133,1667,1668],{"class":150},"(verbose, quiet, expected):\n",[133,1670,1671,1674,1677,1680],{"class":135,"line":279},[133,1672,1673],{"class":139},"    assert",[133,1675,1676],{"class":150}," resolve_level(verbose, quiet) ",[133,1678,1679],{"class":139},"==",[133,1681,1682],{"class":150}," expected\n",[10,1684,1685,1686,1689,1690,1693,1694,1697],{},"To test that a command actually emits at the resolved level, use pytest's ",[14,1687,1688],{},"caplog"," fixture: run the command under ",[14,1691,1692],{},"caplog.at_level(logging.DEBUG)"," and assert on ",[14,1695,1696],{},"caplog.records",". That checks the wiring end to end without scraping terminal text.",[43,1699,1701],{"id":1700},"production-notes","Production notes",[48,1703,1704,1722,1732,1750,1762],{},[51,1705,1706,1710,1711,1713,1714,1717,1718,1721],{},[1707,1708,1709],"strong",{},"Flags are one input among several."," Verbosity may also come from an env var or config file. Resolve it through the same precedence chain you use for everything else — see ",[808,1712,1498],{"href":1497}," so a ",[14,1715,1716],{},"--verbose"," flag beats a ",[14,1719,1720],{},"MYCLI_VERBOSE"," env var beats a config setting.",[51,1723,1724,1727,1728,1731],{},[1707,1725,1726],{},"Configure in the callback, not at import."," Set the level in the group\u002F",[14,1729,1730],{},"main"," callback so an importer of your package never has logging silently reconfigured underneath it.",[51,1733,1734,1739,1740,84,1742,1745,1746,78],{},[1707,1735,1736,1738],{},[14,1737,24],{}," should still show errors."," Map quiet to ",[14,1741,77],{},[14,1743,1744],{},"CRITICAL"," — a user who silenced a tool still needs to know when it failed, which ties into your ",[808,1747,1749],{"href":1748},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","exit-code strategy",[51,1751,1752,1758,1759,1761],{},[1707,1753,1754,1755,78],{},"Document the mapping in ",[14,1756,1757],{},"--help"," Spell out what each level shows so users aren't guessing whether ",[14,1760,16],{}," is enough.",[51,1763,1764,1767,1768,1770],{},[1707,1765,1766],{},"Idempotent setup."," Clear handlers before adding one so a second ",[14,1769,805],{}," call (tests, plugins) doesn't double every line.",[43,1772,1774],{"id":1773},"related","Related",[48,1776,1777,1783,1789,1795],{},[51,1778,1779,1780],{},"Up: ",[808,1781,1782],{"href":810},"Structured Logging for CLI Apps",[51,1784,1779,1785],{},[808,1786,1788],{"href":1787},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing for Python CLIs",[51,1790,1791,1792],{},"Sideways: ",[808,1793,1794],{"href":1257},"Structured JSON logging in Python CLIs",[51,1796,1791,1797],{},[808,1798,1799],{"href":1497},"Config precedence: flags, env, files, defaults",[1801,1802,1803],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .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":129,"searchDepth":154,"depth":154,"links":1805},[1806,1807,1808,1809,1810,1811,1812,1813,1814],{"id":45,"depth":154,"text":46},{"id":115,"depth":154,"text":116},{"id":324,"depth":154,"text":325},{"id":814,"depth":154,"text":815},{"id":1105,"depth":154,"text":1106},{"id":1261,"depth":154,"text":1262},{"id":1505,"depth":154,"text":1506},{"id":1700,"depth":154,"text":1701},{"id":1773,"depth":154,"text":1774},"2026-07-05","Map -v\u002F-vv and --quiet flags to Python logging levels in a CLI, set sane defaults, route logs to stderr, and keep stdout clean for pipes and scripts.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags",{"title":5,"description":1816},"advanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags\u002Findex",[32,1825,1826,1827],"cli","click","errors","Ler7nTenNR4fHQexsqkyRhQEmwcupgNadgJWI4Juc4o",[1830,1833,1836,1839,1842,1845,1848,1851,1854,1857,1859,1862,1865,1868,1871,1874,1875,1877,1880,1882,1885,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],{"path":1831,"title":1832},"\u002Fabout","About Python CLI Toolcraft",{"path":1834,"title":1835},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1837,"title":1838},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1840,"title":1841},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1843,"title":1844},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1846,"title":1847},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1849,"title":1850},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1852,"title":1853},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1855,"title":1856},"\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":1858,"title":1788},"\u002Fadvanced-input-parsing-user-experience",{"path":1860,"title":1861},"\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":1863,"title":1864},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1866,"title":1867},"\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":1869,"title":1870},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1872,"title":1873},"\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":1821,"title":5},{"path":1876,"title":1782},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps",{"path":1878,"title":1879},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":25,"title":1881},"Python CLI Toolcraft",{"path":1883,"title":1884},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1886,"title":1887},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1889,"title":1890},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1892,"title":1893},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1895,"title":1896},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1898,"title":1899},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1901,"title":1902},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1904,"title":1905},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1907,"title":1908},"\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":1910,"title":1911},"\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":1913,"title":1914},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1916,"title":1917},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1919,"title":1920},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":1922,"title":1923},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1925,"title":1926},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1928,"title":1929},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1931,"title":1932},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1934,"title":1935},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1937,"title":1938},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1940,"title":1941},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1943,"title":1944},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1946,"title":1947},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1949,"title":1950},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1952,"title":1953},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1955,"title":1956},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1958,"title":1959},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1961,"title":1962},"\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":1964,"title":1965},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1967,"title":1968},"\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":1970,"title":1971},"\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":1973,"title":1974},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1976,"title":1977},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867196]