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