[{"data":1,"prerenderedAt":1242},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F":3,"content-directory":1090},{"id":4,"title":5,"body":6,"date":1074,"description":1075,"difficulty":1076,"draft":1077,"extension":1078,"meta":1079,"navigation":319,"path":1080,"seo":1081,"stem":1082,"tags":1083,"updated":1074,"__hash__":1089},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Findex.md","CLI Startup Performance and Lazy Loading",{"type":7,"value":8,"toc":1062},"minimark",[9,26,31,76,80,84,95,137,140,144,163,189,204,208,211,253,273,277,280,374,435,448,452,463,717,745,753,757,786,797,801,808,977,983,987,990,1022,1026,1058],[10,11,12,13,17,18,21,22,25],"p",{},"Python CLIs get slow to start long before they get slow to run. The culprit is almost never your code — it's the import graph that fires the instant the interpreter loads your entry module, dragging in ",[14,15,16],"code",{},"pandas",", ",[14,19,20],{},"requests",", or a cloud SDK before the user has even chosen a subcommand. This section shows you how to measure where the startup time actually goes, then defer the expensive work so ",[14,23,24],{},"--help",", tab completion, and quick commands stay instant.",[27,28,30],"h2",{"id":29},"tldr","TL;DR",[32,33,34,45,53,63,66],"ul",{},[35,36,37,38,40,41,44],"li",{},"Startup latency is felt most where users don't expect any work: ",[14,39,24],{},", shell completion, ",[14,42,43],{},"--version",", and short-lived commands in scripts and loops.",[35,46,47,48,52],{},"On a cold CLI, ",[49,50,51],"strong",{},"imports dominate"," — often 80–95% of wall-clock time before your function runs. Your logic is rarely the problem.",[35,54,55,58,59,62],{},[49,56,57],{},"Measure first."," Use ",[14,60,61],{},"python -X importtime"," to find the expensive imports before you change anything; optimizing by guesswork wastes effort on cheap imports.",[35,64,65],{},"Three fixes, in order of payoff: move module-level heavy imports inside the functions that use them; lazy-load whole subcommands so their dependencies load only when invoked; and avoid pulling heavy libraries into your top-level package at all.",[35,67,68,69,72,73,75],{},"Set a ",[49,70,71],{},"startup budget"," (e.g. ",[14,74,24],{}," under 100 ms) and assert it in CI so a careless import can't quietly regress it.",[77,78],"inline-diagram",{"name":79},"lazy-import-startup",[27,81,83],{"id":82},"why-startup-latency-matters","Why startup latency matters",[10,85,86,87,91,92,94],{},"A CLI is not a web server that pays its startup cost once and amortizes it over millions of requests. It pays that cost on ",[88,89,90],"em",{},"every single invocation",". If your tool takes 600 ms to print ",[14,93,24],{},", that half-second is charged to the user every time, and it compounds in the places that should feel free:",[32,96,97,109,127],{},[35,98,99,102,103,108],{},[49,100,101],{},"Shell completion."," Every time the user hits Tab, your CLI runs to produce candidates. If completion takes 400 ms, the shell feels broken — users stop pressing Tab. This is the single most latency-sensitive path in any CLI. See ",[104,105,107],"a",{"href":106},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F","shell completion for Python CLIs"," for how that path is wired.",[35,110,111,119,120,123,124,126],{},[49,112,113,115,116,118],{},[14,114,24],{}," and ",[14,117,43],{},"."," These are pure metadata. A user running ",[14,121,122],{},"mycli --help"," to remember a flag should never wait on ",[14,125,16],{}," importing NumPy and pytz.",[35,128,129,132,133,136],{},[49,130,131],{},"Scripts and loops."," When your CLI is called inside a shell loop — ",[14,134,135],{},"for f in *.csv; do mycli convert \"$f\"; done"," — a 500 ms startup on 1,000 files is over eight minutes of pure import overhead.",[10,138,139],{},"Sub-100 ms feels instant. Past ~250 ms, interactive use starts to feel sluggish. The good news: the fix is almost always mechanical once you know where the time goes.",[27,141,143],{"id":142},"where-the-time-goes-imports-dominate","Where the time goes: imports dominate",[10,145,146,147,150,151,154,155,158,159,162],{},"Here is the mental model. When the shell runs ",[14,148,149],{},"mycli",", Python starts, initializes the ",[14,152,153],{},"site"," machinery, imports your entry-point module, and ",[88,156,157],{},"then"," parses arguments and dispatches. That import step transitively pulls in everything your modules reference at the top level. A single ",[14,160,161],{},"import pandas"," at the top of a command module can cost 150–300 ms on its own — and it runs whether or not the user invoked the command that needs it.",[10,164,165,166,169,170,172,173,169,176,178,179,169,182,185,186,188],{},"Consider a CLI with ",[14,167,168],{},"convert"," (needs ",[14,171,16],{},"), ",[14,174,175],{},"fetch",[14,177,20],{},"), and ",[14,180,181],{},"report",[14,183,184],{},"matplotlib","). If all three command modules are imported when the app is constructed, then ",[14,187,122],{}," pays for all three. The user asked for one line of help and got charged for the entire dependency tree.",[10,190,191,192,195,196,199,200,203],{},"The insight most people miss: ",[49,193,194],{},"your own code is almost never the bottleneck."," Parsing arguments, building a Click group, and dispatching are microsecond-scale operations. The wall-clock time is spent inside ",[14,197,198],{},"import"," statements in third-party packages you don't control. That is why the fix is about ",[88,201,202],{},"when"," imports happen, not about making anything faster.",[27,205,207],{"id":206},"the-measure-first-rule","The measure-first rule",[10,209,210],{},"Do not optimize startup by intuition. The expensive import is frequently not the one you'd guess — a validation library or a lazily-configured logging package can outweigh the \"obviously heavy\" one. Python has a built-in profiler for exactly this:",[212,213,218],"pre",{"className":214,"code":215,"language":216,"meta":217,"style":217},"language-bash shiki shiki-themes github-light github-dark","$ python -X importtime -c \"import mycli.cli\" 2> importtime.log\n","bash","",[14,219,220],{"__ignoreMap":217},[221,222,225,229,233,237,240,243,246,250],"span",{"class":223,"line":224},"line",1,[221,226,228],{"class":227},"sScJk","$",[221,230,232],{"class":231},"sZZnC"," python",[221,234,236],{"class":235},"sj4cs"," -X",[221,238,239],{"class":231}," importtime",[221,241,242],{"class":235}," -c",[221,244,245],{"class":231}," \"import mycli.cli\"",[221,247,249],{"class":248},"szBVR"," 2>",[221,251,252],{"class":231}," importtime.log\n",[10,254,255,256,259,260,263,264,267,268,272],{},"The ",[14,257,258],{},"cumulative"," column shows each import's cost including its children, so the biggest numbers at the top of the tree are your targets. Reading that output — and visualizing it with ",[14,261,262],{},"tuna",", timing the real command with ",[14,265,266],{},"hyperfine",", and spotting the usual offenders — is a topic on its own: ",[104,269,271],{"href":270},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002F","profiling Python CLI startup time"," walks through the full workflow. Measure, change one thing, measure again. Anything else is superstition.",[27,274,276],{"id":275},"fix-1-defer-module-level-imports","Fix 1: defer module-level imports",[10,278,279],{},"The lowest-effort win is moving a heavy import off the module top level and into the function that uses it. Nothing else about your code changes:",[212,281,285],{"className":282,"code":283,"language":284,"meta":217,"style":217},"language-python shiki shiki-themes github-light github-dark","# Before: pandas loads the moment this module is imported,\n# i.e. every time the CLI starts, for every command.\nimport pandas as pd\n\ndef convert(path: str) -> None:\n    df = pd.read_csv(path)\n    df.to_parquet(path.replace(\".csv\", \".parquet\"))\n","python",[14,286,287,293,299,314,321,345,357],{"__ignoreMap":217},[221,288,289],{"class":223,"line":224},[221,290,292],{"class":291},"sJ8bj","# Before: pandas loads the moment this module is imported,\n",[221,294,296],{"class":223,"line":295},2,[221,297,298],{"class":291},"# i.e. every time the CLI starts, for every command.\n",[221,300,302,304,308,311],{"class":223,"line":301},3,[221,303,198],{"class":248},[221,305,307],{"class":306},"sVt8B"," pandas ",[221,309,310],{"class":248},"as",[221,312,313],{"class":306}," pd\n",[221,315,317],{"class":223,"line":316},4,[221,318,320],{"emptyLinePlaceholder":319},true,"\n",[221,322,324,327,330,333,336,339,342],{"class":223,"line":323},5,[221,325,326],{"class":248},"def",[221,328,329],{"class":227}," convert",[221,331,332],{"class":306},"(path: ",[221,334,335],{"class":235},"str",[221,337,338],{"class":306},") -> ",[221,340,341],{"class":235},"None",[221,343,344],{"class":306},":\n",[221,346,348,351,354],{"class":223,"line":347},6,[221,349,350],{"class":306},"    df ",[221,352,353],{"class":248},"=",[221,355,356],{"class":306}," pd.read_csv(path)\n",[221,358,360,363,366,368,371],{"class":223,"line":359},7,[221,361,362],{"class":306},"    df.to_parquet(path.replace(",[221,364,365],{"class":231},"\".csv\"",[221,367,17],{"class":306},[221,369,370],{"class":231},"\".parquet\"",[221,372,373],{"class":306},"))\n",[212,375,377],{"className":282,"code":376,"language":284,"meta":217,"style":217},"# After: pandas loads only when convert() actually runs.\ndef convert(path: str) -> None:\n    import pandas as pd            # deferred to call time\n    df = pd.read_csv(path)\n    df.to_parquet(path.replace(\".csv\", \".parquet\"))\n",[14,378,379,384,400,415,423],{"__ignoreMap":217},[221,380,381],{"class":223,"line":224},[221,382,383],{"class":291},"# After: pandas loads only when convert() actually runs.\n",[221,385,386,388,390,392,394,396,398],{"class":223,"line":295},[221,387,326],{"class":248},[221,389,329],{"class":227},[221,391,332],{"class":306},[221,393,335],{"class":235},[221,395,338],{"class":306},[221,397,341],{"class":235},[221,399,344],{"class":306},[221,401,402,405,407,409,412],{"class":223,"line":301},[221,403,404],{"class":248},"    import",[221,406,307],{"class":306},[221,408,310],{"class":248},[221,410,411],{"class":306}," pd            ",[221,413,414],{"class":291},"# deferred to call time\n",[221,416,417,419,421],{"class":223,"line":316},[221,418,350],{"class":306},[221,420,353],{"class":248},[221,422,356],{"class":306},[221,424,425,427,429,431,433],{"class":223,"line":323},[221,426,362],{"class":306},[221,428,365],{"class":231},[221,430,17],{"class":306},[221,432,370],{"class":231},[221,434,373],{"class":306},[10,436,437,438,441,442,444,445,447],{},"The deferred import still gets cached in ",[14,439,440],{},"sys.modules"," after the first call, so a command that calls ",[14,443,168],{}," in a loop pays the import cost once, not per iteration. This one change often halves ",[14,446,24],{}," time in a CLI that touches data libraries. It reads as unusual to programmers trained to keep imports at the top — but for a CLI, an import inside a function is a deliberate performance tool, not a smell.",[27,449,451],{"id":450},"fix-2-lazy-load-whole-subcommands","Fix 2: lazy-load whole subcommands",[10,453,454,455,458,459,462],{},"Deferring imports inside a function still requires importing the ",[88,456,457],{},"module"," that defines the command, which may itself pull in heavy dependencies at its own top level. The complete fix is to not import a subcommand's module at all until that subcommand is invoked. With Click you do this by subclassing ",[14,460,461],{},"Group"," and overriding command lookup so each subcommand is imported on demand from a string path:",[212,464,466],{"className":282,"code":465,"language":284,"meta":217,"style":217},"import importlib\nimport click\n\nclass LazyGroup(click.Group):\n    def __init__(self, *args, lazy_subcommands: dict[str, str] | None = None, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._lazy = lazy_subcommands or {}   # name -> \"module:attr\"\n\n    def list_commands(self, ctx):\n        return sorted({*super().list_commands(ctx), *self._lazy})\n\n    def get_command(self, ctx, name):\n        if name in self._lazy:\n            module_path, attr = self._lazy[name].split(\":\")\n            return getattr(importlib.import_module(module_path), attr)\n        return super().get_command(ctx, name)\n",[14,467,468,475,482,486,507,552,575,597,602,613,641,646,657,675,694,706],{"__ignoreMap":217},[221,469,470,472],{"class":223,"line":224},[221,471,198],{"class":248},[221,473,474],{"class":306}," importlib\n",[221,476,477,479],{"class":223,"line":295},[221,478,198],{"class":248},[221,480,481],{"class":306}," click\n",[221,483,484],{"class":223,"line":301},[221,485,320],{"emptyLinePlaceholder":319},[221,487,488,491,494,497,500,502,504],{"class":223,"line":316},[221,489,490],{"class":248},"class",[221,492,493],{"class":227}," LazyGroup",[221,495,496],{"class":306},"(",[221,498,499],{"class":227},"click",[221,501,118],{"class":306},[221,503,461],{"class":227},[221,505,506],{"class":306},"):\n",[221,508,509,512,515,518,521,524,526,528,530,533,536,539,542,544,546,549],{"class":223,"line":323},[221,510,511],{"class":248},"    def",[221,513,514],{"class":235}," __init__",[221,516,517],{"class":306},"(self, ",[221,519,520],{"class":248},"*",[221,522,523],{"class":306},"args, lazy_subcommands: dict[",[221,525,335],{"class":235},[221,527,17],{"class":306},[221,529,335],{"class":235},[221,531,532],{"class":306},"] ",[221,534,535],{"class":248},"|",[221,537,538],{"class":235}," None",[221,540,541],{"class":248}," =",[221,543,538],{"class":235},[221,545,17],{"class":306},[221,547,548],{"class":248},"**",[221,550,551],{"class":306},"kwargs):\n",[221,553,554,557,560,563,565,567,570,572],{"class":223,"line":347},[221,555,556],{"class":235},"        super",[221,558,559],{"class":306},"().",[221,561,562],{"class":235},"__init__",[221,564,496],{"class":306},[221,566,520],{"class":248},[221,568,569],{"class":306},"args, ",[221,571,548],{"class":248},[221,573,574],{"class":306},"kwargs)\n",[221,576,577,580,583,585,588,591,594],{"class":223,"line":359},[221,578,579],{"class":235},"        self",[221,581,582],{"class":306},"._lazy ",[221,584,353],{"class":248},[221,586,587],{"class":306}," lazy_subcommands ",[221,589,590],{"class":248},"or",[221,592,593],{"class":306}," {}   ",[221,595,596],{"class":291},"# name -> \"module:attr\"\n",[221,598,600],{"class":223,"line":599},8,[221,601,320],{"emptyLinePlaceholder":319},[221,603,605,607,610],{"class":223,"line":604},9,[221,606,511],{"class":248},[221,608,609],{"class":227}," list_commands",[221,611,612],{"class":306},"(self, ctx):\n",[221,614,616,619,622,625,627,630,633,635,638],{"class":223,"line":615},10,[221,617,618],{"class":248},"        return",[221,620,621],{"class":235}," sorted",[221,623,624],{"class":306},"({",[221,626,520],{"class":248},[221,628,629],{"class":235},"super",[221,631,632],{"class":306},"().list_commands(ctx), ",[221,634,520],{"class":248},[221,636,637],{"class":235},"self",[221,639,640],{"class":306},"._lazy})\n",[221,642,644],{"class":223,"line":643},11,[221,645,320],{"emptyLinePlaceholder":319},[221,647,649,651,654],{"class":223,"line":648},12,[221,650,511],{"class":248},[221,652,653],{"class":227}," get_command",[221,655,656],{"class":306},"(self, ctx, name):\n",[221,658,660,663,666,669,672],{"class":223,"line":659},13,[221,661,662],{"class":248},"        if",[221,664,665],{"class":306}," name ",[221,667,668],{"class":248},"in",[221,670,671],{"class":235}," self",[221,673,674],{"class":306},"._lazy:\n",[221,676,678,681,683,685,688,691],{"class":223,"line":677},14,[221,679,680],{"class":306},"            module_path, attr ",[221,682,353],{"class":248},[221,684,671],{"class":235},[221,686,687],{"class":306},"._lazy[name].split(",[221,689,690],{"class":231},"\":\"",[221,692,693],{"class":306},")\n",[221,695,697,700,703],{"class":223,"line":696},15,[221,698,699],{"class":248},"            return",[221,701,702],{"class":235}," getattr",[221,704,705],{"class":306},"(importlib.import_module(module_path), attr)\n",[221,707,709,711,714],{"class":223,"line":708},16,[221,710,618],{"class":248},[221,712,713],{"class":235}," super",[221,715,716],{"class":306},"().get_command(ctx, name)\n",[10,718,719,720,722,723,726,727,730,731,734,735,737,738,740,741,118],{},"Now ",[14,721,122],{}," calls ",[14,724,725],{},"list_commands"," (cheap — it just lists names) but never ",[14,728,729],{},"get_command",", so no command module is imported. Only ",[14,732,733],{},"mycli convert …"," triggers the import of the ",[14,736,168],{}," module and its ",[14,739,16],{},". The full runnable version, the registry pattern, and the Typer equivalent are in ",[104,742,744],{"href":743},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup\u002F","lazy loading subcommands for faster startup",[10,746,747,748,752],{},"This technique layers cleanly on top of a well-organized command tree. If your CLI is still a single file, restructure it first — ",[104,749,751],{"href":750},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project\u002F","how to structure a large Python CLI project"," covers the package layout that makes per-command lazy loading natural.",[27,754,756],{"id":755},"fix-3-keep-heavy-deps-out-of-your-top-level-package","Fix 3: keep heavy deps out of your top-level package",[10,758,759,760,763,764,767,768,771,772,115,775,778,779,781,782,785],{},"The trap that undoes both fixes above is a heavy import in your package's ",[14,761,762],{},"__init__.py"," or in a shared ",[14,765,766],{},"utils"," module that every command imports. If ",[14,769,770],{},"mycli\u002F__init__.py"," does ",[14,773,774],{},"from .analytics import tracker",[14,776,777],{},"analytics"," imports ",[14,780,16],{},", then ",[88,783,784],{},"importing your package at all"," pays for pandas — lazy subcommands won't save you.",[10,787,788,789,791,792,796],{},"Audit your top-level and shared modules ruthlessly. A package ",[14,790,762],{}," for a CLI should be nearly empty. Push heavy dependencies down into the specific command modules that need them, behind the lazy boundary. The same discipline applies to plugin systems: an ",[104,793,795],{"href":794},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002F","extensible plugin architecture"," already loads each plugin only when needed via entry points, which is lazy loading by another name — don't undo it by importing every plugin eagerly at startup.",[27,798,800],{"id":799},"a-startup-budget-you-enforce-in-ci","A startup budget you enforce in CI",[10,802,803,804,807],{},"Optimizations rot. Someone adds ",[14,805,806],{},"import boto3"," to a shared module six months from now and your instant CLI is slow again. Prevent the regression by encoding a budget as a test:",[212,809,811],{"className":282,"code":810,"language":284,"meta":217,"style":217},"import subprocess\nimport sys\nimport time\n\ndef test_help_is_fast():\n    # Warm the filesystem\u002Fbytecode cache once, then time a clean run.\n    subprocess.run([sys.executable, \"-m\", \"mycli\", \"--help\"], capture_output=True)\n    start = time.perf_counter()\n    subprocess.run([sys.executable, \"-m\", \"mycli\", \"--help\"], capture_output=True)\n    elapsed_ms = (time.perf_counter() - start) * 1000\n    assert elapsed_ms \u003C 150, f\"--help took {elapsed_ms:.0f} ms (budget 150 ms)\"\n",[14,812,813,820,827,834,838,848,853,885,895,919,940],{"__ignoreMap":217},[221,814,815,817],{"class":223,"line":224},[221,816,198],{"class":248},[221,818,819],{"class":306}," subprocess\n",[221,821,822,824],{"class":223,"line":295},[221,823,198],{"class":248},[221,825,826],{"class":306}," sys\n",[221,828,829,831],{"class":223,"line":301},[221,830,198],{"class":248},[221,832,833],{"class":306}," time\n",[221,835,836],{"class":223,"line":316},[221,837,320],{"emptyLinePlaceholder":319},[221,839,840,842,845],{"class":223,"line":323},[221,841,326],{"class":248},[221,843,844],{"class":227}," test_help_is_fast",[221,846,847],{"class":306},"():\n",[221,849,850],{"class":223,"line":347},[221,851,852],{"class":291},"    # Warm the filesystem\u002Fbytecode cache once, then time a clean run.\n",[221,854,855,858,861,863,866,868,871,874,878,880,883],{"class":223,"line":359},[221,856,857],{"class":306},"    subprocess.run([sys.executable, ",[221,859,860],{"class":231},"\"-m\"",[221,862,17],{"class":306},[221,864,865],{"class":231},"\"mycli\"",[221,867,17],{"class":306},[221,869,870],{"class":231},"\"--help\"",[221,872,873],{"class":306},"], ",[221,875,877],{"class":876},"s4XuR","capture_output",[221,879,353],{"class":248},[221,881,882],{"class":235},"True",[221,884,693],{"class":306},[221,886,887,890,892],{"class":223,"line":599},[221,888,889],{"class":306},"    start ",[221,891,353],{"class":248},[221,893,894],{"class":306}," time.perf_counter()\n",[221,896,897,899,901,903,905,907,909,911,913,915,917],{"class":223,"line":604},[221,898,857],{"class":306},[221,900,860],{"class":231},[221,902,17],{"class":306},[221,904,865],{"class":231},[221,906,17],{"class":306},[221,908,870],{"class":231},[221,910,873],{"class":306},[221,912,877],{"class":876},[221,914,353],{"class":248},[221,916,882],{"class":235},[221,918,693],{"class":306},[221,920,921,924,926,929,932,935,937],{"class":223,"line":615},[221,922,923],{"class":306},"    elapsed_ms ",[221,925,353],{"class":248},[221,927,928],{"class":306}," (time.perf_counter() ",[221,930,931],{"class":248},"-",[221,933,934],{"class":306}," start) ",[221,936,520],{"class":248},[221,938,939],{"class":235}," 1000\n",[221,941,942,945,948,951,954,956,959,962,965,968,971,974],{"class":223,"line":643},[221,943,944],{"class":248},"    assert",[221,946,947],{"class":306}," elapsed_ms ",[221,949,950],{"class":248},"\u003C",[221,952,953],{"class":235}," 150",[221,955,17],{"class":306},[221,957,958],{"class":248},"f",[221,960,961],{"class":231},"\"--help took ",[221,963,964],{"class":235},"{",[221,966,967],{"class":306},"elapsed_ms",[221,969,970],{"class":248},":.0f",[221,972,973],{"class":235},"}",[221,975,976],{"class":231}," ms (budget 150 ms)\"\n",[10,978,979,980,118],{},"Give the budget generous headroom over your measured local time — CI machines are slower and noisier — and run it as a normal part of the suite. Now a heavy import added anywhere in the import graph fails a test with a clear message, instead of silently taxing every user. The full budgeting approach, including choosing the number and reducing CI flakiness, is covered in the ",[104,981,982],{"href":270},"profiling guide",[27,984,986],{"id":985},"where-to-go-next","Where to go next",[10,988,989],{},"Work the problem in order: measure, then fix the biggest offenders.",[991,992,993,1010],"ol",{},[35,994,995,1000,1001,17,1004,1006,1007,1009],{},[49,996,997],{},[104,998,999],{"href":270},"Profiling Python CLI startup time"," — find the expensive imports with ",[14,1002,1003],{},"-X importtime",[14,1005,262],{},", and ",[14,1008,266],{},", and set the budget.",[35,1011,1012,1017,1018,1021],{},[49,1013,1014],{},[104,1015,1016],{"href":743},"Lazy loading subcommands for faster startup"," — the full ",[14,1019,1020],{},"LazyGroup"," recipe and Typer notes to defer whole commands.",[27,1023,1025],{"id":1024},"related","Related",[32,1027,1028,1035,1040,1045,1052],{},[35,1029,1030,1034],{},[104,1031,1033],{"href":1032},"\u002Fmodern-python-cli-frameworks-architecture\u002F","Modern Python CLI Frameworks & Architecture"," — the track this section belongs to.",[35,1036,1037,1039],{},[104,1038,999],{"href":270}," — measure before you optimize.",[35,1041,1042,1044],{},[104,1043,1016],{"href":743}," — the deep recipe for deferring command imports.",[35,1046,1047,1051],{},[104,1048,1050],{"href":1049},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","Structuring multi-command Python CLIs"," — the command layout lazy loading builds on.",[35,1053,1054,1057],{},[104,1055,1056],{"href":794},"Plugin architectures for extensible CLIs"," — entry-point loading that is already lazy by design.",[1059,1060,1061],"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":217,"searchDepth":295,"depth":295,"links":1063},[1064,1065,1066,1067,1068,1069,1070,1071,1072,1073],{"id":29,"depth":295,"text":30},{"id":82,"depth":295,"text":83},{"id":142,"depth":295,"text":143},{"id":206,"depth":295,"text":207},{"id":275,"depth":295,"text":276},{"id":450,"depth":295,"text":451},{"id":755,"depth":295,"text":756},{"id":799,"depth":295,"text":800},{"id":985,"depth":295,"text":986},{"id":1024,"depth":295,"text":1025},"2026-07-05","Diagnose and fix slow Python CLI startup: measure import cost, defer heavy imports, lazy-load subcommands, and keep --help and completion instant.","advanced",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading",{"title":5,"description":1075},"modern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Findex",[1084,1085,1086,1087,1088],"performance","startup","lazy-loading","cli","completion","zeDgMPubgc0oq4rAL1NqXtFT5KKQT2iSGJTn4pqhcQw",[1091,1094,1097,1100,1103,1106,1109,1112,1115,1118,1121,1124,1127,1130,1133,1136,1139,1142,1145,1148,1149,1152,1155,1158,1161,1164,1167,1170,1173,1176,1179,1182,1185,1188,1191,1194,1197,1200,1203,1206,1209,1212,1215,1218,1221,1224,1227,1230,1233,1236,1239],{"path":1092,"title":1093},"\u002Fabout","About Python CLI Toolcraft",{"path":1095,"title":1096},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1098,"title":1099},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1101,"title":1102},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1104,"title":1105},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1107,"title":1108},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1110,"title":1111},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1113,"title":1114},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1116,"title":1117},"\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":1119,"title":1120},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1122,"title":1123},"\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":1125,"title":1126},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1128,"title":1129},"\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":1131,"title":1132},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1134,"title":1135},"\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":1137,"title":1138},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1140,"title":1141},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1143,"title":1144},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1146,"title":1147},"\u002F","Python CLI Toolcraft",{"path":1080,"title":5},{"path":1150,"title":1151},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1153,"title":1154},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1156,"title":1157},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1159,"title":1160},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1162,"title":1163},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1165,"title":1166},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1168,"title":1169},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1171,"title":1172},"\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":1174,"title":1175},"\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":1177,"title":1178},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1180,"title":1181},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1183,"title":1184},"\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":1186,"title":1187},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1189,"title":1190},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1192,"title":1193},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1195,"title":1196},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1198,"title":1199},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1201,"title":1202},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1204,"title":1205},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1207,"title":1208},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1210,"title":1211},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1213,"title":1214},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1216,"title":1217},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1219,"title":1220},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1222,"title":1223},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1225,"title":1226},"\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":1228,"title":1229},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1231,"title":1232},"\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":1234,"title":1235},"\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":1237,"title":1238},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1240,"title":1241},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867197]