[{"data":1,"prerenderedAt":1252},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002F":3,"content-directory":1103},{"id":4,"title":5,"body":6,"date":1088,"description":1089,"difficulty":1090,"draft":1091,"extension":1092,"meta":1093,"navigation":667,"path":1094,"seo":1095,"stem":1096,"tags":1097,"updated":1088,"__hash__":1102},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002Findex.md","Profiling Python CLI Startup Time",{"type":7,"value":8,"toc":1077},"minimark",[9,26,31,97,101,107,149,156,164,189,199,202,260,264,274,326,341,345,356,439,453,474,484,488,505,526,548,552,555,609,621,625,628,964,977,981,1043,1047,1073],[10,11,12,13,17,18,21,22,25],"p",{},"Before you speed up a slow CLI, you have to know where the time actually goes — and for almost every Python CLI, the answer is imports, not your code. This guide shows you how to profile startup precisely: find the expensive imports with ",[14,15,16],"code",{},"python -X importtime",", visualize the tree with ",[14,19,20],{},"tuna",", measure real wall-clock time with ",[14,23,24],{},"hyperfine",", and lock in your gains with a startup budget you enforce in CI.",[27,28,30],"h2",{"id":29},"tldr","TL;DR",[32,33,34,47,55,64,86],"ul",{},[35,36,37,38,41,42,46],"li",{},"Run ",[14,39,40],{},"python -X importtime -c \"import yourcli\""," and read the ",[43,44,45],"strong",{},"cumulative"," column — the biggest numbers at the top of the tree are your targets.",[35,48,49,50,54],{},"Visualize the same data with ",[43,51,52],{},[14,53,20],{}," to see the import waterfall as a flame graph.",[35,56,57,58,63],{},"Measure end-to-end wall-clock time of the real command with ",[43,59,60],{},[14,61,62],{},"hyperfine 'yourcli --help'"," — that's what users feel.",[35,65,66,67,70,71,70,74,77,78,81,82,85],{},"The usual offenders are ",[14,68,69],{},"requests",", ",[14,72,73],{},"pandas",[14,75,76],{},"pydantic",", and cloud SDKs; interpreter ",[14,79,80],{},"site"," setup adds a fixed floor you can inspect with ",[14,83,84],{},"-S",".",[35,87,88,89,92,93,96],{},"Encode a ",[43,90,91],{},"budget"," as a pytest that asserts ",[14,94,95],{},"--help"," runs under N milliseconds, so a heavy import added later fails CI instead of taxing every user.",[27,98,100],{"id":99},"find-expensive-imports-with-x-importtime","Find expensive imports with -X importtime",[10,102,103,106],{},[14,104,105],{},"-X importtime"," is a built-in interpreter flag that logs every import and how long it took. Point it at importing your CLI's top module — that import is what runs on startup:",[108,109,114],"pre",{"className":110,"code":111,"language":112,"meta":113,"style":113},"language-bash shiki shiki-themes github-light github-dark","$ python -X importtime -c \"import yourcli.cli\" 2> importtime.log\n","bash","",[14,115,116],{"__ignoreMap":113},[117,118,121,125,129,133,136,139,142,146],"span",{"class":119,"line":120},"line",1,[117,122,124],{"class":123},"sScJk","$",[117,126,128],{"class":127},"sZZnC"," python",[117,130,132],{"class":131},"sj4cs"," -X",[117,134,135],{"class":127}," importtime",[117,137,138],{"class":131}," -c",[117,140,141],{"class":127}," \"import yourcli.cli\"",[117,143,145],{"class":144},"szBVR"," 2>",[117,147,148],{"class":127}," importtime.log\n",[10,150,151,152,155],{},"The output goes to stderr (hence ",[14,153,154],{},"2>","), one line per import, with three columns:",[108,157,162],{"className":158,"code":160,"language":161,"meta":113},[159],"language-text","import time:      self [us] | cumulative | imported package\nimport time:       125 |        125 |   collections.abc\nimport time:      2140 |      48210 |     pandas\nimport time:       310 |      51900 |   yourcli.commands.convert\nimport time:       180 |      54120 | yourcli.cli\n","text",[14,163,160],{"__ignoreMap":113},[32,165,166,177,186],{},[35,167,168,171,172,176],{},[43,169,170],{},"self"," — time spent importing ",[173,174,175],"em",{},"just"," that module, excluding its children.",[35,178,179,181,182,185],{},[43,180,45],{}," — time for that module ",[173,183,184],{},"and everything it imports",". This is the column that matters.",[35,187,188],{},"Indentation shows the import tree; a deeply nested module was pulled in by its less-indented parent.",[10,190,191,192,194,195,198],{},"Read it top-down by cumulative cost. In the sample above, ",[14,193,73],{}," at 48 ms cumulative is dragged in by ",[14,196,197],{},"yourcli.commands.convert"," — so the fix is to stop importing that command module at startup. You are hunting for a single parent import with a large cumulative number; deferring that one import removes its whole subtree from the startup path.",[10,200,201],{},"A useful one-liner to surface the worst offenders sorts the log by cumulative time:",[108,203,205],{"className":110,"code":204,"language":112,"meta":113,"style":113},"$ python -X importtime -c \"import yourcli.cli\" 2>&1 \\\n    | sort -t'|' -k2 -n -r | head -15\n",[14,206,207,227],{"__ignoreMap":113},[117,208,209,211,213,215,217,219,221,224],{"class":119,"line":120},[117,210,124],{"class":123},[117,212,128],{"class":127},[117,214,132],{"class":131},[117,216,135],{"class":127},[117,218,138],{"class":131},[117,220,141],{"class":127},[117,222,223],{"class":144}," 2>&1",[117,225,226],{"class":131}," \\\n",[117,228,230,233,236,239,242,245,248,251,254,257],{"class":119,"line":229},2,[117,231,232],{"class":144},"    |",[117,234,235],{"class":123}," sort",[117,237,238],{"class":131}," -t",[117,240,241],{"class":127},"'|'",[117,243,244],{"class":131}," -k2",[117,246,247],{"class":131}," -n",[117,249,250],{"class":131}," -r",[117,252,253],{"class":144}," |",[117,255,256],{"class":123}," head",[117,258,259],{"class":131}," -15\n",[27,261,263],{"id":262},"visualize-the-tree-with-tuna","Visualize the tree with tuna",[10,265,266,267,270,271,273],{},"Raw ",[14,268,269],{},"importtime"," output gets unwieldy for a large app. ",[14,272,20],{}," turns it into an interactive flame graph in your browser — the wide bars are the expensive subtrees, and you can click to zoom:",[108,275,277],{"className":110,"code":276,"language":112,"meta":113,"style":113},"$ uv tool install tuna          # or: pipx install tuna\n$ python -X importtime -c \"import yourcli.cli\" 2> importtime.log\n$ tuna importtime.log\n",[14,278,279,299,317],{"__ignoreMap":113},[117,280,281,283,286,289,292,295],{"class":119,"line":120},[117,282,124],{"class":123},[117,284,285],{"class":127}," uv",[117,287,288],{"class":127}," tool",[117,290,291],{"class":127}," install",[117,293,294],{"class":127}," tuna",[117,296,298],{"class":297},"sJ8bj","          # or: pipx install tuna\n",[117,300,301,303,305,307,309,311,313,315],{"class":119,"line":229},[117,302,124],{"class":123},[117,304,128],{"class":127},[117,306,132],{"class":131},[117,308,135],{"class":127},[117,310,138],{"class":131},[117,312,141],{"class":127},[117,314,145],{"class":144},[117,316,148],{"class":127},[117,318,320,322,324],{"class":119,"line":319},3,[117,321,124],{"class":123},[117,323,294],{"class":127},[117,325,148],{"class":127},[10,327,328,329,331,332,335,336,85],{},"The graphical view makes it obvious when one dependency dominates — a single wide block for ",[14,330,73],{}," or ",[14,333,334],{},"boto3"," next to a sea of thin standard-library imports tells you exactly what to defer. Installing tuna as an isolated tool keeps it out of your project's environment; if you're weighing how to install these dev tools, see ",[337,338,340],"a",{"href":339},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis\u002F","uv tool install vs pipx for CLIs",[27,342,344],{"id":343},"measure-real-wall-clock-time-with-hyperfine","Measure real wall-clock time with hyperfine",[10,346,347,349,350,352,353,355],{},[14,348,269],{}," measures imports; it does not measure the full user-visible latency, which also includes interpreter start and ",[14,351,80],{}," initialization. For the number users actually feel, benchmark the real command with ",[14,354,24],{},", which runs it many times and reports mean, standard deviation, and warmup behavior:",[108,357,359],{"className":110,"code":358,"language":112,"meta":113,"style":113},"$ hyperfine --warmup 3 'yourcli --help'\nBenchmark 1: yourcli --help\n  Time (mean ± σ):     412.6 ms ±   9.1 ms    [User: 380.2 ms, System: 41.7 ms]\n  Range (min … max):   401.3 ms … 428.9 ms    10 runs\n",[14,360,361,377,391,421],{"__ignoreMap":113},[117,362,363,365,368,371,374],{"class":119,"line":120},[117,364,124],{"class":123},[117,366,367],{"class":127}," hyperfine",[117,369,370],{"class":131}," --warmup",[117,372,373],{"class":131}," 3",[117,375,376],{"class":127}," 'yourcli --help'\n",[117,378,379,382,385,388],{"class":119,"line":229},[117,380,381],{"class":123},"Benchmark",[117,383,384],{"class":127}," 1:",[117,386,387],{"class":127}," yourcli",[117,389,390],{"class":131}," --help\n",[117,392,393,396,400,403,406,409,412,415,418],{"class":119,"line":319},[117,394,395],{"class":123},"  Time",[117,397,399],{"class":398},"sVt8B"," (mean ",[117,401,402],{"class":127},"±",[117,404,405],{"class":127}," σ",[117,407,408],{"class":398},"):     412.6 ms ±   9.1 ms    [User: ",[117,410,411],{"class":131},"380.2",[117,413,414],{"class":398}," ms, System: ",[117,416,417],{"class":131},"41.7",[117,419,420],{"class":398}," ms]\n",[117,422,424,427,430,433,436],{"class":119,"line":423},4,[117,425,426],{"class":123},"  Range",[117,428,429],{"class":398}," (min ",[117,431,432],{"class":127},"…",[117,434,435],{"class":127}," max",[117,437,438],{"class":398},"):   401.3 ms … 428.9 ms    10 runs\n",[10,440,441,442,445,446,449,450,452],{},"The ",[14,443,444],{},"--warmup 3"," runs discard the first few executions so filesystem and bytecode caches are warm — otherwise the first run's ",[14,447,448],{},".pyc"," compilation skews the mean. Use ",[14,451,24],{}," to compare two versions directly:",[108,454,456],{"className":110,"code":455,"language":112,"meta":113,"style":113},"$ hyperfine --warmup 3 'yourcli-eager --help' 'yourcli-lazy --help'\n",[14,457,458],{"__ignoreMap":113},[117,459,460,462,464,466,468,471],{"class":119,"line":120},[117,461,124],{"class":123},[117,463,367],{"class":127},[117,465,370],{"class":131},[117,467,373],{"class":131},[117,469,470],{"class":127}," 'yourcli-eager --help'",[117,472,473],{"class":127}," 'yourcli-lazy --help'\n",[10,475,476,477,479,480,483],{},"It prints a \"N times faster\" ratio, which is the single most convincing before\u002Fafter number to put in a pull request. This end-to-end measurement is what you optimize against; ",[14,478,269],{}," just tells you ",[173,481,482],{},"which import"," to attack.",[27,485,487],{"id":486},"understanding-the-interpreters-fixed-floor","Understanding the interpreter's fixed floor",[10,489,490,491,494,495,70,498,501,502,504],{},"Not all startup time is your imports. Python itself does work before your code runs: initializing built-in modules and processing ",[14,492,493],{},"site.py",", which sets up ",[14,496,497],{},"sys.path",[14,499,500],{},".pth"," files, and site-packages. You can measure that floor by disabling site initialization with ",[14,503,84],{},":",[108,506,508],{"className":110,"code":507,"language":112,"meta":113,"style":113},"$ hyperfine --warmup 3 'python -c pass' 'python -S -c pass'\n",[14,509,510],{"__ignoreMap":113},[117,511,512,514,516,518,520,523],{"class":119,"line":120},[117,513,124],{"class":123},[117,515,367],{"class":127},[117,517,370],{"class":131},[117,519,373],{"class":131},[117,521,522],{"class":127}," 'python -c pass'",[117,524,525],{"class":127}," 'python -S -c pass'\n",[10,527,528,529,531,532,534,535,537,538,540,541,543,544,547],{},"The difference is your ",[14,530,80],{}," overhead. In environments with many installed packages (lots of ",[14,533,500],{}," files), ",[14,536,80],{}," processing can add tens of milliseconds. You generally shouldn't ship ",[14,539,84],{}," — it breaks ",[14,542,497],{}," assumptions and many packages — but knowing the floor tells you how much of your startup is even addressable. If ",[14,545,546],{},"python -c pass"," already takes 40 ms, no amount of import-deferring gets your CLI below that.",[27,549,551],{"id":550},"spotting-the-usual-offenders","Spotting the usual offenders",[10,553,554],{},"A handful of popular libraries dominate CLI startup profiles. Knowing them lets you predict problems before profiling:",[32,556,557,583,590,597],{},[35,558,559,563,564,70,567,570,571,574,575,578,579,582],{},[43,560,561],{},[14,562,69],{}," — pulls in ",[14,565,566],{},"urllib3",[14,568,569],{},"charset_normalizer",", and ",[14,572,573],{},"certifi","; commonly 30–60 ms. For a CLI that only occasionally makes HTTP calls, defer it or use ",[14,576,577],{},"urllib","\u002F",[14,580,581],{},"httpx"," behind a lazy import.",[35,584,585,589],{},[43,586,587],{},[14,588,73],{}," — imports NumPy, pytz, and its own large module tree; 100–300 ms is typical. Almost never belongs at a module top level in a CLI.",[35,591,592,596],{},[43,593,594],{},[14,595,76],{}," — v2's compiled core is fast at runtime but still adds noticeable import cost; if you use it only for one command's config model, defer it.",[35,598,599,608],{},[43,600,601,602,70,604,607],{},"Cloud SDKs (",[14,603,334],{},[14,605,606],{},"google-cloud-*",")"," — among the heaviest, often 200 ms+, because they build service clients from large data files at import.",[10,610,611,612,615,616,620],{},"The pattern is consistent: these are fine ",[173,613,614],{},"inside the command that needs them"," and expensive at your package's top level. The fix is deferral — either an import inside the function or, better, ",[337,617,619],{"href":618},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup\u002F","lazy loading the whole subcommand"," so the module holding the heavy import isn't touched until invoked. Profile to confirm which offender you actually have before rewriting anything.",[27,622,624],{"id":623},"a-pytest-that-enforces-a-startup-budget","A pytest that enforces a startup budget",[10,626,627],{},"Profiling is a one-time act; a budget keeps the win permanent. Encode your target as a test that runs the real CLI in a subprocess and asserts it finishes under a threshold:",[108,629,633],{"className":630,"code":631,"language":632,"meta":113,"style":113},"language-python shiki shiki-themes github-light github-dark","# tests\u002Ftest_startup_budget.py\nimport subprocess\nimport sys\nimport time\n\nimport pytest\n\nBUDGET_MS = 200  # generous headroom over local measurement for slower CI\n\ndef _time_help() -> float:\n    start = time.perf_counter()\n    result = subprocess.run(\n        [sys.executable, \"-m\", \"yourcli\", \"--help\"],\n        capture_output=True,\n        text=True,\n    )\n    elapsed_ms = (time.perf_counter() - start) * 1000\n    assert result.returncode == 0, result.stderr\n    return elapsed_ms\n\ndef test_help_within_budget() -> None:\n    # Warm bytecode\u002Ffilesystem caches, then take the best of three runs\n    # to reduce CI noise from a single unlucky sample.\n    _time_help()\n    best = min(_time_help() for _ in range(3))\n    assert best \u003C BUDGET_MS, f\"--help took {best:.0f} ms (budget {BUDGET_MS} ms)\"\n","python",[14,634,635,640,648,655,662,669,677,682,697,702,720,732,743,765,780,792,798,821,839,848,853,868,874,880,886,921],{"__ignoreMap":113},[117,636,637],{"class":119,"line":120},[117,638,639],{"class":297},"# tests\u002Ftest_startup_budget.py\n",[117,641,642,645],{"class":119,"line":229},[117,643,644],{"class":144},"import",[117,646,647],{"class":398}," subprocess\n",[117,649,650,652],{"class":119,"line":319},[117,651,644],{"class":144},[117,653,654],{"class":398}," sys\n",[117,656,657,659],{"class":119,"line":423},[117,658,644],{"class":144},[117,660,661],{"class":398}," time\n",[117,663,665],{"class":119,"line":664},5,[117,666,668],{"emptyLinePlaceholder":667},true,"\n",[117,670,672,674],{"class":119,"line":671},6,[117,673,644],{"class":144},[117,675,676],{"class":398}," pytest\n",[117,678,680],{"class":119,"line":679},7,[117,681,668],{"emptyLinePlaceholder":667},[117,683,685,688,691,694],{"class":119,"line":684},8,[117,686,687],{"class":131},"BUDGET_MS",[117,689,690],{"class":144}," =",[117,692,693],{"class":131}," 200",[117,695,696],{"class":297},"  # generous headroom over local measurement for slower CI\n",[117,698,700],{"class":119,"line":699},9,[117,701,668],{"emptyLinePlaceholder":667},[117,703,705,708,711,714,717],{"class":119,"line":704},10,[117,706,707],{"class":144},"def",[117,709,710],{"class":123}," _time_help",[117,712,713],{"class":398},"() -> ",[117,715,716],{"class":131},"float",[117,718,719],{"class":398},":\n",[117,721,723,726,729],{"class":119,"line":722},11,[117,724,725],{"class":398},"    start ",[117,727,728],{"class":144},"=",[117,730,731],{"class":398}," time.perf_counter()\n",[117,733,735,738,740],{"class":119,"line":734},12,[117,736,737],{"class":398},"    result ",[117,739,728],{"class":144},[117,741,742],{"class":398}," subprocess.run(\n",[117,744,746,749,752,754,757,759,762],{"class":119,"line":745},13,[117,747,748],{"class":398},"        [sys.executable, ",[117,750,751],{"class":127},"\"-m\"",[117,753,70],{"class":398},[117,755,756],{"class":127},"\"yourcli\"",[117,758,70],{"class":398},[117,760,761],{"class":127},"\"--help\"",[117,763,764],{"class":398},"],\n",[117,766,768,772,774,777],{"class":119,"line":767},14,[117,769,771],{"class":770},"s4XuR","        capture_output",[117,773,728],{"class":144},[117,775,776],{"class":131},"True",[117,778,779],{"class":398},",\n",[117,781,783,786,788,790],{"class":119,"line":782},15,[117,784,785],{"class":770},"        text",[117,787,728],{"class":144},[117,789,776],{"class":131},[117,791,779],{"class":398},[117,793,795],{"class":119,"line":794},16,[117,796,797],{"class":398},"    )\n",[117,799,801,804,806,809,812,815,818],{"class":119,"line":800},17,[117,802,803],{"class":398},"    elapsed_ms ",[117,805,728],{"class":144},[117,807,808],{"class":398}," (time.perf_counter() ",[117,810,811],{"class":144},"-",[117,813,814],{"class":398}," start) ",[117,816,817],{"class":144},"*",[117,819,820],{"class":131}," 1000\n",[117,822,824,827,830,833,836],{"class":119,"line":823},18,[117,825,826],{"class":144},"    assert",[117,828,829],{"class":398}," result.returncode ",[117,831,832],{"class":144},"==",[117,834,835],{"class":131}," 0",[117,837,838],{"class":398},", result.stderr\n",[117,840,842,845],{"class":119,"line":841},19,[117,843,844],{"class":144},"    return",[117,846,847],{"class":398}," elapsed_ms\n",[117,849,851],{"class":119,"line":850},20,[117,852,668],{"emptyLinePlaceholder":667},[117,854,856,858,861,863,866],{"class":119,"line":855},21,[117,857,707],{"class":144},[117,859,860],{"class":123}," test_help_within_budget",[117,862,713],{"class":398},[117,864,865],{"class":131},"None",[117,867,719],{"class":398},[117,869,871],{"class":119,"line":870},22,[117,872,873],{"class":297},"    # Warm bytecode\u002Ffilesystem caches, then take the best of three runs\n",[117,875,877],{"class":119,"line":876},23,[117,878,879],{"class":297},"    # to reduce CI noise from a single unlucky sample.\n",[117,881,883],{"class":119,"line":882},24,[117,884,885],{"class":398},"    _time_help()\n",[117,887,889,892,894,897,900,903,906,909,912,915,918],{"class":119,"line":888},25,[117,890,891],{"class":398},"    best ",[117,893,728],{"class":144},[117,895,896],{"class":131}," min",[117,898,899],{"class":398},"(_time_help() ",[117,901,902],{"class":144},"for",[117,904,905],{"class":398}," _ ",[117,907,908],{"class":144},"in",[117,910,911],{"class":131}," range",[117,913,914],{"class":398},"(",[117,916,917],{"class":131},"3",[117,919,920],{"class":398},"))\n",[117,922,924,926,929,932,935,937,940,943,946,949,952,955,958,961],{"class":119,"line":923},26,[117,925,826],{"class":144},[117,927,928],{"class":398}," best ",[117,930,931],{"class":144},"\u003C",[117,933,934],{"class":131}," BUDGET_MS",[117,936,70],{"class":398},[117,938,939],{"class":144},"f",[117,941,942],{"class":127},"\"--help took ",[117,944,945],{"class":131},"{",[117,947,948],{"class":398},"best",[117,950,951],{"class":144},":.0f",[117,953,954],{"class":131},"}",[117,956,957],{"class":127}," ms (budget ",[117,959,960],{"class":131},"{BUDGET_MS}",[117,962,963],{"class":127}," ms)\"\n",[10,965,966,967,969,970,973,974,976],{},"Two design choices make this robust rather than flaky: warm the caches with a throwaway run first, and take the best of several samples so one noisy CI moment doesn't fail the build. Set ",[14,968,687],{}," well above your measured local time — CI runners are slower and shared — and tighten it only if you have headroom. The goal isn't a precise benchmark; it's a tripwire. The day someone adds ",[14,971,972],{},"import boto3"," to a shared module, this test fails with a clear message pointing at the regression, and you profile again with ",[14,975,269],{}," to find the new offender.",[27,978,980],{"id":979},"production-notes","Production notes",[32,982,983,996,1013,1025,1031],{},[35,984,985,988,989,991,992,995],{},[43,986,987],{},"Always warm up."," The first invocation compiles ",[14,990,448],{}," files and populates OS caches; an unwarmed measurement overstates startup by tens of milliseconds. Both ",[14,993,994],{},"hyperfine --warmup"," and the test above account for this.",[35,997,998,1001,1002,1004,1005,1007,1008,1012],{},[43,999,1000],{},"Profile in a clean environment."," A cluttered virtualenv with many ",[14,1003,500],{}," files inflates ",[14,1006,80],{}," overhead; measure in something close to what users install into. ",[337,1009,1011],{"href":1010},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002F","Virtual environment isolation"," keeps this reproducible.",[35,1014,1015,1020,1021,1024],{},[43,1016,1017,1019],{},[14,1018,105],{}," counts each import once."," A module already in ",[14,1022,1023],{},"sys.modules"," shows near-zero cost, so import order affects the attribution — the first importer of a shared dependency gets \"charged\" for it. Read the tree, not just one line.",[35,1026,1027,1030],{},[43,1028,1029],{},"CI numbers are not local numbers."," Don't copy your laptop's 90 ms into the budget; measure on the CI runner and add margin. A budget that flaps erodes trust and gets disabled.",[35,1032,1033,1038,1039,85],{},[43,1034,1035,1036,85],{},"Measure the hot path, not just ",[14,1037,95],{}," If shell completion is latency-critical, benchmark the completion invocation too — it runs your CLI on every Tab. See ",[337,1040,1042],{"href":1041},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F","shell completion for Python CLIs",[27,1044,1046],{"id":1045},"related","Related",[32,1048,1049,1056,1062,1067],{},[35,1050,1051,1055],{},[337,1052,1054],{"href":1053},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI Startup Performance and Lazy Loading"," — the overview that frames the fixes.",[35,1057,1058,1061],{},[337,1059,1060],{"href":618},"Lazy loading subcommands for faster startup"," — the deferral technique your profiling justifies.",[35,1063,1064,1066],{},[337,1065,340],{"href":339}," — installing tools like tuna in isolation.",[35,1068,1069,1072],{},[337,1070,1071],{"href":1010},"Virtual environments & isolation best practices"," — profiling in a clean, reproducible environment.",[1074,1075,1076],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":113,"searchDepth":229,"depth":229,"links":1078},[1079,1080,1081,1082,1083,1084,1085,1086,1087],{"id":29,"depth":229,"text":30},{"id":99,"depth":229,"text":100},{"id":262,"depth":229,"text":263},{"id":343,"depth":229,"text":344},{"id":486,"depth":229,"text":487},{"id":550,"depth":229,"text":551},{"id":623,"depth":229,"text":624},{"id":979,"depth":229,"text":980},{"id":1045,"depth":229,"text":1046},"2026-07-05","Measure why a Python CLI is slow to start with python -X importtime and tuna, find the expensive imports, and set a startup-time budget you can enforce in CI.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time",{"title":5,"description":1089},"modern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002Findex",[1098,1099,1100,1101],"performance","startup","profiling","cli","uKg50DbPuk5Zd4Z4V4ro0zWqD7eYfoWnmpk7iz-Xcrw",[1104,1107,1110,1113,1116,1119,1122,1125,1128,1131,1134,1137,1140,1143,1146,1149,1152,1155,1158,1160,1162,1165,1166,1169,1172,1175,1178,1181,1184,1187,1190,1193,1196,1199,1202,1205,1208,1211,1214,1217,1220,1223,1226,1229,1232,1235,1238,1241,1244,1246,1249],{"path":1105,"title":1106},"\u002Fabout","About Python CLI Toolcraft",{"path":1108,"title":1109},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1111,"title":1112},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1114,"title":1115},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1117,"title":1118},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1120,"title":1121},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1123,"title":1124},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1126,"title":1127},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1129,"title":1130},"\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":1132,"title":1133},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1135,"title":1136},"\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":1138,"title":1139},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1141,"title":1142},"\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":1144,"title":1145},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1147,"title":1148},"\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":1150,"title":1151},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1153,"title":1154},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1156,"title":1157},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":578,"title":1159},"Python CLI Toolcraft",{"path":1161,"title":1054},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading",{"path":1163,"title":1164},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1094,"title":5},{"path":1167,"title":1168},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1170,"title":1171},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1173,"title":1174},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1176,"title":1177},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1179,"title":1180},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1182,"title":1183},"\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":1185,"title":1186},"\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":1188,"title":1189},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1191,"title":1192},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1194,"title":1195},"\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":1197,"title":1198},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1200,"title":1201},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1203,"title":1204},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1206,"title":1207},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1209,"title":1210},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1212,"title":1213},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1215,"title":1216},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1218,"title":1219},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1221,"title":1222},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1224,"title":1225},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1227,"title":1228},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1230,"title":1231},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1233,"title":1234},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1236,"title":1237},"\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":1239,"title":1240},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1242,"title":1243},"\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":1245,"title":340},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis",{"path":1247,"title":1248},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1250,"title":1251},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867197]