[{"data":1,"prerenderedAt":2594},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests\u002F":3,"content-directory":1749},{"id":4,"title":5,"body":6,"date":1733,"description":1734,"difficulty":1735,"draft":1736,"extension":1737,"meta":1738,"navigation":159,"path":1739,"seo":1740,"stem":1741,"tags":1742,"updated":1733,"__hash__":1748},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests\u002Findex.md","Guarding CLI Startup with Import Tests",{"type":7,"value":8,"toc":1711},"minimark",[9,38,43,58,62,69,73,93,97,102,109,630,650,654,665,1212,1218,1225,1235,1238,1242,1245,1248,1260,1264,1267,1303,1307,1313,1563,1570,1574,1586,1590,1597,1600,1604,1622,1626,1633,1637,1653,1661,1673,1677,1707],[10,11,12,13,17,18,21,22,27,28,32,33,37],"p",{},"Startup time regresses one innocent line at a time. Someone adds ",[14,15,16],"code",{},"from rich.table import Table"," to the top of a command module to format a report, and every invocation of the CLI — including ",[14,19,20],{},"--help"," and shell completion — now loads Rich's table machinery whether or not it prints a table. A wall-clock budget test, as in ",[23,24,26],"a",{"href":25},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002F","profiling Python CLI startup time",", catches the big regressions, but timing tests are noisy on shared CI runners and say nothing about ",[29,30,31],"em",{},"what"," went wrong. A complementary, deterministic guard asks a simpler question: after importing the CLI's entry point, which modules are loaded? If anything on a forbidden list is among them, the test fails — every time, on every machine — and a small helper prints the exact chain of imports responsible. This guide builds both. It belongs to the ",[23,34,36],{"href":35},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","startup performance topic",".",[39,40,42],"h2",{"id":41},"prerequisites","Prerequisites",[44,45,46,55],"ul",{},[47,48,49,50,54],"li",{},"A CLI whose heavy dependencies are already imported lazily, as in ",[23,51,53],{"href":52},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup\u002F","lazy-loading subcommands for faster startup"," — this guide is about keeping it that way.",[47,56,57],{},"pytest.",[39,59,61],{"id":60},"what-to-guard","What to guard",[10,63,64,65,68],{},"The idea is to write down the modules that must ",[29,66,67],{},"not"," be imported just to start the CLI, with the reason for each:",[70,71],"inline-diagram",{"name":72},"importguard-forbidden",[10,74,75,76,79,80,79,83,86,87,89,90,92],{},"Good candidates are packages that are large, slow to import and needed by only some commands: data libraries (pandas, numpy, polars), cloud SDKs (boto3, google-cloud-*), HTTP clients when most commands work offline, template engines, YAML parsers, Rich's heavier modules (",[14,77,78],{},"rich.table",", ",[14,81,82],{},"rich.syntax",[14,84,85],{},"rich.markdown",") and Textual. Measuring a fresh interpreter is instructive: importing Typer itself takes around 25 ms and does ",[29,88,67],{}," import Rich — Typer loads it only when it renders help or an error — so a command module that imports ",[14,91,78],{}," at the top undoes that work for every run.",[39,94,96],{"id":95},"the-recipe","The recipe",[98,99,101],"h3",{"id":100},"a-deterministic-import-test","A deterministic import test",[10,103,104,105,108],{},"Run the import in a fresh interpreter — the test process has long since imported everything — and compare ",[14,106,107],{},"sys.modules"," against the forbidden list:",[110,111,116],"pre",{"className":112,"code":113,"language":114,"meta":115,"style":115},"language-python shiki shiki-themes github-light github-dark","# tests\u002Ftest_startup_imports.py\nimport json\nimport subprocess\nimport sys\n\nimport pytest\n\nHEAVY = {\n    \"pandas\": \"only the export command needs it\",\n    \"boto3\": \"only the s3 backend needs it\",\n    \"httpx\": \"most commands work offline\",\n    \"yaml\": \"only YAML output needs it\",\n}\nRICH = {\"rich.table\": \"import inside the commands that print tables\"}\n\n# What each startup path may not load. Rendering Typer's help legitimately loads Rich.\nENTRY_POINTS = {\n    \"import mytool.cli\": {**HEAVY, **RICH},\n    \"from mytool.cli import app; app(['--help'], standalone_mode=False)\": HEAVY,\n}\n\n\ndef loaded_modules(code: str) -> set[str]:\n    probe = f\"import json, sys\\n{code}\\nprint(json.dumps(sorted(sys.modules)))\"\n    result = subprocess.run([sys.executable, \"-c\", probe], capture_output=True, text=True,\n                            check=True)\n    return set(json.loads(result.stdout.splitlines()[-1]))\n\n\n@pytest.mark.parametrize(\"code\", ENTRY_POINTS)\ndef test_startup_does_not_import_heavy_modules(code):\n    loaded = loaded_modules(code)\n    offenders = {m: why for m, why in ENTRY_POINTS[code].items() if m in loaded}\n    assert not offenders, (\n        \"heavy modules imported at startup: \"\n        + \"; \".join(f\"{m} ({why})\" for m, why in offenders.items())\n        + \" — run scripts\u002Fimport_chain.py to see who imported them\"\n    )\n","python","",[14,117,118,127,138,146,154,161,169,174,187,203,216,229,242,248,269,274,280,290,313,325,330,335,340,364,390,427,440,461,466,471,489,500,511,548,560,566,616,624],{"__ignoreMap":115},[119,120,123],"span",{"class":121,"line":122},"line",1,[119,124,126],{"class":125},"sJ8bj","# tests\u002Ftest_startup_imports.py\n",[119,128,130,134],{"class":121,"line":129},2,[119,131,133],{"class":132},"szBVR","import",[119,135,137],{"class":136},"sVt8B"," json\n",[119,139,141,143],{"class":121,"line":140},3,[119,142,133],{"class":132},[119,144,145],{"class":136}," subprocess\n",[119,147,149,151],{"class":121,"line":148},4,[119,150,133],{"class":132},[119,152,153],{"class":136}," sys\n",[119,155,157],{"class":121,"line":156},5,[119,158,160],{"emptyLinePlaceholder":159},true,"\n",[119,162,164,166],{"class":121,"line":163},6,[119,165,133],{"class":132},[119,167,168],{"class":136}," pytest\n",[119,170,172],{"class":121,"line":171},7,[119,173,160],{"emptyLinePlaceholder":159},[119,175,177,181,184],{"class":121,"line":176},8,[119,178,180],{"class":179},"sj4cs","HEAVY",[119,182,183],{"class":132}," =",[119,185,186],{"class":136}," {\n",[119,188,190,194,197,200],{"class":121,"line":189},9,[119,191,193],{"class":192},"sZZnC","    \"pandas\"",[119,195,196],{"class":136},": ",[119,198,199],{"class":192},"\"only the export command needs it\"",[119,201,202],{"class":136},",\n",[119,204,206,209,211,214],{"class":121,"line":205},10,[119,207,208],{"class":192},"    \"boto3\"",[119,210,196],{"class":136},[119,212,213],{"class":192},"\"only the s3 backend needs it\"",[119,215,202],{"class":136},[119,217,219,222,224,227],{"class":121,"line":218},11,[119,220,221],{"class":192},"    \"httpx\"",[119,223,196],{"class":136},[119,225,226],{"class":192},"\"most commands work offline\"",[119,228,202],{"class":136},[119,230,232,235,237,240],{"class":121,"line":231},12,[119,233,234],{"class":192},"    \"yaml\"",[119,236,196],{"class":136},[119,238,239],{"class":192},"\"only YAML output needs it\"",[119,241,202],{"class":136},[119,243,245],{"class":121,"line":244},13,[119,246,247],{"class":136},"}\n",[119,249,251,254,256,259,262,264,267],{"class":121,"line":250},14,[119,252,253],{"class":179},"RICH",[119,255,183],{"class":132},[119,257,258],{"class":136}," {",[119,260,261],{"class":192},"\"rich.table\"",[119,263,196],{"class":136},[119,265,266],{"class":192},"\"import inside the commands that print tables\"",[119,268,247],{"class":136},[119,270,272],{"class":121,"line":271},15,[119,273,160],{"emptyLinePlaceholder":159},[119,275,277],{"class":121,"line":276},16,[119,278,279],{"class":125},"# What each startup path may not load. Rendering Typer's help legitimately loads Rich.\n",[119,281,283,286,288],{"class":121,"line":282},17,[119,284,285],{"class":179},"ENTRY_POINTS",[119,287,183],{"class":132},[119,289,186],{"class":136},[119,291,293,296,299,302,304,306,308,310],{"class":121,"line":292},18,[119,294,295],{"class":192},"    \"import mytool.cli\"",[119,297,298],{"class":136},": {",[119,300,301],{"class":132},"**",[119,303,180],{"class":179},[119,305,79],{"class":136},[119,307,301],{"class":132},[119,309,253],{"class":179},[119,311,312],{"class":136},"},\n",[119,314,316,319,321,323],{"class":121,"line":315},19,[119,317,318],{"class":192},"    \"from mytool.cli import app; app(['--help'], standalone_mode=False)\"",[119,320,196],{"class":136},[119,322,180],{"class":179},[119,324,202],{"class":136},[119,326,328],{"class":121,"line":327},20,[119,329,247],{"class":136},[119,331,333],{"class":121,"line":332},21,[119,334,160],{"emptyLinePlaceholder":159},[119,336,338],{"class":121,"line":337},22,[119,339,160],{"emptyLinePlaceholder":159},[119,341,343,346,350,353,356,359,361],{"class":121,"line":342},23,[119,344,345],{"class":132},"def",[119,347,349],{"class":348},"sScJk"," loaded_modules",[119,351,352],{"class":136},"(code: ",[119,354,355],{"class":179},"str",[119,357,358],{"class":136},") -> set[",[119,360,355],{"class":179},[119,362,363],{"class":136},"]:\n",[119,365,367,370,373,376,379,382,384,387],{"class":121,"line":366},24,[119,368,369],{"class":136},"    probe ",[119,371,372],{"class":132},"=",[119,374,375],{"class":132}," f",[119,377,378],{"class":192},"\"import json, sys",[119,380,381],{"class":179},"\\n{",[119,383,14],{"class":136},[119,385,386],{"class":179},"}\\n",[119,388,389],{"class":192},"print(json.dumps(sorted(sys.modules)))\"\n",[119,391,393,396,398,401,404,407,411,413,416,418,421,423,425],{"class":121,"line":392},25,[119,394,395],{"class":136},"    result ",[119,397,372],{"class":132},[119,399,400],{"class":136}," subprocess.run([sys.executable, ",[119,402,403],{"class":192},"\"-c\"",[119,405,406],{"class":136},", probe], ",[119,408,410],{"class":409},"s4XuR","capture_output",[119,412,372],{"class":132},[119,414,415],{"class":179},"True",[119,417,79],{"class":136},[119,419,420],{"class":409},"text",[119,422,372],{"class":132},[119,424,415],{"class":179},[119,426,202],{"class":136},[119,428,430,433,435,437],{"class":121,"line":429},26,[119,431,432],{"class":409},"                            check",[119,434,372],{"class":132},[119,436,415],{"class":179},[119,438,439],{"class":136},")\n",[119,441,443,446,449,452,455,458],{"class":121,"line":442},27,[119,444,445],{"class":132},"    return",[119,447,448],{"class":179}," set",[119,450,451],{"class":136},"(json.loads(result.stdout.splitlines()[",[119,453,454],{"class":132},"-",[119,456,457],{"class":179},"1",[119,459,460],{"class":136},"]))\n",[119,462,464],{"class":121,"line":463},28,[119,465,160],{"emptyLinePlaceholder":159},[119,467,469],{"class":121,"line":468},29,[119,470,160],{"emptyLinePlaceholder":159},[119,472,474,477,480,483,485,487],{"class":121,"line":473},30,[119,475,476],{"class":348},"@pytest.mark.parametrize",[119,478,479],{"class":136},"(",[119,481,482],{"class":192},"\"code\"",[119,484,79],{"class":136},[119,486,285],{"class":179},[119,488,439],{"class":136},[119,490,492,494,497],{"class":121,"line":491},31,[119,493,345],{"class":132},[119,495,496],{"class":348}," test_startup_does_not_import_heavy_modules",[119,498,499],{"class":136},"(code):\n",[119,501,503,506,508],{"class":121,"line":502},32,[119,504,505],{"class":136},"    loaded ",[119,507,372],{"class":132},[119,509,510],{"class":136}," loaded_modules(code)\n",[119,512,514,517,519,522,525,528,531,534,537,540,543,545],{"class":121,"line":513},33,[119,515,516],{"class":136},"    offenders ",[119,518,372],{"class":132},[119,520,521],{"class":136}," {m: why ",[119,523,524],{"class":132},"for",[119,526,527],{"class":136}," m, why ",[119,529,530],{"class":132},"in",[119,532,533],{"class":179}," ENTRY_POINTS",[119,535,536],{"class":136},"[code].items() ",[119,538,539],{"class":132},"if",[119,541,542],{"class":136}," m ",[119,544,530],{"class":132},[119,546,547],{"class":136}," loaded}\n",[119,549,551,554,557],{"class":121,"line":550},34,[119,552,553],{"class":132},"    assert",[119,555,556],{"class":132}," not",[119,558,559],{"class":136}," offenders, (\n",[119,561,563],{"class":121,"line":562},35,[119,564,565],{"class":192},"        \"heavy modules imported at startup: \"\n",[119,567,569,572,575,578,581,584,587,590,593,596,598,601,603,606,609,611,613],{"class":121,"line":568},36,[119,570,571],{"class":132},"        +",[119,573,574],{"class":192}," \"; \"",[119,576,577],{"class":136},".join(",[119,579,580],{"class":132},"f",[119,582,583],{"class":192},"\"",[119,585,586],{"class":179},"{",[119,588,589],{"class":136},"m",[119,591,592],{"class":179},"}",[119,594,595],{"class":192}," (",[119,597,586],{"class":179},[119,599,600],{"class":136},"why",[119,602,592],{"class":179},[119,604,605],{"class":192},")\"",[119,607,608],{"class":132}," for",[119,610,527],{"class":136},[119,612,530],{"class":132},[119,614,615],{"class":136}," offenders.items())\n",[119,617,619,621],{"class":121,"line":618},37,[119,620,571],{"class":132},[119,622,623],{"class":192}," \" — run scripts\u002Fimport_chain.py to see who imported them\"\n",[119,625,627],{"class":121,"line":626},38,[119,628,629],{"class":136},"    )\n",[10,631,632,633,635,636,638,639,641,642,645,646,649],{},"Testing two entry points matters: a plain import catches module-level imports, while invoking ",[14,634,20],{}," also exercises whatever the framework does to render help, including command registration code that runs at that point. The two paths get different lists for a reason found the hard way: the first version of this test forbade ",[14,637,78],{}," everywhere and failed on ",[14,640,20],{}," even after the offending command was fixed, because Typer renders its help panels ",[29,643,644],{},"with"," Rich tables. That is fine — Rich is only loaded when help is shown — so Rich modules are forbidden for the plain import and allowed for help, while the genuinely heavy packages are forbidden on both paths. ",[14,647,648],{},"standalone_mode=False"," keeps the help call from exiting the probe before it prints the module list; the last line of output is the JSON, so anything the help printed is ignored.",[98,651,653],{"id":652},"find-out-who-imported-it","Find out who imported it",[10,655,656,657,660,661,664],{},"When the test fails, the next question is always \"which import pulled this in?\" Python's ",[14,658,659],{},"-X importtime"," option prints every import as a tree, indented by depth, with children listed ",[29,662,663],{},"before"," their parents. Walking that output backwards from the forbidden module up to the shallower entries gives the chain:",[110,666,668],{"className":112,"code":667,"language":114,"meta":115,"style":115},"# scripts\u002Fimport_chain.py\n\"\"\"Show the chain of imports that loads TARGET when importing MODULE.\"\"\"\nfrom __future__ import annotations\n\nimport re\nimport subprocess\nimport sys\n\nLINE = re.compile(r\"import time:\\s+\\d+ \\|\\s+\\d+ \\|( *)(\\S+)\")\n\n\ndef import_chain(module: str, target: str, python: str = sys.executable) -> list[str]:\n    stderr = subprocess.run([python, \"-X\", \"importtime\", \"-c\", f\"import {module}\"],\n                            capture_output=True, text=True, check=True).stderr\n    entries = [(len(m.group(1)), m.group(2)) for m in map(LINE.match, stderr.splitlines()) if m]\n    hits = [i for i, (_, name) in enumerate(entries) if name == target] or \\\n           [i for i, (_, name) in enumerate(entries) if name.startswith(target + \".\")]\n    if not hits:\n        return []\n    depth, chain = entries[hits[0]][0], [entries[hits[0]][1]]\n    for d, name in entries[hits[0] + 1:]:\n        if d \u003C depth:                       # a shallower entry after us is our importer\n            chain.append(name)\n            depth = d\n    return chain[::-1]\n\n\nif __name__ == \"__main__\":\n    chain = import_chain(sys.argv[1], sys.argv[2])\n    print(\" -> \".join(chain) if chain else f\"{sys.argv[2]} is not imported\")\n",[14,669,670,675,680,694,698,705,711,717,721,782,786,790,821,862,891,939,979,1007,1017,1025,1055,1080,1097,1102,1112,1126,1130,1134,1150,1170],{"__ignoreMap":115},[119,671,672],{"class":121,"line":122},[119,673,674],{"class":125},"# scripts\u002Fimport_chain.py\n",[119,676,677],{"class":121,"line":129},[119,678,679],{"class":192},"\"\"\"Show the chain of imports that loads TARGET when importing MODULE.\"\"\"\n",[119,681,682,685,688,691],{"class":121,"line":140},[119,683,684],{"class":132},"from",[119,686,687],{"class":179}," __future__",[119,689,690],{"class":132}," import",[119,692,693],{"class":136}," annotations\n",[119,695,696],{"class":121,"line":148},[119,697,160],{"emptyLinePlaceholder":159},[119,699,700,702],{"class":121,"line":156},[119,701,133],{"class":132},[119,703,704],{"class":136}," re\n",[119,706,707,709],{"class":121,"line":163},[119,708,133],{"class":132},[119,710,145],{"class":136},[119,712,713,715],{"class":121,"line":171},[119,714,133],{"class":132},[119,716,153],{"class":136},[119,718,719],{"class":121,"line":176},[119,720,160],{"emptyLinePlaceholder":159},[119,722,723,726,728,731,734,736,740,743,746,749,751,755,757,759,761,763,765,767,770,773,775,778,780],{"class":121,"line":189},[119,724,725],{"class":179},"LINE",[119,727,183],{"class":132},[119,729,730],{"class":136}," re.compile(",[119,732,733],{"class":132},"r",[119,735,583],{"class":192},[119,737,739],{"class":738},"sA_wV","import time:",[119,741,742],{"class":179},"\\s",[119,744,745],{"class":132},"+",[119,747,748],{"class":179},"\\d",[119,750,745],{"class":132},[119,752,754],{"class":753},"snhLl"," \\|",[119,756,742],{"class":179},[119,758,745],{"class":132},[119,760,748],{"class":179},[119,762,745],{"class":132},[119,764,754],{"class":753},[119,766,479],{"class":179},[119,768,769],{"class":132}," *",[119,771,772],{"class":179},")(\\S",[119,774,745],{"class":132},[119,776,777],{"class":179},")",[119,779,583],{"class":192},[119,781,439],{"class":136},[119,783,784],{"class":121,"line":205},[119,785,160],{"emptyLinePlaceholder":159},[119,787,788],{"class":121,"line":218},[119,789,160],{"emptyLinePlaceholder":159},[119,791,792,794,797,800,802,805,807,810,812,814,817,819],{"class":121,"line":231},[119,793,345],{"class":132},[119,795,796],{"class":348}," import_chain",[119,798,799],{"class":136},"(module: ",[119,801,355],{"class":179},[119,803,804],{"class":136},", target: ",[119,806,355],{"class":179},[119,808,809],{"class":136},", python: ",[119,811,355],{"class":179},[119,813,183],{"class":132},[119,815,816],{"class":136}," sys.executable) -> list[",[119,818,355],{"class":179},[119,820,363],{"class":136},[119,822,823,826,828,831,834,836,839,841,843,845,847,850,852,855,857,859],{"class":121,"line":244},[119,824,825],{"class":136},"    stderr ",[119,827,372],{"class":132},[119,829,830],{"class":136}," subprocess.run([python, ",[119,832,833],{"class":192},"\"-X\"",[119,835,79],{"class":136},[119,837,838],{"class":192},"\"importtime\"",[119,840,79],{"class":136},[119,842,403],{"class":192},[119,844,79],{"class":136},[119,846,580],{"class":132},[119,848,849],{"class":192},"\"import ",[119,851,586],{"class":179},[119,853,854],{"class":136},"module",[119,856,592],{"class":179},[119,858,583],{"class":192},[119,860,861],{"class":136},"],\n",[119,863,864,867,869,871,873,875,877,879,881,884,886,888],{"class":121,"line":250},[119,865,866],{"class":409},"                            capture_output",[119,868,372],{"class":132},[119,870,415],{"class":179},[119,872,79],{"class":136},[119,874,420],{"class":409},[119,876,372],{"class":132},[119,878,415],{"class":179},[119,880,79],{"class":136},[119,882,883],{"class":409},"check",[119,885,372],{"class":132},[119,887,415],{"class":179},[119,889,890],{"class":136},").stderr\n",[119,892,893,896,898,901,904,907,909,912,915,918,920,922,924,927,929,931,934,936],{"class":121,"line":271},[119,894,895],{"class":136},"    entries ",[119,897,372],{"class":132},[119,899,900],{"class":136}," [(",[119,902,903],{"class":179},"len",[119,905,906],{"class":136},"(m.group(",[119,908,457],{"class":179},[119,910,911],{"class":136},")), m.group(",[119,913,914],{"class":179},"2",[119,916,917],{"class":136},")) ",[119,919,524],{"class":132},[119,921,542],{"class":136},[119,923,530],{"class":132},[119,925,926],{"class":179}," map",[119,928,479],{"class":136},[119,930,725],{"class":179},[119,932,933],{"class":136},".match, stderr.splitlines()) ",[119,935,539],{"class":132},[119,937,938],{"class":136}," m]\n",[119,940,941,944,946,949,951,954,956,959,962,964,967,970,973,976],{"class":121,"line":276},[119,942,943],{"class":136},"    hits ",[119,945,372],{"class":132},[119,947,948],{"class":136}," [i ",[119,950,524],{"class":132},[119,952,953],{"class":136}," i, (_, name) ",[119,955,530],{"class":132},[119,957,958],{"class":179}," enumerate",[119,960,961],{"class":136},"(entries) ",[119,963,539],{"class":132},[119,965,966],{"class":136}," name ",[119,968,969],{"class":132},"==",[119,971,972],{"class":136}," target] ",[119,974,975],{"class":132},"or",[119,977,978],{"class":136}," \\\n",[119,980,981,984,986,988,990,992,994,996,999,1001,1004],{"class":121,"line":282},[119,982,983],{"class":136},"           [i ",[119,985,524],{"class":132},[119,987,953],{"class":136},[119,989,530],{"class":132},[119,991,958],{"class":179},[119,993,961],{"class":136},[119,995,539],{"class":132},[119,997,998],{"class":136}," name.startswith(target ",[119,1000,745],{"class":132},[119,1002,1003],{"class":192}," \".\"",[119,1005,1006],{"class":136},")]\n",[119,1008,1009,1012,1014],{"class":121,"line":292},[119,1010,1011],{"class":132},"    if",[119,1013,556],{"class":132},[119,1015,1016],{"class":136}," hits:\n",[119,1018,1019,1022],{"class":121,"line":315},[119,1020,1021],{"class":132},"        return",[119,1023,1024],{"class":136}," []\n",[119,1026,1027,1030,1032,1035,1038,1041,1043,1046,1048,1050,1052],{"class":121,"line":327},[119,1028,1029],{"class":136},"    depth, chain ",[119,1031,372],{"class":132},[119,1033,1034],{"class":136}," entries[hits[",[119,1036,1037],{"class":179},"0",[119,1039,1040],{"class":136},"]][",[119,1042,1037],{"class":179},[119,1044,1045],{"class":136},"], [entries[hits[",[119,1047,1037],{"class":179},[119,1049,1040],{"class":136},[119,1051,457],{"class":179},[119,1053,1054],{"class":136},"]]\n",[119,1056,1057,1060,1063,1065,1067,1069,1072,1074,1077],{"class":121,"line":332},[119,1058,1059],{"class":132},"    for",[119,1061,1062],{"class":136}," d, name ",[119,1064,530],{"class":132},[119,1066,1034],{"class":136},[119,1068,1037],{"class":179},[119,1070,1071],{"class":136},"] ",[119,1073,745],{"class":132},[119,1075,1076],{"class":179}," 1",[119,1078,1079],{"class":136},":]:\n",[119,1081,1082,1085,1088,1091,1094],{"class":121,"line":337},[119,1083,1084],{"class":132},"        if",[119,1086,1087],{"class":136}," d ",[119,1089,1090],{"class":132},"\u003C",[119,1092,1093],{"class":136}," depth:                       ",[119,1095,1096],{"class":125},"# a shallower entry after us is our importer\n",[119,1098,1099],{"class":121,"line":342},[119,1100,1101],{"class":136},"            chain.append(name)\n",[119,1103,1104,1107,1109],{"class":121,"line":366},[119,1105,1106],{"class":136},"            depth ",[119,1108,372],{"class":132},[119,1110,1111],{"class":136}," d\n",[119,1113,1114,1116,1119,1121,1123],{"class":121,"line":392},[119,1115,445],{"class":132},[119,1117,1118],{"class":136}," chain[::",[119,1120,454],{"class":132},[119,1122,457],{"class":179},[119,1124,1125],{"class":136},"]\n",[119,1127,1128],{"class":121,"line":429},[119,1129,160],{"emptyLinePlaceholder":159},[119,1131,1132],{"class":121,"line":442},[119,1133,160],{"emptyLinePlaceholder":159},[119,1135,1136,1138,1141,1144,1147],{"class":121,"line":463},[119,1137,539],{"class":132},[119,1139,1140],{"class":179}," __name__",[119,1142,1143],{"class":132}," ==",[119,1145,1146],{"class":192}," \"__main__\"",[119,1148,1149],{"class":136},":\n",[119,1151,1152,1155,1157,1160,1162,1165,1167],{"class":121,"line":468},[119,1153,1154],{"class":136},"    chain ",[119,1156,372],{"class":132},[119,1158,1159],{"class":136}," import_chain(sys.argv[",[119,1161,457],{"class":179},[119,1163,1164],{"class":136},"], sys.argv[",[119,1166,914],{"class":179},[119,1168,1169],{"class":136},"])\n",[119,1171,1172,1175,1177,1180,1183,1185,1188,1191,1193,1195,1197,1200,1202,1205,1207,1210],{"class":121,"line":473},[119,1173,1174],{"class":179},"    print",[119,1176,479],{"class":136},[119,1178,1179],{"class":192},"\" -> \"",[119,1181,1182],{"class":136},".join(chain) ",[119,1184,539],{"class":132},[119,1186,1187],{"class":136}," chain ",[119,1189,1190],{"class":132},"else",[119,1192,375],{"class":132},[119,1194,583],{"class":192},[119,1196,586],{"class":179},[119,1198,1199],{"class":136},"sys.argv[",[119,1201,914],{"class":179},[119,1203,1204],{"class":136},"]",[119,1206,592],{"class":179},[119,1208,1209],{"class":192}," is not imported\"",[119,1211,439],{"class":136},[10,1213,1214,1215,1217],{},"Against a CLI whose report command imports ",[14,1216,78],{}," at the top:",[110,1219,1223],{"className":1220,"code":1222,"language":420,"meta":115},[1221],"language-text","$ python scripts\u002Fimport_chain.py mytool.cli rich.table\nmytool.cli -> mytool.commands.report -> rich.table\n",[14,1224,1222],{"__ignoreMap":115},[10,1226,1227,1228,1230,1231,1234],{},"The fix is then obvious and local: move ",[14,1229,16],{}," inside the function in ",[14,1232,1233],{},"mytool\u002Fcommands\u002Freport.py"," that builds the table.",[70,1236],{"name":1237},"importguard-terminal",[39,1239,1241],{"id":1240},"choosing-between-budgets-and-import-tests","Choosing between budgets and import tests",[10,1243,1244],{},"The two kinds of guard complement each other:",[70,1246],{"name":1247},"importguard-compare",[10,1249,1250,1251,1255,1256,1259],{},"A ",[1252,1253,1254],"strong",{},"time budget"," measures what users feel and catches slowness from any cause — a slow plugin discovery, an expensive computation at import — but it is noisy, needs generous margins on CI, and says nothing about the cause. An ",[1252,1257,1258],{},"import test"," is exact and explains itself, but only guards against the modules you thought to list. Use the import test as the everyday gate and keep a loose time budget as a backstop.",[39,1261,1263],{"id":1262},"ux-considerations","UX considerations",[10,1265,1266],{},"The users of this test are contributors adding features:",[44,1268,1269,1275,1287,1293],{},[47,1270,1271,1274],{},[1252,1272,1273],{},"Explain every entry."," The reason string appears in the failure message; \"only the export command needs it\" tells a contributor exactly where the import belongs.",[47,1276,1277,1280,1281,1284,1285,37],{},[1252,1278,1279],{},"Point at the tool."," The assertion message names ",[14,1282,1283],{},"scripts\u002Fimport_chain.py",", so nobody has to know about ",[14,1286,659],{},[47,1288,1289,1292],{},[1252,1290,1291],{},"Keep the list short and meaningful."," Five or ten genuinely heavy modules. A list of fifty standard-library modules turns the test into noise.",[47,1294,1295,1298,1299,1302],{},[1252,1296,1297],{},"Review additions to the list."," Adding a module to ",[14,1300,1301],{},"FORBIDDEN"," is a performance decision worth a sentence in the pull request.",[39,1304,1306],{"id":1305},"testing-the-behaviour","Testing the behaviour",[10,1308,1309,1310,1312],{},"The import-chain parser is pure string processing on real ",[14,1311,659],{}," output, so test it with a captured sample:",[110,1314,1316],{"className":112,"code":1315,"language":114,"meta":115,"style":115},"# tests\u002Ftest_import_chain.py\nfrom scripts import import_chain as ic\n\nSAMPLE = \"\"\"\\\nimport time: self [us] | cumulative | imported package\nimport time:        65 |         65 |   mytool\nimport time:        68 |         68 |   mytool.commands\nimport time:       164 |        259 |       rich\nimport time:      1022 |       8598 |     rich.table\nimport time:        66 |       9356 |   mytool.commands.report\nimport time:        94 |      31732 | mytool.cli\n\"\"\"\n\n\nclass FakeResult:\n    stderr = SAMPLE\n\n\ndef test_chain_walks_up_to_the_entry_module(monkeypatch):\n    monkeypatch.setattr(ic.subprocess, \"run\", lambda *a, **k: FakeResult())\n    assert ic.import_chain(\"mytool.cli\", \"rich\") == [\n        \"mytool.cli\", \"mytool.commands.report\", \"rich.table\", \"rich\"]\n\n\ndef test_missing_target_returns_empty(monkeypatch):\n    monkeypatch.setattr(ic.subprocess, \"run\", lambda *a, **k: FakeResult())\n    assert ic.import_chain(\"mytool.cli\", \"pandas\") == []\n",[14,1317,1318,1323,1341,1345,1358,1363,1368,1373,1378,1383,1388,1393,1398,1402,1406,1416,1425,1429,1433,1443,1466,1489,1509,1513,1517,1526,1544],{"__ignoreMap":115},[119,1319,1320],{"class":121,"line":122},[119,1321,1322],{"class":125},"# tests\u002Ftest_import_chain.py\n",[119,1324,1325,1327,1330,1332,1335,1338],{"class":121,"line":129},[119,1326,684],{"class":132},[119,1328,1329],{"class":136}," scripts ",[119,1331,133],{"class":132},[119,1333,1334],{"class":136}," import_chain ",[119,1336,1337],{"class":132},"as",[119,1339,1340],{"class":136}," ic\n",[119,1342,1343],{"class":121,"line":140},[119,1344,160],{"emptyLinePlaceholder":159},[119,1346,1347,1350,1352,1355],{"class":121,"line":148},[119,1348,1349],{"class":179},"SAMPLE",[119,1351,183],{"class":132},[119,1353,1354],{"class":192}," \"\"\"",[119,1356,1357],{"class":179},"\\\n",[119,1359,1360],{"class":121,"line":156},[119,1361,1362],{"class":192},"import time: self [us] | cumulative | imported package\n",[119,1364,1365],{"class":121,"line":163},[119,1366,1367],{"class":192},"import time:        65 |         65 |   mytool\n",[119,1369,1370],{"class":121,"line":171},[119,1371,1372],{"class":192},"import time:        68 |         68 |   mytool.commands\n",[119,1374,1375],{"class":121,"line":176},[119,1376,1377],{"class":192},"import time:       164 |        259 |       rich\n",[119,1379,1380],{"class":121,"line":189},[119,1381,1382],{"class":192},"import time:      1022 |       8598 |     rich.table\n",[119,1384,1385],{"class":121,"line":205},[119,1386,1387],{"class":192},"import time:        66 |       9356 |   mytool.commands.report\n",[119,1389,1390],{"class":121,"line":218},[119,1391,1392],{"class":192},"import time:        94 |      31732 | mytool.cli\n",[119,1394,1395],{"class":121,"line":231},[119,1396,1397],{"class":192},"\"\"\"\n",[119,1399,1400],{"class":121,"line":244},[119,1401,160],{"emptyLinePlaceholder":159},[119,1403,1404],{"class":121,"line":250},[119,1405,160],{"emptyLinePlaceholder":159},[119,1407,1408,1411,1414],{"class":121,"line":271},[119,1409,1410],{"class":132},"class",[119,1412,1413],{"class":348}," FakeResult",[119,1415,1149],{"class":136},[119,1417,1418,1420,1422],{"class":121,"line":276},[119,1419,825],{"class":136},[119,1421,372],{"class":132},[119,1423,1424],{"class":179}," SAMPLE\n",[119,1426,1427],{"class":121,"line":282},[119,1428,160],{"emptyLinePlaceholder":159},[119,1430,1431],{"class":121,"line":292},[119,1432,160],{"emptyLinePlaceholder":159},[119,1434,1435,1437,1440],{"class":121,"line":315},[119,1436,345],{"class":132},[119,1438,1439],{"class":348}," test_chain_walks_up_to_the_entry_module",[119,1441,1442],{"class":136},"(monkeypatch):\n",[119,1444,1445,1448,1451,1453,1456,1458,1461,1463],{"class":121,"line":327},[119,1446,1447],{"class":136},"    monkeypatch.setattr(ic.subprocess, ",[119,1449,1450],{"class":192},"\"run\"",[119,1452,79],{"class":136},[119,1454,1455],{"class":132},"lambda",[119,1457,769],{"class":132},[119,1459,1460],{"class":136},"a, ",[119,1462,301],{"class":132},[119,1464,1465],{"class":136},"k: FakeResult())\n",[119,1467,1468,1470,1473,1476,1478,1481,1484,1486],{"class":121,"line":332},[119,1469,553],{"class":132},[119,1471,1472],{"class":136}," ic.import_chain(",[119,1474,1475],{"class":192},"\"mytool.cli\"",[119,1477,79],{"class":136},[119,1479,1480],{"class":192},"\"rich\"",[119,1482,1483],{"class":136},") ",[119,1485,969],{"class":132},[119,1487,1488],{"class":136}," [\n",[119,1490,1491,1494,1496,1499,1501,1503,1505,1507],{"class":121,"line":337},[119,1492,1493],{"class":192},"        \"mytool.cli\"",[119,1495,79],{"class":136},[119,1497,1498],{"class":192},"\"mytool.commands.report\"",[119,1500,79],{"class":136},[119,1502,261],{"class":192},[119,1504,79],{"class":136},[119,1506,1480],{"class":192},[119,1508,1125],{"class":136},[119,1510,1511],{"class":121,"line":342},[119,1512,160],{"emptyLinePlaceholder":159},[119,1514,1515],{"class":121,"line":366},[119,1516,160],{"emptyLinePlaceholder":159},[119,1518,1519,1521,1524],{"class":121,"line":392},[119,1520,345],{"class":132},[119,1522,1523],{"class":348}," test_missing_target_returns_empty",[119,1525,1442],{"class":136},[119,1527,1528,1530,1532,1534,1536,1538,1540,1542],{"class":121,"line":429},[119,1529,1447],{"class":136},[119,1531,1450],{"class":192},[119,1533,79],{"class":136},[119,1535,1455],{"class":132},[119,1537,769],{"class":132},[119,1539,1460],{"class":136},[119,1541,301],{"class":132},[119,1543,1465],{"class":136},[119,1545,1546,1548,1550,1552,1554,1557,1559,1561],{"class":121,"line":442},[119,1547,553],{"class":132},[119,1549,1472],{"class":136},[119,1551,1475],{"class":192},[119,1553,79],{"class":136},[119,1555,1556],{"class":192},"\"pandas\"",[119,1558,1483],{"class":136},[119,1560,969],{"class":132},[119,1562,1024],{"class":136},[10,1564,1565,1566,1569],{},"Then see the real failure once: add a top-level ",[14,1567,1568],{},"import yaml"," to a command module, run the suite, and follow the message to the chain script.",[39,1571,1573],{"id":1572},"conclusion","Conclusion",[10,1575,1576,1577,1579,1580,1582,1583,1585],{},"A forbidden-imports test turns \"startup got slower\" into a precise, deterministic failure: import the CLI in a fresh interpreter (and render ",[14,1578,20],{},"), compare ",[14,1581,107],{}," with a short list of heavy modules and the reasons they are deferred, and point contributors at a script that prints the import chain from ",[14,1584,659],{},". Keep a loose time budget alongside it, and lazy imports stay lazy.",[39,1587,1589],{"id":1588},"frequently-asked-questions","Frequently asked questions",[98,1591,1593,1594,1596],{"id":1592},"why-not-just-check-sysmodules-inside-the-test-process","Why not just check ",[14,1595,107],{}," inside the test process?",[10,1598,1599],{},"Because pytest, plugins and other tests have already imported half the world by then. A fresh interpreter in a subprocess is the only reliable view of what the CLI itself loads.",[98,1601,1603],{"id":1602},"should-the-standard-library-be-on-the-list","Should the standard library be on the list?",[10,1605,1606,1607,79,1610,1613,1614,1617,1618,1621],{},"Rarely. Most standard-library modules are cheap. A few are worth deferring in very startup-sensitive tools — ",[14,1608,1609],{},"asyncio",[14,1611,1612],{},"email"," (pulled in by ",[14,1615,1616],{},"importlib.metadata"," in some versions), ",[14,1619,1620],{},"decimal"," — but measure before adding them.",[98,1623,1625],{"id":1624},"does-this-work-for-plugin-based-clis","Does this work for plugin-based CLIs?",[10,1627,1628,1629,37],{},"Yes, and it is especially useful there: installed plugins can import heavy modules at registration time. Run the test with your bundled plugins installed, and document the expectation for third-party plugin authors, as in ",[23,1630,1632],{"href":1631},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli\u002F","writing a plugin for an existing CLI",[98,1634,1636],{"id":1635},"how-does-this-relate-to-import-time-side-effects","How does this relate to import-time side effects?",[10,1638,1639,1640,1643,1644,1648,1649,1652],{},"They are siblings: this test checks what importing ",[29,1641,1642],{},"loads",", the audit-hook probe in ",[23,1645,1647],{"href":1646},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects\u002F","avoiding import-time side effects"," checks what importing ",[29,1650,1651],{},"does",". Run both in the same test module.",[98,1654,1656,1657,1660],{"id":1655},"should-version-and-completion-get-their-own-entries","Should ",[14,1658,1659],{},"--version"," and completion get their own entries?",[10,1662,1663,1664,1666,1667,1669,1670,1672],{},"If they have their own code paths, yes. ",[14,1665,1659],{}," should be the cheapest path of all — ideally nothing beyond the framework and ",[14,1668,1616],{}," — and shell completion runs on every Tab press, so its path deserves the strictest list. Add each as another key in ",[14,1671,285],{},", with the completion environment variables your framework uses set in the probe.",[39,1674,1676],{"id":1675},"related","Related",[44,1678,1679,1685,1690,1695,1701],{},[47,1680,1681,1682],{},"Up: ",[23,1683,1684],{"href":35},"CLI startup performance and lazy loading",[47,1686,1687],{},[23,1688,1689],{"href":25},"Profiling Python CLI startup time",[47,1691,1692],{},[23,1693,1694],{"href":1646},"Avoiding import-time side effects",[47,1696,1697],{},[23,1698,1700],{"href":1699},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002F","Reducing CLI dependency weight",[47,1702,1703],{},[23,1704,1706],{"href":1705},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase\u002F","Enforcing import boundaries in a CLI codebase",[1708,1709,1710],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":115,"searchDepth":129,"depth":129,"links":1712},[1713,1714,1715,1719,1720,1721,1722,1723,1732],{"id":41,"depth":129,"text":42},{"id":60,"depth":129,"text":61},{"id":95,"depth":129,"text":96,"children":1716},[1717,1718],{"id":100,"depth":140,"text":101},{"id":652,"depth":140,"text":653},{"id":1240,"depth":129,"text":1241},{"id":1262,"depth":129,"text":1263},{"id":1305,"depth":129,"text":1306},{"id":1572,"depth":129,"text":1573},{"id":1588,"depth":129,"text":1589,"children":1724},[1725,1727,1728,1729,1730],{"id":1592,"depth":140,"text":1726},"Why not just check sys.modules inside the test process?",{"id":1602,"depth":140,"text":1603},{"id":1624,"depth":140,"text":1625},{"id":1635,"depth":140,"text":1636},{"id":1655,"depth":140,"text":1731},"Should --version and completion get their own entries?",{"id":1675,"depth":129,"text":1676},"2026-10-02","Stop heavy modules creeping into a Python CLI’s startup path: a deterministic test of what importing the CLI loads, a forbidden-modules list, and a tool that shows the import chain.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests",{"title":5,"description":1734},"modern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests\u002Findex",[1743,1744,1745,1746,1747],"startup","imports","testing","performance","ci","dj5jzv645HfwxmApUXtaw5pHoznnNL9tzhyEPY3kDdA",[1750,1753,1756,1759,1762,1765,1768,1771,1774,1777,1780,1783,1786,1789,1792,1795,1798,1801,1804,1807,1810,1813,1816,1819,1822,1825,1828,1831,1834,1837,1840,1843,1846,1849,1852,1855,1858,1861,1864,1867,1870,1873,1876,1879,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,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,2131,2134,2137,2140,2143,2146,2149,2152,2155,2158,2161,2164,2167,2170,2173,2176,2179,2182,2185,2188,2191,2192,2195,2198,2201,2204,2207,2210,2213,2216,2219,2222,2225,2228,2231,2234,2237,2240,2243,2246,2249,2252,2255,2258,2261,2264,2267,2270,2273,2276,2279,2282,2285,2288,2291,2294,2297,2300,2303,2306,2309,2312,2315,2318,2321,2324,2327,2330,2333,2336,2339,2342,2345,2348,2351,2354,2357,2360,2363,2366,2369,2372,2375,2378,2381,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591],{"path":1751,"title":1752},"\u002Fabout","About Python CLI Toolcraft",{"path":1754,"title":1755},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1757,"title":1758},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1760,"title":1761},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":1763,"title":1764},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1766,"title":1767},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1769,"title":1770},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":1772,"title":1773},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1775,"title":1776},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":1778,"title":1779},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":1781,"title":1782},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":1784,"title":1785},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1787,"title":1788},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":1790,"title":1791},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":1793,"title":1794},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1796,"title":1797},"\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":1799,"title":1800},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":1802,"title":1803},"\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":1805,"title":1806},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1808,"title":1809},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":1811,"title":1812},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1814,"title":1815},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1817,"title":1818},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1820,"title":1821},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1823,"title":1824},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":1826,"title":1827},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":1829,"title":1830},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1832,"title":1833},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1835,"title":1836},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":1838,"title":1839},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1841,"title":1842},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":1844,"title":1845},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":1847,"title":1848},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1850,"title":1851},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1853,"title":1854},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1856,"title":1857},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":1859,"title":1860},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":1862,"title":1863},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1865,"title":1866},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":1868,"title":1869},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1871,"title":1872},"\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":1874,"title":1875},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1877,"title":1878},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1880,"title":1881},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":1883,"title":1884},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":1886,"title":1887},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1889,"title":1890},"\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":1892,"title":1893},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1895,"title":1896},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":1898,"title":1899},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1901,"title":1902},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1904,"title":1905},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":1907,"title":1908},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1910,"title":1911},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1913,"title":1914},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":1916,"title":1917},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":1919,"title":1920},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":1922,"title":1923},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":1925,"title":1926},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":1928,"title":1929},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":1931,"title":1932},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":1934,"title":1935},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":1937,"title":1938},"\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":1940,"title":1941},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1943,"title":1944},"\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":1946,"title":1947},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":1949,"title":1950},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1952,"title":1953},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":1955,"title":1956},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1958,"title":1959},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1961,"title":1962},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":1964,"title":1965},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":1967,"title":1968},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1970,"title":1971},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":1973,"title":1974},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1976,"title":1977},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1979,"title":1980},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1982,"title":1983},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1985,"title":1986},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":1988,"title":1989},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":1991,"title":1992},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1994,"title":1995},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":1997,"title":1998},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2000,"title":2001},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2003,"title":2004},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":2006,"title":2007},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2009,"title":2010},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2012,"title":2013},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2015,"title":2016},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":2018,"title":2019},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2021,"title":2022},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2024,"title":2025},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2027,"title":2028},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2030,"title":2031},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2033,"title":2034},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2036,"title":2037},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":2039,"title":2040},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2042,"title":2043},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2045,"title":2046},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2048,"title":2049},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":2051,"title":2052},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2054,"title":2055},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2057,"title":2058},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2060,"title":2061},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":2063,"title":2064},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2066,"title":2067},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2069,"title":2070},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":2072,"title":2073},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2075,"title":2076},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":2078,"title":2079},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":2081,"title":2082},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":2084,"title":2085},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2087,"title":2088},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2090,"title":2091},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2093,"title":2094},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2096,"title":2097},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2099,"title":2100},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":2102,"title":2103},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2105,"title":2106},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":2108,"title":2109},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2111,"title":2112},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2114,"title":2115},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2117,"title":2118},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2120,"title":2121},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":2123,"title":2124},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2126,"title":2127},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2129,"title":2130},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2132,"title":2133},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2135,"title":2136},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2138,"title":2139},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2141,"title":2142},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2144,"title":2145},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2147,"title":2148},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2150,"title":2151},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2153,"title":2154},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":2156,"title":2157},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2159,"title":2160},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":2162,"title":2163},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":2165,"title":2166},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":2168,"title":2169},"\u002F","Python CLI Toolcraft",{"path":2171,"title":2172},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2174,"title":2175},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2177,"title":2178},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2180,"title":2181},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2183,"title":2184},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":2186,"title":2187},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":2189,"title":2190},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":1739,"title":5},{"path":2193,"title":2194},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2196,"title":2197},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2199,"title":2200},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2202,"title":2203},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2205,"title":2206},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2208,"title":2209},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2211,"title":2212},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":2214,"title":2215},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2217,"title":2218},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2220,"title":2221},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2223,"title":2224},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":2226,"title":2227},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2229,"title":2230},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2232,"title":2233},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2235,"title":2236},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2238,"title":2239},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2241,"title":2242},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2244,"title":2245},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2247,"title":2248},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2250,"title":2251},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2253,"title":2254},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2256,"title":2257},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2259,"title":2260},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2262,"title":2263},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2265,"title":2266},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2268,"title":2269},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2271,"title":2272},"\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":2274,"title":2275},"\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":2277,"title":2278},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2280,"title":2281},"\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":2283,"title":2284},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2286,"title":2287},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":2289,"title":2290},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2292,"title":2293},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2295,"title":2296},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2298,"title":2299},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2301,"title":2302},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2304,"title":2305},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2307,"title":2308},"\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":2310,"title":2311},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2313,"title":2314},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2316,"title":2317},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2319,"title":2320},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2322,"title":2323},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2325,"title":2326},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2328,"title":2329},"\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":2331,"title":2332},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2334,"title":2335},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":2337,"title":2338},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":2340,"title":2341},"\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":2343,"title":2344},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2346,"title":2347},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":2349,"title":2350},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2352,"title":2353},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2355,"title":2356},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2358,"title":2359},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2361,"title":2362},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2364,"title":2365},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":2367,"title":2368},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2370,"title":2371},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":2373,"title":2374},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2376,"title":2377},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2379,"title":2380},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2382,"title":2383},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2385,"title":2386},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2388,"title":2389},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2391,"title":2392},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":2394,"title":2395},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":2397,"title":2398},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":2400,"title":2401},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2403,"title":2404},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2406,"title":2407},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":2409,"title":2410},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2412,"title":2413},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2415,"title":2416},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2418,"title":2419},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":2421,"title":2422},"\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":2424,"title":2425},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2427,"title":2428},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2430,"title":2431},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2433,"title":2434},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":2436,"title":2437},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2439,"title":2440},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":2442,"title":2443},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":2445,"title":2446},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2448,"title":2449},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2451,"title":2452},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":2454,"title":2455},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2457,"title":2458},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2460,"title":2461},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2463,"title":2464},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2466,"title":2467},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":2469,"title":2470},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2472,"title":2473},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2475,"title":2476},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":2478,"title":2479},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2481,"title":2482},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2484,"title":2485},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":2487,"title":2488},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2490,"title":2491},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2493,"title":2494},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":2496,"title":2497},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2499,"title":2500},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":2502,"title":2503},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2505,"title":2506},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2508,"title":2509},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2511,"title":2512},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":2514,"title":2515},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2517,"title":2518},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":2520,"title":2521},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":2523,"title":2524},"\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":2526,"title":2527},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2529,"title":2530},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":2532,"title":2533},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2535,"title":2536},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":2538,"title":2539},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":2541,"title":2542},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":2544,"title":2545},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":2547,"title":2548},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":2550,"title":2551},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":2553,"title":2554},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2556,"title":2557},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":2559,"title":2560},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2562,"title":2563},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":2565,"title":2566},"\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":2568,"title":2569},"\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":2571,"title":2572},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2574,"title":2575},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":2577,"title":2578},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":2580,"title":2581},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2583,"title":2584},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":2586,"title":2587},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2589,"title":2590},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2592,"title":2593},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967540210]