[{"data":1,"prerenderedAt":1814},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F":3,"content-directory":1566},{"id":4,"title":5,"body":6,"date":1549,"description":1550,"difficulty":1551,"draft":1552,"extension":1553,"meta":1554,"navigation":322,"path":1555,"seo":1556,"stem":1557,"tags":1558,"updated":1564,"__hash__":1565},"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":1525},"minimark",[9,26,31,76,80,84,95,137,140,144,163,166,192,207,211,214,256,276,280,283,377,438,451,455,466,720,748,756,760,789,800,804,811,814,983,989,993,996,1028,1032,1035,1146,1164,1169,1175,1179,1215,1218,1224,1227,1253,1257,1263,1319,1322,1376,1397,1410,1414,1419,1425,1429,1436,1440,1443,1447,1458,1462,1471,1475,1478,1482,1485,1489,1521],[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.",[77,164],{"name":165},"startup-time-breakdown",[10,167,168,169,172,173,175,176,172,179,181,182,172,185,188,189,191],{},"Consider a CLI with ",[14,170,171],{},"convert"," (needs ",[14,174,16],{},"), ",[14,177,178],{},"fetch",[14,180,20],{},"), and ",[14,183,184],{},"report",[14,186,187],{},"matplotlib","). If all three command modules are imported when the app is constructed, then ",[14,190,122],{}," pays for all three. The user asked for one line of help and got charged for the entire dependency tree.",[10,193,194,195,198,199,202,203,206],{},"The insight most people miss: ",[49,196,197],{},"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,200,201],{},"import"," statements in third-party packages you don't control. That is why the fix is about ",[88,204,205],{},"when"," imports happen, not about making anything faster.",[27,208,210],{"id":209},"the-measure-first-rule","The measure-first rule",[10,212,213],{},"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:",[215,216,221],"pre",{"className":217,"code":218,"language":219,"meta":220,"style":220},"language-bash shiki shiki-themes github-light github-dark","$ python -X importtime -c \"import mycli.cli\" 2> importtime.log\n","bash","",[14,222,223],{"__ignoreMap":220},[224,225,228,232,236,240,243,246,249,253],"span",{"class":226,"line":227},"line",1,[224,229,231],{"class":230},"sScJk","$",[224,233,235],{"class":234},"sZZnC"," python",[224,237,239],{"class":238},"sj4cs"," -X",[224,241,242],{"class":234}," importtime",[224,244,245],{"class":238}," -c",[224,247,248],{"class":234}," \"import mycli.cli\"",[224,250,252],{"class":251},"szBVR"," 2>",[224,254,255],{"class":234}," importtime.log\n",[10,257,258,259,262,263,266,267,270,271,275],{},"The ",[14,260,261],{},"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,264,265],{},"tuna",", timing the real command with ",[14,268,269],{},"hyperfine",", and spotting the usual offenders — is a topic on its own: ",[104,272,274],{"href":273},"\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,277,279],{"id":278},"fix-1-defer-module-level-imports","Fix 1: defer module-level imports",[10,281,282],{},"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:",[215,284,288],{"className":285,"code":286,"language":287,"meta":220,"style":220},"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,289,290,296,302,317,324,348,360],{"__ignoreMap":220},[224,291,292],{"class":226,"line":227},[224,293,295],{"class":294},"sJ8bj","# Before: pandas loads the moment this module is imported,\n",[224,297,299],{"class":226,"line":298},2,[224,300,301],{"class":294},"# i.e. every time the CLI starts, for every command.\n",[224,303,305,307,311,314],{"class":226,"line":304},3,[224,306,201],{"class":251},[224,308,310],{"class":309},"sVt8B"," pandas ",[224,312,313],{"class":251},"as",[224,315,316],{"class":309}," pd\n",[224,318,320],{"class":226,"line":319},4,[224,321,323],{"emptyLinePlaceholder":322},true,"\n",[224,325,327,330,333,336,339,342,345],{"class":226,"line":326},5,[224,328,329],{"class":251},"def",[224,331,332],{"class":230}," convert",[224,334,335],{"class":309},"(path: ",[224,337,338],{"class":238},"str",[224,340,341],{"class":309},") -> ",[224,343,344],{"class":238},"None",[224,346,347],{"class":309},":\n",[224,349,351,354,357],{"class":226,"line":350},6,[224,352,353],{"class":309},"    df ",[224,355,356],{"class":251},"=",[224,358,359],{"class":309}," pd.read_csv(path)\n",[224,361,363,366,369,371,374],{"class":226,"line":362},7,[224,364,365],{"class":309},"    df.to_parquet(path.replace(",[224,367,368],{"class":234},"\".csv\"",[224,370,17],{"class":309},[224,372,373],{"class":234},"\".parquet\"",[224,375,376],{"class":309},"))\n",[215,378,380],{"className":285,"code":379,"language":287,"meta":220,"style":220},"# 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,381,382,387,403,418,426],{"__ignoreMap":220},[224,383,384],{"class":226,"line":227},[224,385,386],{"class":294},"# After: pandas loads only when convert() actually runs.\n",[224,388,389,391,393,395,397,399,401],{"class":226,"line":298},[224,390,329],{"class":251},[224,392,332],{"class":230},[224,394,335],{"class":309},[224,396,338],{"class":238},[224,398,341],{"class":309},[224,400,344],{"class":238},[224,402,347],{"class":309},[224,404,405,408,410,412,415],{"class":226,"line":304},[224,406,407],{"class":251},"    import",[224,409,310],{"class":309},[224,411,313],{"class":251},[224,413,414],{"class":309}," pd            ",[224,416,417],{"class":294},"# deferred to call time\n",[224,419,420,422,424],{"class":226,"line":319},[224,421,353],{"class":309},[224,423,356],{"class":251},[224,425,359],{"class":309},[224,427,428,430,432,434,436],{"class":226,"line":326},[224,429,365],{"class":309},[224,431,368],{"class":234},[224,433,17],{"class":309},[224,435,373],{"class":234},[224,437,376],{"class":309},[10,439,440,441,444,445,447,448,450],{},"The deferred import still gets cached in ",[14,442,443],{},"sys.modules"," after the first call, so a command that calls ",[14,446,171],{}," in a loop pays the import cost once, not per iteration. This one change often halves ",[14,449,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,452,454],{"id":453},"fix-2-lazy-load-whole-subcommands","Fix 2: lazy-load whole subcommands",[10,456,457,458,461,462,465],{},"Deferring imports inside a function still requires importing the ",[88,459,460],{},"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,463,464],{},"Group"," and overriding command lookup so each subcommand is imported on demand from a string path:",[215,467,469],{"className":285,"code":468,"language":287,"meta":220,"style":220},"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,470,471,478,485,489,510,555,578,600,605,616,644,649,660,678,697,709],{"__ignoreMap":220},[224,472,473,475],{"class":226,"line":227},[224,474,201],{"class":251},[224,476,477],{"class":309}," importlib\n",[224,479,480,482],{"class":226,"line":298},[224,481,201],{"class":251},[224,483,484],{"class":309}," click\n",[224,486,487],{"class":226,"line":304},[224,488,323],{"emptyLinePlaceholder":322},[224,490,491,494,497,500,503,505,507],{"class":226,"line":319},[224,492,493],{"class":251},"class",[224,495,496],{"class":230}," LazyGroup",[224,498,499],{"class":309},"(",[224,501,502],{"class":230},"click",[224,504,118],{"class":309},[224,506,464],{"class":230},[224,508,509],{"class":309},"):\n",[224,511,512,515,518,521,524,527,529,531,533,536,539,542,545,547,549,552],{"class":226,"line":326},[224,513,514],{"class":251},"    def",[224,516,517],{"class":238}," __init__",[224,519,520],{"class":309},"(self, ",[224,522,523],{"class":251},"*",[224,525,526],{"class":309},"args, lazy_subcommands: dict[",[224,528,338],{"class":238},[224,530,17],{"class":309},[224,532,338],{"class":238},[224,534,535],{"class":309},"] ",[224,537,538],{"class":251},"|",[224,540,541],{"class":238}," None",[224,543,544],{"class":251}," =",[224,546,541],{"class":238},[224,548,17],{"class":309},[224,550,551],{"class":251},"**",[224,553,554],{"class":309},"kwargs):\n",[224,556,557,560,563,566,568,570,573,575],{"class":226,"line":350},[224,558,559],{"class":238},"        super",[224,561,562],{"class":309},"().",[224,564,565],{"class":238},"__init__",[224,567,499],{"class":309},[224,569,523],{"class":251},[224,571,572],{"class":309},"args, ",[224,574,551],{"class":251},[224,576,577],{"class":309},"kwargs)\n",[224,579,580,583,586,588,591,594,597],{"class":226,"line":362},[224,581,582],{"class":238},"        self",[224,584,585],{"class":309},"._lazy ",[224,587,356],{"class":251},[224,589,590],{"class":309}," lazy_subcommands ",[224,592,593],{"class":251},"or",[224,595,596],{"class":309}," {}   ",[224,598,599],{"class":294},"# name -> \"module:attr\"\n",[224,601,603],{"class":226,"line":602},8,[224,604,323],{"emptyLinePlaceholder":322},[224,606,608,610,613],{"class":226,"line":607},9,[224,609,514],{"class":251},[224,611,612],{"class":230}," list_commands",[224,614,615],{"class":309},"(self, ctx):\n",[224,617,619,622,625,628,630,633,636,638,641],{"class":226,"line":618},10,[224,620,621],{"class":251},"        return",[224,623,624],{"class":238}," sorted",[224,626,627],{"class":309},"({",[224,629,523],{"class":251},[224,631,632],{"class":238},"super",[224,634,635],{"class":309},"().list_commands(ctx), ",[224,637,523],{"class":251},[224,639,640],{"class":238},"self",[224,642,643],{"class":309},"._lazy})\n",[224,645,647],{"class":226,"line":646},11,[224,648,323],{"emptyLinePlaceholder":322},[224,650,652,654,657],{"class":226,"line":651},12,[224,653,514],{"class":251},[224,655,656],{"class":230}," get_command",[224,658,659],{"class":309},"(self, ctx, name):\n",[224,661,663,666,669,672,675],{"class":226,"line":662},13,[224,664,665],{"class":251},"        if",[224,667,668],{"class":309}," name ",[224,670,671],{"class":251},"in",[224,673,674],{"class":238}," self",[224,676,677],{"class":309},"._lazy:\n",[224,679,681,684,686,688,691,694],{"class":226,"line":680},14,[224,682,683],{"class":309},"            module_path, attr ",[224,685,356],{"class":251},[224,687,674],{"class":238},[224,689,690],{"class":309},"._lazy[name].split(",[224,692,693],{"class":234},"\":\"",[224,695,696],{"class":309},")\n",[224,698,700,703,706],{"class":226,"line":699},15,[224,701,702],{"class":251},"            return",[224,704,705],{"class":238}," getattr",[224,707,708],{"class":309},"(importlib.import_module(module_path), attr)\n",[224,710,712,714,717],{"class":226,"line":711},16,[224,713,621],{"class":251},[224,715,716],{"class":238}," super",[224,718,719],{"class":309},"().get_command(ctx, name)\n",[10,721,722,723,725,726,729,730,733,734,737,738,740,741,743,744,118],{},"Now ",[14,724,122],{}," calls ",[14,727,728],{},"list_commands"," (cheap — it just lists names) but never ",[14,731,732],{},"get_command",", so no command module is imported. Only ",[14,735,736],{},"mycli convert …"," triggers the import of the ",[14,739,171],{}," module and its ",[14,742,16],{},". The full runnable version, the registry pattern, and the Typer equivalent are in ",[104,745,747],{"href":746},"\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,749,750,751,755],{},"This technique layers cleanly on top of a well-organized command tree. If your CLI is still a single file, restructure it first — ",[104,752,754],{"href":753},"\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,757,759],{"id":758},"fix-3-keep-heavy-deps-out-of-your-top-level-package","Fix 3: keep heavy deps out of your top-level package",[10,761,762,763,766,767,770,771,774,775,115,778,781,782,784,785,788],{},"The trap that undoes both fixes above is a heavy import in your package's ",[14,764,765],{},"__init__.py"," or in a shared ",[14,768,769],{},"utils"," module that every command imports. If ",[14,772,773],{},"mycli\u002F__init__.py"," does ",[14,776,777],{},"from .analytics import tracker",[14,779,780],{},"analytics"," imports ",[14,783,16],{},", then ",[88,786,787],{},"importing your package at all"," pays for pandas — lazy subcommands won't save you.",[10,790,791,792,794,795,799],{},"Audit your top-level and shared modules ruthlessly. A package ",[14,793,765],{}," 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,796,798],{"href":797},"\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,801,803],{"id":802},"a-startup-budget-you-enforce-in-ci","A startup budget you enforce in CI",[10,805,806,807,810],{},"Optimizations rot. Someone adds ",[14,808,809],{},"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:",[77,812],{"name":813},"startup-budget-ci",[215,815,817],{"className":285,"code":816,"language":287,"meta":220,"style":220},"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,818,819,826,833,840,844,854,859,891,901,925,946],{"__ignoreMap":220},[224,820,821,823],{"class":226,"line":227},[224,822,201],{"class":251},[224,824,825],{"class":309}," subprocess\n",[224,827,828,830],{"class":226,"line":298},[224,829,201],{"class":251},[224,831,832],{"class":309}," sys\n",[224,834,835,837],{"class":226,"line":304},[224,836,201],{"class":251},[224,838,839],{"class":309}," time\n",[224,841,842],{"class":226,"line":319},[224,843,323],{"emptyLinePlaceholder":322},[224,845,846,848,851],{"class":226,"line":326},[224,847,329],{"class":251},[224,849,850],{"class":230}," test_help_is_fast",[224,852,853],{"class":309},"():\n",[224,855,856],{"class":226,"line":350},[224,857,858],{"class":294},"    # Warm the filesystem\u002Fbytecode cache once, then time a clean run.\n",[224,860,861,864,867,869,872,874,877,880,884,886,889],{"class":226,"line":362},[224,862,863],{"class":309},"    subprocess.run([sys.executable, ",[224,865,866],{"class":234},"\"-m\"",[224,868,17],{"class":309},[224,870,871],{"class":234},"\"mycli\"",[224,873,17],{"class":309},[224,875,876],{"class":234},"\"--help\"",[224,878,879],{"class":309},"], ",[224,881,883],{"class":882},"s4XuR","capture_output",[224,885,356],{"class":251},[224,887,888],{"class":238},"True",[224,890,696],{"class":309},[224,892,893,896,898],{"class":226,"line":602},[224,894,895],{"class":309},"    start ",[224,897,356],{"class":251},[224,899,900],{"class":309}," time.perf_counter()\n",[224,902,903,905,907,909,911,913,915,917,919,921,923],{"class":226,"line":607},[224,904,863],{"class":309},[224,906,866],{"class":234},[224,908,17],{"class":309},[224,910,871],{"class":234},[224,912,17],{"class":309},[224,914,876],{"class":234},[224,916,879],{"class":309},[224,918,883],{"class":882},[224,920,356],{"class":251},[224,922,888],{"class":238},[224,924,696],{"class":309},[224,926,927,930,932,935,938,941,943],{"class":226,"line":618},[224,928,929],{"class":309},"    elapsed_ms ",[224,931,356],{"class":251},[224,933,934],{"class":309}," (time.perf_counter() ",[224,936,937],{"class":251},"-",[224,939,940],{"class":309}," start) ",[224,942,523],{"class":251},[224,944,945],{"class":238}," 1000\n",[224,947,948,951,954,957,960,962,965,968,971,974,977,980],{"class":226,"line":646},[224,949,950],{"class":251},"    assert",[224,952,953],{"class":309}," elapsed_ms ",[224,955,956],{"class":251},"\u003C",[224,958,959],{"class":238}," 150",[224,961,17],{"class":309},[224,963,964],{"class":251},"f",[224,966,967],{"class":234},"\"--help took ",[224,969,970],{"class":238},"{",[224,972,973],{"class":309},"elapsed_ms",[224,975,976],{"class":251},":.0f",[224,978,979],{"class":238},"}",[224,981,982],{"class":234}," ms (budget 150 ms)\"\n",[10,984,985,986,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,987,988],{"href":273},"profiling guide",[27,990,992],{"id":991},"where-to-go-next","Where to go next",[10,994,995],{},"Work the problem in order: measure, then fix the biggest offenders.",[997,998,999,1016],"ol",{},[35,1000,1001,1006,1007,17,1010,1012,1013,1015],{},[49,1002,1003],{},[104,1004,1005],{"href":273},"Profiling Python CLI startup time"," — find the expensive imports with ",[14,1008,1009],{},"-X importtime",[14,1011,265],{},", and ",[14,1014,269],{},", and set the budget.",[35,1017,1018,1023,1024,1027],{},[49,1019,1020],{},[104,1021,1022],{"href":746},"Lazy loading subcommands for faster startup"," — the full ",[14,1025,1026],{},"LazyGroup"," recipe and Typer notes to defer whole commands.",[27,1029,1031],{"id":1030},"reading-a-measurement-properly","Reading a measurement properly",[10,1033,1034],{},"Before changing anything, get a number you trust. Three tools answer three different questions, and\nusing the wrong one produces confident nonsense.",[215,1036,1038],{"className":217,"code":1037,"language":219,"meta":220,"style":220},"# what did each import cost?\npython -X importtime -m mytool --help 2>&1 | sort -k2 -rn | head -12\n\n# how long does it take in reality, with noise averaged out?\nhyperfine --warmup 3 'mytool --help' 'mytool sync --dry-run .\u002Fdata'\n\n# where is the time inside my own code?\npython -X importtime -m mytool --help 2> import.log && tuna import.log\n",[14,1039,1040,1045,1085,1089,1094,1110,1114,1119],{"__ignoreMap":220},[224,1041,1042],{"class":226,"line":227},[224,1043,1044],{"class":294},"# what did each import cost?\n",[224,1046,1047,1049,1051,1053,1056,1059,1062,1065,1068,1071,1074,1077,1079,1082],{"class":226,"line":298},[224,1048,287],{"class":230},[224,1050,239],{"class":238},[224,1052,242],{"class":234},[224,1054,1055],{"class":238}," -m",[224,1057,1058],{"class":234}," mytool",[224,1060,1061],{"class":238}," --help",[224,1063,1064],{"class":251}," 2>&1",[224,1066,1067],{"class":251}," |",[224,1069,1070],{"class":230}," sort",[224,1072,1073],{"class":238}," -k2",[224,1075,1076],{"class":238}," -rn",[224,1078,1067],{"class":251},[224,1080,1081],{"class":230}," head",[224,1083,1084],{"class":238}," -12\n",[224,1086,1087],{"class":226,"line":304},[224,1088,323],{"emptyLinePlaceholder":322},[224,1090,1091],{"class":226,"line":319},[224,1092,1093],{"class":294},"# how long does it take in reality, with noise averaged out?\n",[224,1095,1096,1098,1101,1104,1107],{"class":226,"line":326},[224,1097,269],{"class":230},[224,1099,1100],{"class":238}," --warmup",[224,1102,1103],{"class":238}," 3",[224,1105,1106],{"class":234}," 'mytool --help'",[224,1108,1109],{"class":234}," 'mytool sync --dry-run .\u002Fdata'\n",[224,1111,1112],{"class":226,"line":350},[224,1113,323],{"emptyLinePlaceholder":322},[224,1115,1116],{"class":226,"line":362},[224,1117,1118],{"class":294},"# where is the time inside my own code?\n",[224,1120,1121,1123,1125,1127,1129,1131,1133,1135,1138,1141,1143],{"class":226,"line":602},[224,1122,287],{"class":230},[224,1124,239],{"class":238},[224,1126,242],{"class":234},[224,1128,1055],{"class":238},[224,1130,1058],{"class":234},[224,1132,1061],{"class":238},[224,1134,252],{"class":251},[224,1136,1137],{"class":234}," import.log",[224,1139,1140],{"class":309}," && ",[224,1142,265],{"class":230},[224,1144,1145],{"class":234}," import.log\n",[10,1147,1148,1149,1152,1153,1155,1156,1159,1160,1163],{},"Two details make ",[14,1150,1151],{},"importtime"," output readable. The ",[49,1154,261],{}," column is the one that matters —\nit includes everything that import pulled in transitively, which is the subtree you would remove\nby deferring it. The self column is only interesting once you have found the subtree. And the\noutput is on ",[49,1157,1158],{},"stderr",", hence the ",[14,1161,1162],{},"2>&1","; piping stdout alone gives you nothing.",[10,1165,1166,1168],{},[14,1167,269],{}," is the check that a change actually helped. Import counters measure work, not\nwall-clock time, and a change that removes 40 ms of import can be swamped by something else. It\nalso warms the page cache, which removes the largest source of run-to-run variance.",[10,1170,1171,1172,1174],{},"Measure the invocation users actually make most often. For most tools that is ",[14,1173,24],{}," or a fast\nsubcommand, not the heavy one — the heavy command is dominated by its own work, while the light\nones are pure overhead.",[27,1176,1178],{"id":1177},"the-fixed-floor-and-what-sits-above-it","The fixed floor, and what sits above it",[215,1180,1182],{"className":217,"code":1181,"language":219,"meta":220,"style":220},"$ hyperfine --warmup 3 'python -c pass'\n  Time (mean ± σ):      19.8 ms ±   0.7 ms\n",[14,1183,1184,1198],{"__ignoreMap":220},[224,1185,1186,1188,1191,1193,1195],{"class":226,"line":227},[224,1187,231],{"class":230},[224,1189,1190],{"class":234}," hyperfine",[224,1192,1100],{"class":238},[224,1194,1103],{"class":238},[224,1196,1197],{"class":234}," 'python -c pass'\n",[224,1199,1200,1203,1206,1209,1212],{"class":226,"line":298},[224,1201,1202],{"class":230},"  Time",[224,1204,1205],{"class":309}," (mean ",[224,1207,1208],{"class":234},"±",[224,1210,1211],{"class":234}," σ",[224,1213,1214],{"class":309},"):      19.8 ms ±   0.7 ms\n",[10,1216,1217],{},"Roughly 20 ms of interpreter start-up is not yours to optimise. Everything above it is: your\nimports, your parser construction, and any work that happens at module level.",[10,1219,1220,1221,1223],{},"Parser construction is almost never the problem. Building a command tree with thirty commands\ncosts single-digit milliseconds; a single ",[14,1222,161],{}," costs ten times that. When a CLI feels\nslow, the answer is an import, and the only question is which one.",[10,1225,1226],{},"That leads to a simple triage:",[997,1228,1229,1239,1244],{},[35,1230,1231,1232,115,1235,1238],{},"If ",[14,1233,1234],{},"python -c pass",[14,1236,1237],{},"mytool --help"," are close, there is nothing to fix.",[35,1240,1231,1241,1243],{},[14,1242,24],{}," is slow, something heavy is imported at module level in a path that runs always.",[35,1245,1231,1246,1248,1249,1252],{},[14,1247,24],{}," is fast but one command is slow to ",[88,1250,1251],{},"start",", that command imports something heavy —\nwhich is fine, as long as it is inside the function.",[27,1254,1256],{"id":1255},"the-three-fixes-in-order-of-value","The three fixes, in order of value",[10,1258,1259,1262],{},[49,1260,1261],{},"Defer heavy imports into the functions that need them."," This is most of the win, and it is a\ntwo-line change:",[215,1264,1266],{"className":285,"code":1265,"language":287,"meta":220,"style":220},"def report(period: str) -> None:\n    import pandas as pd          # 250 ms, and only this command needs it\n\n    frame = pd.DataFrame(core.collect(period))\n    ...\n",[14,1267,1268,1286,1300,1304,1314],{"__ignoreMap":220},[224,1269,1270,1272,1275,1278,1280,1282,1284],{"class":226,"line":227},[224,1271,329],{"class":251},[224,1273,1274],{"class":230}," report",[224,1276,1277],{"class":309},"(period: ",[224,1279,338],{"class":238},[224,1281,341],{"class":309},[224,1283,344],{"class":238},[224,1285,347],{"class":309},[224,1287,1288,1290,1292,1294,1297],{"class":226,"line":298},[224,1289,407],{"class":251},[224,1291,310],{"class":309},[224,1293,313],{"class":251},[224,1295,1296],{"class":309}," pd          ",[224,1298,1299],{"class":294},"# 250 ms, and only this command needs it\n",[224,1301,1302],{"class":226,"line":304},[224,1303,323],{"emptyLinePlaceholder":322},[224,1305,1306,1309,1311],{"class":226,"line":319},[224,1307,1308],{"class":309},"    frame ",[224,1310,356],{"class":251},[224,1312,1313],{"class":309}," pd.DataFrame(core.collect(period))\n",[224,1315,1316],{"class":226,"line":326},[224,1317,1318],{"class":238},"    ...\n",[10,1320,1321],{},"The cost is that a broken dependency now surfaces when that command runs rather than at start-up,\nso keep an import-everything test in CI:",[215,1323,1325],{"className":285,"code":1324,"language":287,"meta":220,"style":220},"def test_all_command_modules_import():\n    for module in pkgutil.iter_modules(mytool.commands.__path__):\n        importlib.import_module(f\"mytool.commands.{module.name}\")\n",[14,1326,1327,1336,1354],{"__ignoreMap":220},[224,1328,1329,1331,1334],{"class":226,"line":227},[224,1330,329],{"class":251},[224,1332,1333],{"class":230}," test_all_command_modules_import",[224,1335,853],{"class":309},[224,1337,1338,1341,1344,1346,1349,1352],{"class":226,"line":298},[224,1339,1340],{"class":251},"    for",[224,1342,1343],{"class":309}," module ",[224,1345,671],{"class":251},[224,1347,1348],{"class":309}," pkgutil.iter_modules(mytool.commands.",[224,1350,1351],{"class":238},"__path__",[224,1353,509],{"class":309},[224,1355,1356,1359,1361,1364,1366,1369,1371,1374],{"class":226,"line":304},[224,1357,1358],{"class":309},"        importlib.import_module(",[224,1360,964],{"class":251},[224,1362,1363],{"class":234},"\"mytool.commands.",[224,1365,970],{"class":238},[224,1367,1368],{"class":309},"module.name",[224,1370,979],{"class":238},[224,1372,1373],{"class":234},"\"",[224,1375,696],{"class":309},[10,1377,1378,1381,1382,1385,1386,1389,1390,1393,1394,118],{},[49,1379,1380],{},"Keep the top-level package empty."," ",[14,1383,1384],{},"mytool\u002F__init__.py"," runs on ",[88,1387,1388],{},"every"," import of anything\ninside the package, including the entry point. Re-exporting a convenient API there — ",[14,1391,1392],{},"from .core.sync import sync_directory"," — quietly makes every invocation pay for the whole tree. Leave it\nempty, or limited to ",[14,1395,1396],{},"__version__",[10,1398,1399,1402,1403,1405,1406,1409],{},[49,1400,1401],{},"Resolve command modules on demand."," Once the first two are done, what remains is the cost of\nimporting every command module to build ",[14,1404,24],{},". A registry of dotted paths, resolved when a\ncommand is actually chosen, removes it; the\n",[104,1407,1408],{"href":746},"lazy loading guide","\nhas the implementation.",[27,1411,1413],{"id":1412},"frequently-asked-questions","Frequently asked questions",[1415,1416,1418],"h3",{"id":1417},"what-is-a-good-startup-target","What is a good startup target?",[10,1420,1421,1422,1424],{},"Under 100 ms for ",[14,1423,24],{}," feels instant; past roughly 250 ms interactive use starts to feel\nsluggish, and by 500 ms people stop using tab completion — which re-invokes your program on every\nkeystroke. Those numbers matter more for a tool run dozens of times a day than for one run nightly\nby a scheduler.",[1415,1426,1428],{"id":1427},"does-lazy-importing-hurt-readability","Does lazy importing hurt readability?",[10,1430,1431,1432,1435],{},"A little, and it is worth being deliberate: put the import at the top of the function with a short\ncomment saying why it is there, rather than scattering imports mid-body. Linters will flag\nnon-top-level imports, so configure the rule rather than fighting it — ",[14,1433,1434],{},"PLC0415"," in Ruff, if you\nrun that rule set.",[1415,1437,1439],{"id":1438},"can-i-cache-the-parser-between-runs","Can I cache the parser between runs?",[10,1441,1442],{},"No, and it is not worth trying. Each invocation is a fresh process, so anything cached must be\nserialised and re-read, which costs more than building the tree. The only durable win is not doing\nthe work at all, which is what lazy resolution achieves.",[1415,1444,1446],{"id":1445},"why-is-my-tool-slow-only-on-the-first-run","Why is my tool slow only on the first run?",[10,1448,1449,1450,1453,1454,1457],{},"Cold page cache, and sometimes a cold ",[14,1451,1452],{},"__pycache__",". The first import of a module compiles it to\nbytecode; subsequent runs read the cached version. ",[14,1455,1456],{},"hyperfine --warmup 3"," measures the steady state,\nwhich is what users experience after the first invocation — but if your tool is run once per CI job\nin a fresh container, the cold number is the honest one.",[1415,1459,1461],{"id":1460},"does-the-number-of-dependencies-matter-or-just-their-size","Does the number of dependencies matter, or just their size?",[10,1463,1464,1465,1467,1468,1470],{},"Their import cost, which correlates with size but not perfectly. A large package that imports\nlazily internally may cost less than a small one that pulls in ",[14,1466,20],{}," at module level.\n",[14,1469,1009],{}," tells you the truth for your specific tree, which is why measuring beats\nreasoning about it.",[1415,1472,1474],{"id":1473},"should-i-worry-about-start-up-for-a-tool-run-in-a-loop","Should I worry about start-up for a tool run in a loop?",[10,1476,1477],{},"That is precisely when it matters most. A 500 ms start-up over a thousand-file loop is more than\neight minutes of pure overhead. If a tool is likely to be called that way, the better answer is\noften to accept a list of inputs — one invocation processing a thousand files instead of a thousand\ninvocations processing one.",[1415,1479,1481],{"id":1480},"is-a-compiled-or-frozen-build-worth-it-for-speed","Is a compiled or frozen build worth it for speed?",[10,1483,1484],{},"Rarely, because it does not remove the interpreter start-up or the imports — a PyInstaller bundle\nusually starts slower than an installed package, since it unpacks before running. Freezing solves\ndistribution to machines without Python, not latency. If start-up is the problem, the fix is still\nthe imports — measure the frozen build before assuming otherwise.",[27,1486,1488],{"id":1487},"related","Related",[32,1490,1491,1498,1503,1508,1515],{},[35,1492,1493,1497],{},[104,1494,1496],{"href":1495},"\u002Fmodern-python-cli-frameworks-architecture\u002F","Modern Python CLI Frameworks & Architecture"," — the track this section belongs to.",[35,1499,1500,1502],{},[104,1501,1005],{"href":273}," — measure before you optimize.",[35,1504,1505,1507],{},[104,1506,1022],{"href":746}," — the deep recipe for deferring command imports.",[35,1509,1510,1514],{},[104,1511,1513],{"href":1512},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","Structuring multi-command Python CLIs"," — the command layout lazy loading builds on.",[35,1516,1517,1520],{},[104,1518,1519],{"href":797},"Plugin architectures for extensible CLIs"," — entry-point loading that is already lazy by design.",[1522,1523,1524],"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":220,"searchDepth":298,"depth":298,"links":1526},[1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1548],{"id":29,"depth":298,"text":30},{"id":82,"depth":298,"text":83},{"id":142,"depth":298,"text":143},{"id":209,"depth":298,"text":210},{"id":278,"depth":298,"text":279},{"id":453,"depth":298,"text":454},{"id":758,"depth":298,"text":759},{"id":802,"depth":298,"text":803},{"id":991,"depth":298,"text":992},{"id":1030,"depth":298,"text":1031},{"id":1177,"depth":298,"text":1178},{"id":1255,"depth":298,"text":1256},{"id":1412,"depth":298,"text":1413,"children":1540},[1541,1542,1543,1544,1545,1546,1547],{"id":1417,"depth":304,"text":1418},{"id":1427,"depth":304,"text":1428},{"id":1438,"depth":304,"text":1439},{"id":1445,"depth":304,"text":1446},{"id":1460,"depth":304,"text":1461},{"id":1473,"depth":304,"text":1474},{"id":1480,"depth":304,"text":1481},{"id":1487,"depth":298,"text":1488},"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":1550},"modern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Findex",[1559,1560,1561,1562,1563],"performance","startup","lazy-loading","cli","completion","2026-08-01","dolqe5qge5cfN8nX-nGoOmZNXcADsuX5t8g-D4hVALg",[1567,1570,1573,1576,1579,1582,1585,1588,1591,1594,1597,1600,1603,1606,1609,1612,1615,1618,1621,1624,1627,1630,1633,1636,1639,1642,1645,1648,1651,1654,1657,1660,1663,1664,1667,1670,1673,1676,1679,1682,1685,1688,1691,1694,1697,1700,1703,1706,1709,1712,1715,1718,1721,1724,1727,1730,1733,1736,1739,1742,1745,1748,1751,1754,1757,1760,1763,1766,1769,1772,1775,1778,1781,1784,1787,1790,1793,1796,1799,1802,1805,1808,1811],{"path":1568,"title":1569},"\u002Fabout","About Python CLI Toolcraft",{"path":1571,"title":1572},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1574,"title":1575},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1577,"title":1578},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1580,"title":1581},"\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":1583,"title":1584},"\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":1586,"title":1587},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1589,"title":1590},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1592,"title":1593},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1595,"title":1596},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1598,"title":1599},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1601,"title":1602},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1604,"title":1605},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1607,"title":1608},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1610,"title":1611},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1613,"title":1614},"\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":1616,"title":1617},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1619,"title":1620},"\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":1622,"title":1623},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1625,"title":1626},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1628,"title":1629},"\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":1631,"title":1632},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1634,"title":1635},"\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":1637,"title":1638},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1640,"title":1641},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1643,"title":1644},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1646,"title":1647},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1649,"title":1650},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1652,"title":1653},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1655,"title":1656},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1658,"title":1659},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1661,"title":1662},"\u002F","Python CLI Toolcraft",{"path":1555,"title":5},{"path":1665,"title":1666},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1668,"title":1669},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1671,"title":1672},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1674,"title":1675},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1677,"title":1678},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1680,"title":1681},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1683,"title":1684},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1686,"title":1687},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1689,"title":1690},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1692,"title":1693},"\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":1695,"title":1696},"\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":1698,"title":1699},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1701,"title":1702},"\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":1704,"title":1705},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1707,"title":1708},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1710,"title":1711},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1713,"title":1714},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1716,"title":1717},"\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":1719,"title":1720},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1722,"title":1723},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1725,"title":1726},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1728,"title":1729},"\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":1731,"title":1732},"\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":1734,"title":1735},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1737,"title":1738},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1740,"title":1741},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1743,"title":1744},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1746,"title":1747},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1749,"title":1750},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1752,"title":1753},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1755,"title":1756},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1758,"title":1759},"\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":1761,"title":1762},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1764,"title":1765},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1767,"title":1768},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1770,"title":1771},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1773,"title":1774},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1776,"title":1777},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1779,"title":1780},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1782,"title":1783},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1785,"title":1786},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1788,"title":1789},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1791,"title":1792},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1794,"title":1795},"\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":1797,"title":1798},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1800,"title":1801},"\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":1803,"title":1804},"\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":1806,"title":1807},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1809,"title":1810},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1812,"title":1813},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690031]