[{"data":1,"prerenderedAt":1358},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002F":3,"content-directory":1113},{"id":4,"title":5,"body":6,"date":1098,"description":1099,"difficulty":1100,"draft":1101,"extension":1102,"meta":1103,"navigation":677,"path":1104,"seo":1105,"stem":1106,"tags":1107,"updated":1098,"__hash__":1112},"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":1087},"minimark",[9,26,31,97,101,107,111,153,160,168,193,203,206,264,268,278,281,333,348,352,363,446,460,481,491,495,512,515,536,558,562,565,619,631,635,638,974,987,991,1053,1057,1083],[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],"inline-diagram",{"name":110},"importtime-reading",[112,113,118],"pre",{"className":114,"code":115,"language":116,"meta":117,"style":117},"language-bash shiki shiki-themes github-light github-dark","$ python -X importtime -c \"import yourcli.cli\" 2> importtime.log\n","bash","",[14,119,120],{"__ignoreMap":117},[121,122,125,129,133,137,140,143,146,150],"span",{"class":123,"line":124},"line",1,[121,126,128],{"class":127},"sScJk","$",[121,130,132],{"class":131},"sZZnC"," python",[121,134,136],{"class":135},"sj4cs"," -X",[121,138,139],{"class":131}," importtime",[121,141,142],{"class":135}," -c",[121,144,145],{"class":131}," \"import yourcli.cli\"",[121,147,149],{"class":148},"szBVR"," 2>",[121,151,152],{"class":131}," importtime.log\n",[10,154,155,156,159],{},"The output goes to stderr (hence ",[14,157,158],{},"2>","), one line per import, with three columns:",[112,161,166],{"className":162,"code":164,"language":165,"meta":117},[163],"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,167,164],{"__ignoreMap":117},[32,169,170,181,190],{},[35,171,172,175,176,180],{},[43,173,174],{},"self"," — time spent importing ",[177,178,179],"em",{},"just"," that module, excluding its children.",[35,182,183,185,186,189],{},[43,184,45],{}," — time for that module ",[177,187,188],{},"and everything it imports",". This is the column that matters.",[35,191,192],{},"Indentation shows the import tree; a deeply nested module was pulled in by its less-indented parent.",[10,194,195,196,198,199,202],{},"Read it top-down by cumulative cost. In the sample above, ",[14,197,73],{}," at 48 ms cumulative is dragged in by ",[14,200,201],{},"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,204,205],{},"A useful one-liner to surface the worst offenders sorts the log by cumulative time:",[112,207,209],{"className":114,"code":208,"language":116,"meta":117,"style":117},"$ python -X importtime -c \"import yourcli.cli\" 2>&1 \\\n    | sort -t'|' -k2 -n -r | head -15\n",[14,210,211,231],{"__ignoreMap":117},[121,212,213,215,217,219,221,223,225,228],{"class":123,"line":124},[121,214,128],{"class":127},[121,216,132],{"class":131},[121,218,136],{"class":135},[121,220,139],{"class":131},[121,222,142],{"class":135},[121,224,145],{"class":131},[121,226,227],{"class":148}," 2>&1",[121,229,230],{"class":135}," \\\n",[121,232,234,237,240,243,246,249,252,255,258,261],{"class":123,"line":233},2,[121,235,236],{"class":148},"    |",[121,238,239],{"class":127}," sort",[121,241,242],{"class":135}," -t",[121,244,245],{"class":131},"'|'",[121,247,248],{"class":135}," -k2",[121,250,251],{"class":135}," -n",[121,253,254],{"class":135}," -r",[121,256,257],{"class":148}," |",[121,259,260],{"class":127}," head",[121,262,263],{"class":135}," -15\n",[27,265,267],{"id":266},"visualize-the-tree-with-tuna","Visualize the tree with tuna",[10,269,270,271,274,275,277],{},"Raw ",[14,272,273],{},"importtime"," output gets unwieldy for a large app. ",[14,276,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,279],{"name":280},"importtime-toolchain",[112,282,284],{"className":114,"code":283,"language":116,"meta":117,"style":117},"$ 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,285,286,306,324],{"__ignoreMap":117},[121,287,288,290,293,296,299,302],{"class":123,"line":124},[121,289,128],{"class":127},[121,291,292],{"class":131}," uv",[121,294,295],{"class":131}," tool",[121,297,298],{"class":131}," install",[121,300,301],{"class":131}," tuna",[121,303,305],{"class":304},"sJ8bj","          # or: pipx install tuna\n",[121,307,308,310,312,314,316,318,320,322],{"class":123,"line":233},[121,309,128],{"class":127},[121,311,132],{"class":131},[121,313,136],{"class":135},[121,315,139],{"class":131},[121,317,142],{"class":135},[121,319,145],{"class":131},[121,321,149],{"class":148},[121,323,152],{"class":131},[121,325,327,329,331],{"class":123,"line":326},3,[121,328,128],{"class":127},[121,330,301],{"class":131},[121,332,152],{"class":131},[10,334,335,336,338,339,342,343,85],{},"The graphical view makes it obvious when one dependency dominates — a single wide block for ",[14,337,73],{}," or ",[14,340,341],{},"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 ",[344,345,347],"a",{"href":346},"\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,349,351],{"id":350},"measure-real-wall-clock-time-with-hyperfine","Measure real wall-clock time with hyperfine",[10,353,354,356,357,359,360,362],{},[14,355,273],{}," measures imports; it does not measure the full user-visible latency, which also includes interpreter start and ",[14,358,80],{}," initialization. For the number users actually feel, benchmark the real command with ",[14,361,24],{},", which runs it many times and reports mean, standard deviation, and warmup behavior:",[112,364,366],{"className":114,"code":365,"language":116,"meta":117,"style":117},"$ 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,367,368,384,398,428],{"__ignoreMap":117},[121,369,370,372,375,378,381],{"class":123,"line":124},[121,371,128],{"class":127},[121,373,374],{"class":131}," hyperfine",[121,376,377],{"class":135}," --warmup",[121,379,380],{"class":135}," 3",[121,382,383],{"class":131}," 'yourcli --help'\n",[121,385,386,389,392,395],{"class":123,"line":233},[121,387,388],{"class":127},"Benchmark",[121,390,391],{"class":131}," 1:",[121,393,394],{"class":131}," yourcli",[121,396,397],{"class":135}," --help\n",[121,399,400,403,407,410,413,416,419,422,425],{"class":123,"line":326},[121,401,402],{"class":127},"  Time",[121,404,406],{"class":405},"sVt8B"," (mean ",[121,408,409],{"class":131},"±",[121,411,412],{"class":131}," σ",[121,414,415],{"class":405},"):     412.6 ms ±   9.1 ms    [User: ",[121,417,418],{"class":135},"380.2",[121,420,421],{"class":405}," ms, System: ",[121,423,424],{"class":135},"41.7",[121,426,427],{"class":405}," ms]\n",[121,429,431,434,437,440,443],{"class":123,"line":430},4,[121,432,433],{"class":127},"  Range",[121,435,436],{"class":405}," (min ",[121,438,439],{"class":131},"…",[121,441,442],{"class":131}," max",[121,444,445],{"class":405},"):   401.3 ms … 428.9 ms    10 runs\n",[10,447,448,449,452,453,456,457,459],{},"The ",[14,450,451],{},"--warmup 3"," runs discard the first few executions so filesystem and bytecode caches are warm — otherwise the first run's ",[14,454,455],{},".pyc"," compilation skews the mean. Use ",[14,458,24],{}," to compare two versions directly:",[112,461,463],{"className":114,"code":462,"language":116,"meta":117,"style":117},"$ hyperfine --warmup 3 'yourcli-eager --help' 'yourcli-lazy --help'\n",[14,464,465],{"__ignoreMap":117},[121,466,467,469,471,473,475,478],{"class":123,"line":124},[121,468,128],{"class":127},[121,470,374],{"class":131},[121,472,377],{"class":135},[121,474,380],{"class":135},[121,476,477],{"class":131}," 'yourcli-eager --help'",[121,479,480],{"class":131}," 'yourcli-lazy --help'\n",[10,482,483,484,486,487,490],{},"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,485,273],{}," just tells you ",[177,488,489],{},"which import"," to attack.",[27,492,494],{"id":493},"understanding-the-interpreters-fixed-floor","Understanding the interpreter's fixed floor",[10,496,497,498,501,502,70,505,508,509,511],{},"Not all startup time is your imports. Python itself does work before your code runs: initializing built-in modules and processing ",[14,499,500],{},"site.py",", which sets up ",[14,503,504],{},"sys.path",[14,506,507],{},".pth"," files, and site-packages. You can measure that floor by disabling site initialization with ",[14,510,84],{},":",[108,513],{"name":514},"interpreter-floor-bars",[112,516,518],{"className":114,"code":517,"language":116,"meta":117,"style":117},"$ hyperfine --warmup 3 'python -c pass' 'python -S -c pass'\n",[14,519,520],{"__ignoreMap":117},[121,521,522,524,526,528,530,533],{"class":123,"line":124},[121,523,128],{"class":127},[121,525,374],{"class":131},[121,527,377],{"class":135},[121,529,380],{"class":135},[121,531,532],{"class":131}," 'python -c pass'",[121,534,535],{"class":131}," 'python -S -c pass'\n",[10,537,538,539,541,542,544,545,547,548,550,551,553,554,557],{},"The difference is your ",[14,540,80],{}," overhead. In environments with many installed packages (lots of ",[14,543,507],{}," files), ",[14,546,80],{}," processing can add tens of milliseconds. You generally shouldn't ship ",[14,549,84],{}," — it breaks ",[14,552,504],{}," assumptions and many packages — but knowing the floor tells you how much of your startup is even addressable. If ",[14,555,556],{},"python -c pass"," already takes 40 ms, no amount of import-deferring gets your CLI below that.",[27,559,561],{"id":560},"spotting-the-usual-offenders","Spotting the usual offenders",[10,563,564],{},"A handful of popular libraries dominate CLI startup profiles. Knowing them lets you predict problems before profiling:",[32,566,567,593,600,607],{},[35,568,569,573,574,70,577,580,581,584,585,588,589,592],{},[43,570,571],{},[14,572,69],{}," — pulls in ",[14,575,576],{},"urllib3",[14,578,579],{},"charset_normalizer",", and ",[14,582,583],{},"certifi","; commonly 30–60 ms. For a CLI that only occasionally makes HTTP calls, defer it or use ",[14,586,587],{},"urllib","\u002F",[14,590,591],{},"httpx"," behind a lazy import.",[35,594,595,599],{},[43,596,597],{},[14,598,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,601,602,606],{},[43,603,604],{},[14,605,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,608,609,618],{},[43,610,611,612,70,614,617],{},"Cloud SDKs (",[14,613,341],{},[14,615,616],{},"google-cloud-*",")"," — among the heaviest, often 200 ms+, because they build service clients from large data files at import.",[10,620,621,622,625,626,630],{},"The pattern is consistent: these are fine ",[177,623,624],{},"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, ",[344,627,629],{"href":628},"\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,632,634],{"id":633},"a-pytest-that-enforces-a-startup-budget","A pytest that enforces a startup budget",[10,636,637],{},"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:",[112,639,643],{"className":640,"code":641,"language":642,"meta":117,"style":117},"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,644,645,650,658,665,672,679,687,692,707,712,730,742,753,775,790,802,808,831,849,858,863,878,884,890,896,931],{"__ignoreMap":117},[121,646,647],{"class":123,"line":124},[121,648,649],{"class":304},"# tests\u002Ftest_startup_budget.py\n",[121,651,652,655],{"class":123,"line":233},[121,653,654],{"class":148},"import",[121,656,657],{"class":405}," subprocess\n",[121,659,660,662],{"class":123,"line":326},[121,661,654],{"class":148},[121,663,664],{"class":405}," sys\n",[121,666,667,669],{"class":123,"line":430},[121,668,654],{"class":148},[121,670,671],{"class":405}," time\n",[121,673,675],{"class":123,"line":674},5,[121,676,678],{"emptyLinePlaceholder":677},true,"\n",[121,680,682,684],{"class":123,"line":681},6,[121,683,654],{"class":148},[121,685,686],{"class":405}," pytest\n",[121,688,690],{"class":123,"line":689},7,[121,691,678],{"emptyLinePlaceholder":677},[121,693,695,698,701,704],{"class":123,"line":694},8,[121,696,697],{"class":135},"BUDGET_MS",[121,699,700],{"class":148}," =",[121,702,703],{"class":135}," 200",[121,705,706],{"class":304},"  # generous headroom over local measurement for slower CI\n",[121,708,710],{"class":123,"line":709},9,[121,711,678],{"emptyLinePlaceholder":677},[121,713,715,718,721,724,727],{"class":123,"line":714},10,[121,716,717],{"class":148},"def",[121,719,720],{"class":127}," _time_help",[121,722,723],{"class":405},"() -> ",[121,725,726],{"class":135},"float",[121,728,729],{"class":405},":\n",[121,731,733,736,739],{"class":123,"line":732},11,[121,734,735],{"class":405},"    start ",[121,737,738],{"class":148},"=",[121,740,741],{"class":405}," time.perf_counter()\n",[121,743,745,748,750],{"class":123,"line":744},12,[121,746,747],{"class":405},"    result ",[121,749,738],{"class":148},[121,751,752],{"class":405}," subprocess.run(\n",[121,754,756,759,762,764,767,769,772],{"class":123,"line":755},13,[121,757,758],{"class":405},"        [sys.executable, ",[121,760,761],{"class":131},"\"-m\"",[121,763,70],{"class":405},[121,765,766],{"class":131},"\"yourcli\"",[121,768,70],{"class":405},[121,770,771],{"class":131},"\"--help\"",[121,773,774],{"class":405},"],\n",[121,776,778,782,784,787],{"class":123,"line":777},14,[121,779,781],{"class":780},"s4XuR","        capture_output",[121,783,738],{"class":148},[121,785,786],{"class":135},"True",[121,788,789],{"class":405},",\n",[121,791,793,796,798,800],{"class":123,"line":792},15,[121,794,795],{"class":780},"        text",[121,797,738],{"class":148},[121,799,786],{"class":135},[121,801,789],{"class":405},[121,803,805],{"class":123,"line":804},16,[121,806,807],{"class":405},"    )\n",[121,809,811,814,816,819,822,825,828],{"class":123,"line":810},17,[121,812,813],{"class":405},"    elapsed_ms ",[121,815,738],{"class":148},[121,817,818],{"class":405}," (time.perf_counter() ",[121,820,821],{"class":148},"-",[121,823,824],{"class":405}," start) ",[121,826,827],{"class":148},"*",[121,829,830],{"class":135}," 1000\n",[121,832,834,837,840,843,846],{"class":123,"line":833},18,[121,835,836],{"class":148},"    assert",[121,838,839],{"class":405}," result.returncode ",[121,841,842],{"class":148},"==",[121,844,845],{"class":135}," 0",[121,847,848],{"class":405},", result.stderr\n",[121,850,852,855],{"class":123,"line":851},19,[121,853,854],{"class":148},"    return",[121,856,857],{"class":405}," elapsed_ms\n",[121,859,861],{"class":123,"line":860},20,[121,862,678],{"emptyLinePlaceholder":677},[121,864,866,868,871,873,876],{"class":123,"line":865},21,[121,867,717],{"class":148},[121,869,870],{"class":127}," test_help_within_budget",[121,872,723],{"class":405},[121,874,875],{"class":135},"None",[121,877,729],{"class":405},[121,879,881],{"class":123,"line":880},22,[121,882,883],{"class":304},"    # Warm bytecode\u002Ffilesystem caches, then take the best of three runs\n",[121,885,887],{"class":123,"line":886},23,[121,888,889],{"class":304},"    # to reduce CI noise from a single unlucky sample.\n",[121,891,893],{"class":123,"line":892},24,[121,894,895],{"class":405},"    _time_help()\n",[121,897,899,902,904,907,910,913,916,919,922,925,928],{"class":123,"line":898},25,[121,900,901],{"class":405},"    best ",[121,903,738],{"class":148},[121,905,906],{"class":135}," min",[121,908,909],{"class":405},"(_time_help() ",[121,911,912],{"class":148},"for",[121,914,915],{"class":405}," _ ",[121,917,918],{"class":148},"in",[121,920,921],{"class":135}," range",[121,923,924],{"class":405},"(",[121,926,927],{"class":135},"3",[121,929,930],{"class":405},"))\n",[121,932,934,936,939,942,945,947,950,953,956,959,962,965,968,971],{"class":123,"line":933},26,[121,935,836],{"class":148},[121,937,938],{"class":405}," best ",[121,940,941],{"class":148},"\u003C",[121,943,944],{"class":135}," BUDGET_MS",[121,946,70],{"class":405},[121,948,949],{"class":148},"f",[121,951,952],{"class":131},"\"--help took ",[121,954,955],{"class":135},"{",[121,957,958],{"class":405},"best",[121,960,961],{"class":148},":.0f",[121,963,964],{"class":135},"}",[121,966,967],{"class":131}," ms (budget ",[121,969,970],{"class":135},"{BUDGET_MS}",[121,972,973],{"class":131}," ms)\"\n",[10,975,976,977,979,980,983,984,986],{},"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,978,697],{}," 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,981,982],{},"import boto3"," to a shared module, this test fails with a clear message pointing at the regression, and you profile again with ",[14,985,273],{}," to find the new offender.",[27,988,990],{"id":989},"production-notes","Production notes",[32,992,993,1006,1023,1035,1041],{},[35,994,995,998,999,1001,1002,1005],{},[43,996,997],{},"Always warm up."," The first invocation compiles ",[14,1000,455],{}," files and populates OS caches; an unwarmed measurement overstates startup by tens of milliseconds. Both ",[14,1003,1004],{},"hyperfine --warmup"," and the test above account for this.",[35,1007,1008,1011,1012,1014,1015,1017,1018,1022],{},[43,1009,1010],{},"Profile in a clean environment."," A cluttered virtualenv with many ",[14,1013,507],{}," files inflates ",[14,1016,80],{}," overhead; measure in something close to what users install into. ",[344,1019,1021],{"href":1020},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002F","Virtual environment isolation"," keeps this reproducible.",[35,1024,1025,1030,1031,1034],{},[43,1026,1027,1029],{},[14,1028,105],{}," counts each import once."," A module already in ",[14,1032,1033],{},"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,1036,1037,1040],{},[43,1038,1039],{},"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,1042,1043,1048,1049,85],{},[43,1044,1045,1046,85],{},"Measure the hot path, not just ",[14,1047,95],{}," If shell completion is latency-critical, benchmark the completion invocation too — it runs your CLI on every Tab. See ",[344,1050,1052],{"href":1051},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F","shell completion for Python CLIs",[27,1054,1056],{"id":1055},"related","Related",[32,1058,1059,1066,1072,1077],{},[35,1060,1061,1065],{},[344,1062,1064],{"href":1063},"\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,1067,1068,1071],{},[344,1069,1070],{"href":628},"Lazy loading subcommands for faster startup"," — the deferral technique your profiling justifies.",[35,1073,1074,1076],{},[344,1075,347],{"href":346}," — installing tools like tuna in isolation.",[35,1078,1079,1082],{},[344,1080,1081],{"href":1020},"Virtual environments & isolation best practices"," — profiling in a clean, reproducible environment.",[1084,1085,1086],"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":117,"searchDepth":233,"depth":233,"links":1088},[1089,1090,1091,1092,1093,1094,1095,1096,1097],{"id":29,"depth":233,"text":30},{"id":99,"depth":233,"text":100},{"id":266,"depth":233,"text":267},{"id":350,"depth":233,"text":351},{"id":493,"depth":233,"text":494},{"id":560,"depth":233,"text":561},{"id":633,"depth":233,"text":634},{"id":989,"depth":233,"text":990},{"id":1055,"depth":233,"text":1056},"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":1099},"modern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002Findex",[1108,1109,1110,1111],"performance","startup","profiling","cli","NBA1BYX24Wtke3hfs-z6h8KFjEv8ftiIB7bpbWI16js",[1114,1117,1120,1123,1126,1129,1132,1135,1138,1141,1144,1147,1150,1153,1156,1159,1162,1165,1168,1171,1174,1177,1180,1183,1186,1189,1192,1195,1198,1201,1204,1207,1209,1211,1214,1215,1218,1221,1224,1227,1230,1233,1236,1239,1242,1245,1248,1251,1254,1257,1260,1263,1266,1269,1272,1275,1278,1281,1284,1287,1290,1293,1296,1299,1302,1305,1308,1311,1314,1317,1320,1323,1326,1329,1332,1335,1338,1341,1344,1347,1349,1352,1355],{"path":1115,"title":1116},"\u002Fabout","About Python CLI Toolcraft",{"path":1118,"title":1119},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1121,"title":1122},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1124,"title":1125},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1127,"title":1128},"\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":1130,"title":1131},"\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":1133,"title":1134},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1136,"title":1137},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1139,"title":1140},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1142,"title":1143},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1145,"title":1146},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1148,"title":1149},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1151,"title":1152},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1154,"title":1155},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1157,"title":1158},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1160,"title":1161},"\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":1163,"title":1164},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1166,"title":1167},"\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":1169,"title":1170},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1172,"title":1173},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1175,"title":1176},"\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":1178,"title":1179},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1181,"title":1182},"\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":1184,"title":1185},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1187,"title":1188},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1190,"title":1191},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1193,"title":1194},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1196,"title":1197},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1199,"title":1200},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1202,"title":1203},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1205,"title":1206},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":588,"title":1208},"Python CLI Toolcraft",{"path":1210,"title":1064},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading",{"path":1212,"title":1213},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1104,"title":5},{"path":1216,"title":1217},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1219,"title":1220},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1222,"title":1223},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1225,"title":1226},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1228,"title":1229},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1231,"title":1232},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1234,"title":1235},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1237,"title":1238},"\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":1240,"title":1241},"\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":1243,"title":1244},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1246,"title":1247},"\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":1249,"title":1250},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1252,"title":1253},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1255,"title":1256},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1258,"title":1259},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1261,"title":1262},"\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":1264,"title":1265},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1267,"title":1268},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1270,"title":1271},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1273,"title":1274},"\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":1276,"title":1277},"\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":1279,"title":1280},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1282,"title":1283},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1285,"title":1286},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1288,"title":1289},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1291,"title":1292},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1294,"title":1295},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1297,"title":1298},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1300,"title":1301},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1303,"title":1304},"\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":1306,"title":1307},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1309,"title":1310},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1312,"title":1313},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1315,"title":1316},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1318,"title":1319},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1321,"title":1322},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1324,"title":1325},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1327,"title":1328},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1330,"title":1331},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1333,"title":1334},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1336,"title":1337},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1339,"title":1340},"\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":1342,"title":1343},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1345,"title":1346},"\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":1348,"title":347},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis",{"path":1350,"title":1351},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1353,"title":1354},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1356,"title":1357},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690031]