[{"data":1,"prerenderedAt":1358},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002F":3,"content-directory":1110},{"id":4,"title":5,"body":6,"date":1094,"description":1095,"difficulty":1096,"draft":1097,"extension":1098,"meta":1099,"navigation":289,"path":1100,"seo":1101,"stem":1102,"tags":1103,"updated":1094,"__hash__":1109},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002Findex.md","Reducing CLI Dependency Weight",{"type":7,"value":8,"toc":1073},"minimark",[9,18,23,63,67,75,79,83,86,97,101,104,107,222,232,235,239,242,245,251,327,333,413,423,499,502,506,509,530,543,547,550,556,631,637,643,647,657,663,669,673,676,883,886,893,963,966,970,973,977,982,988,992,995,999,1008,1012,1015,1019,1025,1029,1032,1036,1069],[10,11,12,13,17],"p",{},"Every dependency you add is paid for on every invocation, including ",[14,15,16],"code",{},"--help"," and every keystroke of\ntab completion. Most of that cost is avoidable — not by removing libraries, but by deciding which\nones load at start-up and which load only when a command actually needs them.",[19,20,22],"h2",{"id":21},"tldr","TL;DR",[24,25,26,34,42,53,60],"ul",{},[27,28,29,30,33],"li",{},"Measure first: ",[14,31,32],{},"python -X importtime -m mytool --help"," and sort by the cumulative column.",[27,35,36,37,41],{},"Anything imported at module level costs on ",[38,39,40],"strong",{},"every"," invocation.",[27,43,44,45,48,49,52],{},"Move heavy, single-command dependencies behind an ",[38,46,47],{},"optional extra"," plus a ",[38,50,51],{},"lazy import",".",[27,54,55,56,59],{},"Keep the top-level ",[14,57,58],{},"__init__.py"," empty — it runs on every import of anything in the package.",[27,61,62],{},"Enforce the result with a start-up budget test, or it regresses within a month.",[19,64,66],{"id":65},"prerequisites","Prerequisites",[10,68,69,70,52],{},"A working CLI and the measurement habits from\n",[71,72,74],"a",{"href":73},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time\u002F","profiling Python CLI startup time",[19,76,78],{"id":77},"what-each-dependency-actually-costs","What each dependency actually costs",[80,81],"inline-diagram",{"name":82},"dependency-weight-bars",[10,84,85],{},"The numbers are worth internalising because they set expectations. A parser costs ten milliseconds.\nA parser plus a rendering library costs fifty. An HTTP client costs about as much again. A\nscientific stack costs several hundred, and it is usually the only one that matters.",[10,87,88,89,92,93,96],{},"Against a 20 ms interpreter floor, that means a CLI with Typer, Rich and ",[14,90,91],{},"httpx"," starts in roughly\n130 ms — noticeable but tolerable — and the same tool with a top-level ",[14,94,95],{},"import pandas"," starts in\n440 ms, which is where tab completion begins to feel broken.",[19,98,100],{"id":99},"auditing-the-tree","Auditing the tree",[80,102],{"name":103},"dependency-audit-flow",[10,105,106],{},"Three commands give you the whole picture:",[108,109,114],"pre",{"className":110,"code":111,"language":112,"meta":113,"style":113},"language-bash shiki shiki-themes github-light github-dark","uv tree                                     # what is actually installed, and why\npython -X importtime -m mytool --help 2>&1 | sort -k2 -rn | head -15\nuv run --with tuna python -X importtime -m mytool --help 2> import.log && tuna import.log\n","bash","",[14,115,116,133,179],{"__ignoreMap":113},[117,118,121,125,129],"span",{"class":119,"line":120},"line",1,[117,122,124],{"class":123},"sScJk","uv",[117,126,128],{"class":127},"sZZnC"," tree",[117,130,132],{"class":131},"sJ8bj","                                     # what is actually installed, and why\n",[117,134,136,139,143,146,149,152,155,159,162,165,168,171,173,176],{"class":119,"line":135},2,[117,137,138],{"class":123},"python",[117,140,142],{"class":141},"sj4cs"," -X",[117,144,145],{"class":127}," importtime",[117,147,148],{"class":141}," -m",[117,150,151],{"class":127}," mytool",[117,153,154],{"class":141}," --help",[117,156,158],{"class":157},"szBVR"," 2>&1",[117,160,161],{"class":157}," |",[117,163,164],{"class":123}," sort",[117,166,167],{"class":141}," -k2",[117,169,170],{"class":141}," -rn",[117,172,161],{"class":157},[117,174,175],{"class":123}," head",[117,177,178],{"class":141}," -15\n",[117,180,182,184,187,190,193,196,198,200,202,204,206,209,212,216,219],{"class":119,"line":181},3,[117,183,124],{"class":123},[117,185,186],{"class":127}," run",[117,188,189],{"class":141}," --with",[117,191,192],{"class":127}," tuna",[117,194,195],{"class":127}," python",[117,197,142],{"class":141},[117,199,145],{"class":127},[117,201,148],{"class":141},[117,203,151],{"class":127},[117,205,154],{"class":141},[117,207,208],{"class":157}," 2>",[117,210,211],{"class":127}," import.log",[117,213,215],{"class":214},"sVt8B"," && ",[117,217,218],{"class":123},"tuna",[117,220,221],{"class":127}," import.log\n",[10,223,224,227,228,231],{},[14,225,226],{},"uv tree"," answers \"why is this here\" — often the surprise is transitive: a small utility package\npulling in a large one. The ",[14,229,230],{},"importtime"," output answers \"what does it cost\", sorted by cumulative\nmicroseconds so the top line is the subtree worth attacking.",[10,233,234],{},"Then classify every top-level import in the start-up path into one of three buckets: needed on\nevery invocation, needed by one command, or not needed at all. The third bucket is smaller than\npeople hope and never empty.",[19,236,238],{"id":237},"moving-a-dependency-out-of-the-start-up-path","Moving a dependency out of the start-up path",[80,240],{"name":241},"extras-vs-runtime",[10,243,244],{},"The pattern has three parts, and all three are needed for it to be pleasant.",[10,246,247,250],{},[38,248,249],{},"Declare it as an extra",", so a user who never runs that command never installs it:",[108,252,256],{"className":253,"code":254,"language":255,"meta":113,"style":113},"language-toml shiki shiki-themes github-light github-dark","[project]\ndependencies = [\"typer>=0.12\", \"rich>=13.7\"]\n\n[project.optional-dependencies]\nanalysis = [\"pandas>=2.2\"]\naws = [\"boto3>=1.35\"]\n","toml",[14,257,258,269,285,291,305,316],{"__ignoreMap":113},[117,259,260,263,266],{"class":119,"line":120},[117,261,262],{"class":214},"[",[117,264,265],{"class":123},"project",[117,267,268],{"class":214},"]\n",[117,270,271,274,277,280,283],{"class":119,"line":135},[117,272,273],{"class":214},"dependencies = [",[117,275,276],{"class":127},"\"typer>=0.12\"",[117,278,279],{"class":214},", ",[117,281,282],{"class":127},"\"rich>=13.7\"",[117,284,268],{"class":214},[117,286,287],{"class":119,"line":181},[117,288,290],{"emptyLinePlaceholder":289},true,"\n",[117,292,294,296,298,300,303],{"class":119,"line":293},4,[117,295,262],{"class":214},[117,297,265],{"class":123},[117,299,52],{"class":214},[117,301,302],{"class":123},"optional-dependencies",[117,304,268],{"class":214},[117,306,308,311,314],{"class":119,"line":307},5,[117,309,310],{"class":214},"analysis = [",[117,312,313],{"class":127},"\"pandas>=2.2\"",[117,315,268],{"class":214},[117,317,319,322,325],{"class":119,"line":318},6,[117,320,321],{"class":214},"aws = [",[117,323,324],{"class":127},"\"boto3>=1.35\"",[117,326,268],{"class":214},[10,328,329,332],{},[38,330,331],{},"Import it inside the function"," that needs it:",[108,334,337],{"className":335,"code":336,"language":138,"meta":113,"style":113},"language-python shiki shiki-themes github-light github-dark","@app.command()\ndef report(period: str) -> None:\n    \"\"\"Summarise activity for PERIOD.\"\"\"\n    import pandas as pd          # 310 ms, and only this command needs it\n\n    frame = pd.DataFrame(core.collect(period))\n    ...\n",[14,338,339,347,370,375,392,396,407],{"__ignoreMap":113},[117,340,341,344],{"class":119,"line":120},[117,342,343],{"class":123},"@app.command",[117,345,346],{"class":214},"()\n",[117,348,349,352,355,358,361,364,367],{"class":119,"line":135},[117,350,351],{"class":157},"def",[117,353,354],{"class":123}," report",[117,356,357],{"class":214},"(period: ",[117,359,360],{"class":141},"str",[117,362,363],{"class":214},") -> ",[117,365,366],{"class":141},"None",[117,368,369],{"class":214},":\n",[117,371,372],{"class":119,"line":181},[117,373,374],{"class":127},"    \"\"\"Summarise activity for PERIOD.\"\"\"\n",[117,376,377,380,383,386,389],{"class":119,"line":293},[117,378,379],{"class":157},"    import",[117,381,382],{"class":214}," pandas ",[117,384,385],{"class":157},"as",[117,387,388],{"class":214}," pd          ",[117,390,391],{"class":131},"# 310 ms, and only this command needs it\n",[117,393,394],{"class":119,"line":307},[117,395,290],{"emptyLinePlaceholder":289},[117,397,398,401,404],{"class":119,"line":318},[117,399,400],{"class":214},"    frame ",[117,402,403],{"class":157},"=",[117,405,406],{"class":214}," pd.DataFrame(core.collect(period))\n",[117,408,410],{"class":119,"line":409},7,[117,411,412],{"class":141},"    ...\n",[10,414,415,418,419,422],{},[38,416,417],{},"Fail with an instruction"," when the extra is missing, because the default ",[14,420,421],{},"ImportError"," tells the\nuser nothing about how to fix it:",[108,424,426],{"className":335,"code":425,"language":138,"meta":113,"style":113},"def _load_pandas():\n    try:\n        import pandas\n    except ImportError:  # pragma: no cover - depends on install extras\n        raise typer.BadParameter(\n            \"the report command needs the analysis extra: pipx install 'mytool[analysis]'\"\n        ) from None\n    return pandas\n",[14,427,428,438,445,453,467,475,480,491],{"__ignoreMap":113},[117,429,430,432,435],{"class":119,"line":120},[117,431,351],{"class":157},[117,433,434],{"class":123}," _load_pandas",[117,436,437],{"class":214},"():\n",[117,439,440,443],{"class":119,"line":135},[117,441,442],{"class":157},"    try",[117,444,369],{"class":214},[117,446,447,450],{"class":119,"line":181},[117,448,449],{"class":157},"        import",[117,451,452],{"class":214}," pandas\n",[117,454,455,458,461,464],{"class":119,"line":293},[117,456,457],{"class":157},"    except",[117,459,460],{"class":141}," ImportError",[117,462,463],{"class":214},":  ",[117,465,466],{"class":131},"# pragma: no cover - depends on install extras\n",[117,468,469,472],{"class":119,"line":307},[117,470,471],{"class":157},"        raise",[117,473,474],{"class":214}," typer.BadParameter(\n",[117,476,477],{"class":119,"line":318},[117,478,479],{"class":127},"            \"the report command needs the analysis extra: pipx install 'mytool[analysis]'\"\n",[117,481,482,485,488],{"class":119,"line":409},[117,483,484],{"class":214},"        ) ",[117,486,487],{"class":157},"from",[117,489,490],{"class":141}," None\n",[117,492,494,497],{"class":119,"line":493},8,[117,495,496],{"class":157},"    return",[117,498,452],{"class":214},[10,500,501],{},"Three small changes, and the cost of the heaviest dependency moves from every invocation to the one\ncommand that needs it — and from every install to the users who asked for it.",[19,503,505],{"id":504},"the-two-structural-wins","The two structural wins",[10,507,508],{},"Beyond individual dependencies, two arrangements matter more than any single import.",[10,510,511,517,518,521,522,525,526,529],{},[38,512,513,514,516],{},"Keep ",[14,515,58],{}," empty."," The top-level package module runs on ",[519,520,40],"em",{}," import of anything\ninside the package, including the entry point. A convenient re-export there —\n",[14,523,524],{},"from .core.sync import sync_directory"," — quietly makes every invocation import the whole subtree.\nLimit it to ",[14,527,528],{},"__version__",", or leave it empty entirely.",[10,531,532,535,536,538,539,52],{},[38,533,534],{},"Resolve command modules on demand."," Once the obvious dependencies are deferred, what remains is\nimporting thirty command modules to build ",[14,537,16],{},". A registry of dotted paths resolved when a\ncommand is chosen removes it, and is covered in\n",[71,540,542],{"href":541},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup\u002F","lazy loading subcommands",[19,544,546],{"id":545},"what-you-give-up","What you give up",[10,548,549],{},"Lazy imports have a real cost, and it is worth naming rather than discovering.",[10,551,552,555],{},[38,553,554],{},"Import errors move."," A broken dependency no longer fails at start-up; it fails when someone runs\nthat command. Keep an import-all test in CI so the failure surfaces in the pull request instead:",[108,557,559],{"className":335,"code":558,"language":138,"meta":113,"style":113},"def test_every_command_module_imports():\n    package = importlib.import_module(\"mytool.commands\")\n    for module in pkgutil.iter_modules(package.__path__):\n        importlib.import_module(f\"mytool.commands.{module.name}\")\n",[14,560,561,570,586,606],{"__ignoreMap":113},[117,562,563,565,568],{"class":119,"line":120},[117,564,351],{"class":157},[117,566,567],{"class":123}," test_every_command_module_imports",[117,569,437],{"class":214},[117,571,572,575,577,580,583],{"class":119,"line":135},[117,573,574],{"class":214},"    package ",[117,576,403],{"class":157},[117,578,579],{"class":214}," importlib.import_module(",[117,581,582],{"class":127},"\"mytool.commands\"",[117,584,585],{"class":214},")\n",[117,587,588,591,594,597,600,603],{"class":119,"line":181},[117,589,590],{"class":157},"    for",[117,592,593],{"class":214}," module ",[117,595,596],{"class":157},"in",[117,598,599],{"class":214}," pkgutil.iter_modules(package.",[117,601,602],{"class":141},"__path__",[117,604,605],{"class":214},"):\n",[117,607,608,611,614,617,620,623,626,629],{"class":119,"line":293},[117,609,610],{"class":214},"        importlib.import_module(",[117,612,613],{"class":157},"f",[117,615,616],{"class":127},"\"mytool.commands.",[117,618,619],{"class":141},"{",[117,621,622],{"class":214},"module.name",[117,624,625],{"class":141},"}",[117,627,628],{"class":127},"\"",[117,630,585],{"class":214},[10,632,633,636],{},[38,634,635],{},"Linters complain."," A non-top-level import is flagged by default in most rule sets. Configure the\nexception rather than fighting it, and put a short comment on each deferred import saying what it\ncosts — that comment is also the reason nobody moves it back.",[10,638,639,642],{},[38,640,641],{},"Coverage shifts."," Lazily imported modules are not measured unless a test invokes that command,\nwhich is another reason the import-all test earns its place.",[19,644,646],{"id":645},"ux-considerations","UX considerations",[10,648,649,652,653,656],{},[38,650,651],{},"Advertise the extras."," A user who never learns ",[14,654,655],{},"mytool[analysis]"," exists concludes the report\ncommand is broken. Name the extra in the error, in the README install section, and in the command's\nhelp text.",[10,658,659,662],{},[38,660,661],{},"Keep the base install useful."," An extra should gate a genuinely optional capability, not the main\none. If the most common command needs an extra, the split is in the wrong place.",[10,664,665,668],{},[38,666,667],{},"Do not split too finely."," Five extras is a matrix of install combinations you now support and\ntest. Two or three, chosen around real user groups, is usually the right granularity.",[19,670,672],{"id":671},"testing-the-behaviour","Testing the behaviour",[10,674,675],{},"A budget test turns a one-off improvement into something that stays true:",[108,677,679],{"className":335,"code":678,"language":138,"meta":113,"style":113},"import subprocess, sys, time\n\nBUDGET_MS = 150\n\ndef test_help_starts_within_budget():\n    best = min(_time_help() for _ in range(5))\n    assert best \u003C BUDGET_MS, f\"--help took {best:.0f} ms (budget {BUDGET_MS} ms)\"\n\ndef _time_help() -> float:\n    start = time.perf_counter()\n    subprocess.run([sys.executable, \"-m\", \"mytool\", \"--help\"], capture_output=True, check=True)\n    return (time.perf_counter() - start) * 1000\n",[14,680,681,689,693,704,708,717,750,790,794,810,821,863],{"__ignoreMap":113},[117,682,683,686],{"class":119,"line":120},[117,684,685],{"class":157},"import",[117,687,688],{"class":214}," subprocess, sys, time\n",[117,690,691],{"class":119,"line":135},[117,692,290],{"emptyLinePlaceholder":289},[117,694,695,698,701],{"class":119,"line":181},[117,696,697],{"class":141},"BUDGET_MS",[117,699,700],{"class":157}," =",[117,702,703],{"class":141}," 150\n",[117,705,706],{"class":119,"line":293},[117,707,290],{"emptyLinePlaceholder":289},[117,709,710,712,715],{"class":119,"line":307},[117,711,351],{"class":157},[117,713,714],{"class":123}," test_help_starts_within_budget",[117,716,437],{"class":214},[117,718,719,722,724,727,730,733,736,738,741,744,747],{"class":119,"line":318},[117,720,721],{"class":214},"    best ",[117,723,403],{"class":157},[117,725,726],{"class":141}," min",[117,728,729],{"class":214},"(_time_help() ",[117,731,732],{"class":157},"for",[117,734,735],{"class":214}," _ ",[117,737,596],{"class":157},[117,739,740],{"class":141}," range",[117,742,743],{"class":214},"(",[117,745,746],{"class":141},"5",[117,748,749],{"class":214},"))\n",[117,751,752,755,758,761,764,766,768,771,773,776,779,781,784,787],{"class":119,"line":409},[117,753,754],{"class":157},"    assert",[117,756,757],{"class":214}," best ",[117,759,760],{"class":157},"\u003C",[117,762,763],{"class":141}," BUDGET_MS",[117,765,279],{"class":214},[117,767,613],{"class":157},[117,769,770],{"class":127},"\"--help took ",[117,772,619],{"class":141},[117,774,775],{"class":214},"best",[117,777,778],{"class":157},":.0f",[117,780,625],{"class":141},[117,782,783],{"class":127}," ms (budget ",[117,785,786],{"class":141},"{BUDGET_MS}",[117,788,789],{"class":127}," ms)\"\n",[117,791,792],{"class":119,"line":493},[117,793,290],{"emptyLinePlaceholder":289},[117,795,797,799,802,805,808],{"class":119,"line":796},9,[117,798,351],{"class":157},[117,800,801],{"class":123}," _time_help",[117,803,804],{"class":214},"() -> ",[117,806,807],{"class":141},"float",[117,809,369],{"class":214},[117,811,813,816,818],{"class":119,"line":812},10,[117,814,815],{"class":214},"    start ",[117,817,403],{"class":157},[117,819,820],{"class":214}," time.perf_counter()\n",[117,822,824,827,830,832,835,837,840,843,847,849,852,854,857,859,861],{"class":119,"line":823},11,[117,825,826],{"class":214},"    subprocess.run([sys.executable, ",[117,828,829],{"class":127},"\"-m\"",[117,831,279],{"class":214},[117,833,834],{"class":127},"\"mytool\"",[117,836,279],{"class":214},[117,838,839],{"class":127},"\"--help\"",[117,841,842],{"class":214},"], ",[117,844,846],{"class":845},"s4XuR","capture_output",[117,848,403],{"class":157},[117,850,851],{"class":141},"True",[117,853,279],{"class":214},[117,855,856],{"class":845},"check",[117,858,403],{"class":157},[117,860,851],{"class":141},[117,862,585],{"class":214},[117,864,866,868,871,874,877,880],{"class":119,"line":865},12,[117,867,496],{"class":157},[117,869,870],{"class":214}," (time.perf_counter() ",[117,872,873],{"class":157},"-",[117,875,876],{"class":214}," start) ",[117,878,879],{"class":157},"*",[117,881,882],{"class":141}," 1000\n",[10,884,885],{},"Take the best of several runs rather than an average — you are measuring a floor, and CI runners are\nnoisy. Set the budget slightly above your current number so it fails on a regression rather than on\na bad afternoon, and lower it when the figure improves.",[10,887,888,889,892],{},"A second, sharper test asserts that specific modules are ",[519,890,891],{},"not"," imported at start-up:",[108,894,896],{"className":335,"code":895,"language":138,"meta":113,"style":113},"def test_heavy_dependencies_are_not_imported_for_help():\n    code = \"import sys, mytool.cli; print('pandas' in sys.modules)\"\n    out = subprocess.run([sys.executable, \"-c\", code], capture_output=True, text=True)\n    assert out.stdout.strip() == \"False\"\n",[14,897,898,907,917,950],{"__ignoreMap":113},[117,899,900,902,905],{"class":119,"line":120},[117,901,351],{"class":157},[117,903,904],{"class":123}," test_heavy_dependencies_are_not_imported_for_help",[117,906,437],{"class":214},[117,908,909,912,914],{"class":119,"line":135},[117,910,911],{"class":214},"    code ",[117,913,403],{"class":157},[117,915,916],{"class":127}," \"import sys, mytool.cli; print('pandas' in sys.modules)\"\n",[117,918,919,922,924,927,930,933,935,937,939,941,944,946,948],{"class":119,"line":181},[117,920,921],{"class":214},"    out ",[117,923,403],{"class":157},[117,925,926],{"class":214}," subprocess.run([sys.executable, ",[117,928,929],{"class":127},"\"-c\"",[117,931,932],{"class":214},", code], ",[117,934,846],{"class":845},[117,936,403],{"class":157},[117,938,851],{"class":141},[117,940,279],{"class":214},[117,942,943],{"class":845},"text",[117,945,403],{"class":157},[117,947,851],{"class":141},[117,949,585],{"class":214},[117,951,952,954,957,960],{"class":119,"line":293},[117,953,754],{"class":157},[117,955,956],{"class":214}," out.stdout.strip() ",[117,958,959],{"class":157},"==",[117,961,962],{"class":127}," \"False\"\n",[10,964,965],{},"That one names the offender directly, which makes the failure immediately actionable.",[19,967,969],{"id":968},"conclusion","Conclusion",[10,971,972],{},"Measure, classify, then move: an extra for the packaging, a lazy import for the start-up cost, and\na helpful error so the split is invisible to users who need the feature. Add a budget test and an\nimport-all test, and the improvement survives the next six months of dependency additions.",[19,974,976],{"id":975},"frequently-asked-questions","Frequently asked questions",[978,979,981],"h3",{"id":980},"is-rich-too-heavy-for-a-cli","Is Rich too heavy for a CLI?",[10,983,984,985,987],{},"For most tools, no — roughly 40 ms buys tables, progress, colour and correct behaviour when output\nis redirected, all of which you would otherwise write. If you need ",[14,986,16],{}," under 60 ms, import it\ninside the rendering functions rather than dropping it.",[978,989,991],{"id":990},"should-i-vendor-a-small-dependency-instead-of-depending-on-it","Should I vendor a small dependency instead of depending on it?",[10,993,994],{},"Almost never. Vendoring means you own the security updates and the bug reports, and the import cost\nis the same either way. The exception is a handful of lines you would otherwise pull a whole package\nfor — and even then, copying with attribution beats a dependency only if the code is genuinely\nstable.",[978,996,998],{"id":997},"how-do-i-find-which-dependency-pulled-in-a-heavy-transitive-one","How do I find which dependency pulled in a heavy transitive one?",[10,1000,1001,1003,1004,1007],{},[14,1002,226],{}," shows the graph with the requester of each package; ",[14,1005,1006],{},"pipdeptree --reverse"," does the same\nfor a pip environment. The usual discovery is that a convenience library depends on something\nsubstantial, and replacing the convenience removes both.",[978,1009,1011],{"id":1010},"do-lazy-imports-slow-down-the-command-that-uses-them","Do lazy imports slow down the command that uses them?",[10,1013,1014],{},"Marginally — the import happens on first use instead of at start-up, so that one command pays what\nevery command used to. That is the trade, and it is nearly always worth it: the heavy command was\ngoing to do heavy work anyway.",[978,1016,1018],{"id":1017},"what-is-a-realistic-target-after-this-work","What is a realistic target after this work?",[10,1020,1021,1022,1024],{},"Under 100 ms for ",[14,1023,16],{}," on a tool with a normal dependency set. Getting below the 40 ms range\nusually means dropping the rendering library, which is a trade most tools should not make.",[978,1026,1028],{"id":1027},"does-removing-a-dependency-ever-make-things-worse","Does removing a dependency ever make things worse?",[10,1030,1031],{},"It can. Replacing a well-tested library with two hundred lines of your own moves the cost from\nimport time to maintenance time, and your version will have the bugs theirs already fixed. The\ndependencies worth removing are the ones you barely use — a whole HTTP library for two GET requests,\na date library for one format string — not the ones doing real work.",[19,1033,1035],{"id":1034},"related","Related",[24,1037,1038,1045,1051,1056,1063],{},[27,1039,1040,1041],{},"Up: ",[71,1042,1044],{"href":1043},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI startup performance and lazy loading",[27,1046,1047,1048],{},"Sideways: ",[71,1049,1050],{"href":73},"Profiling Python CLI startup time",[27,1052,1047,1053],{},[71,1054,1055],{"href":541},"Lazy loading subcommands for faster startup",[27,1057,1058,1059],{},"Related: ",[71,1060,1062],{"href":1061},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002F","uv for Python CLI dependency management",[27,1064,1058,1065],{},[71,1066,1068],{"href":1067},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","Packaging Python CLIs for distribution",[1070,1071,1072],"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":113,"searchDepth":135,"depth":135,"links":1074},[1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1093],{"id":21,"depth":135,"text":22},{"id":65,"depth":135,"text":66},{"id":77,"depth":135,"text":78},{"id":99,"depth":135,"text":100},{"id":237,"depth":135,"text":238},{"id":504,"depth":135,"text":505},{"id":545,"depth":135,"text":546},{"id":645,"depth":135,"text":646},{"id":671,"depth":135,"text":672},{"id":968,"depth":135,"text":969},{"id":975,"depth":135,"text":976,"children":1086},[1087,1088,1089,1090,1091,1092],{"id":980,"depth":181,"text":981},{"id":990,"depth":181,"text":991},{"id":997,"depth":181,"text":998},{"id":1010,"depth":181,"text":1011},{"id":1017,"depth":181,"text":1018},{"id":1027,"depth":181,"text":1028},{"id":1034,"depth":135,"text":1035},"2026-08-01","Cut a Python CLI's import cost - audit the dependency tree, move heavy packages behind optional extras and lazy imports, and enforce the win in CI.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight",{"title":5,"description":1095},"modern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002Findex",[1104,1105,1106,1107,1108],"performance","dependencies","imports","packaging","extras","D4snsL9xAaNc01ImJAD1sLzGeNQTOI0xcFqkxTm5l10",[1111,1114,1117,1120,1123,1126,1129,1132,1135,1138,1141,1144,1147,1150,1153,1156,1159,1162,1165,1168,1171,1174,1177,1180,1183,1186,1189,1192,1195,1198,1201,1204,1207,1210,1213,1216,1217,1220,1223,1226,1229,1232,1235,1238,1241,1244,1247,1250,1253,1256,1259,1262,1265,1268,1271,1274,1277,1280,1283,1286,1289,1292,1295,1298,1301,1304,1307,1310,1313,1316,1319,1322,1325,1328,1331,1334,1337,1340,1343,1346,1349,1352,1355],{"path":1112,"title":1113},"\u002Fabout","About Python CLI Toolcraft",{"path":1115,"title":1116},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1118,"title":1119},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1121,"title":1122},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1124,"title":1125},"\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":1127,"title":1128},"\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":1130,"title":1131},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1133,"title":1134},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1136,"title":1137},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1139,"title":1140},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1142,"title":1143},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1145,"title":1146},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1148,"title":1149},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1151,"title":1152},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1154,"title":1155},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1157,"title":1158},"\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":1160,"title":1161},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1163,"title":1164},"\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":1166,"title":1167},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1169,"title":1170},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1172,"title":1173},"\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":1175,"title":1176},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1178,"title":1179},"\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":1181,"title":1182},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1184,"title":1185},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1187,"title":1188},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1190,"title":1191},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1193,"title":1194},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1196,"title":1197},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1199,"title":1200},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1202,"title":1203},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1205,"title":1206},"\u002F","Python CLI Toolcraft",{"path":1208,"title":1209},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1211,"title":1212},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1214,"title":1215},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1100,"title":5},{"path":1218,"title":1219},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1221,"title":1222},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1224,"title":1225},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1227,"title":1228},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1230,"title":1231},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1233,"title":1234},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1236,"title":1237},"\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":1239,"title":1240},"\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":1242,"title":1243},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1245,"title":1246},"\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":1248,"title":1249},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1251,"title":1252},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1254,"title":1255},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1257,"title":1258},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1260,"title":1261},"\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":1263,"title":1264},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1266,"title":1267},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1269,"title":1270},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1272,"title":1273},"\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":1275,"title":1276},"\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":1278,"title":1279},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1281,"title":1282},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1284,"title":1285},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1287,"title":1288},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1290,"title":1291},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1293,"title":1294},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1296,"title":1297},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1299,"title":1300},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1302,"title":1303},"\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":1305,"title":1306},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1308,"title":1309},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1311,"title":1312},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1314,"title":1315},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1317,"title":1318},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1320,"title":1321},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1323,"title":1324},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1326,"title":1327},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1329,"title":1330},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1332,"title":1333},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1335,"title":1336},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1338,"title":1339},"\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":1341,"title":1342},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1344,"title":1345},"\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":1347,"title":1348},"\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":1350,"title":1351},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1353,"title":1354},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1356,"title":1357},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690031]