[{"data":1,"prerenderedAt":1261},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli\u002F":3,"content-directory":1014},{"id":4,"title":5,"body":6,"date":1000,"description":1001,"difficulty":1002,"draft":1003,"extension":1004,"meta":1005,"navigation":167,"path":1006,"seo":1007,"stem":1008,"tags":1009,"updated":1000,"__hash__":1013},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli\u002Findex.md","Writing a Plugin for an Existing CLI",{"type":7,"value":8,"toc":979},"minimark",[9,13,18,68,72,79,83,87,202,410,417,421,424,431,493,500,504,507,510,520,534,544,547,551,554,567,577,580,584,593,602,608,614,618,625,856,863,867,870,874,879,890,894,897,901,904,908,911,915,918,922,925,929,935,939,975],[10,11,12],"p",{},"Most of the writing about plugin systems is aimed at the host. This is the other side: you have a\nCLI you do not own, it advertises an extension point, and you want to add commands to it without\nforking anything.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,38,49,56],"ul",{},[22,23,24,25,29],"li",{},"A plugin is an ",[26,27,28],"strong",{},"ordinary package"," whose metadata names the host's entry-point group.",[22,31,32,33,37],{},"Installing it ",[34,35,36],"em",{},"is"," the registration — the host reads installed metadata at run time.",[22,39,40,41,44,45,48],{},"Depend on the host with a ",[26,42,43],{},"version range",", and declare whatever ",[26,46,47],{},"API version"," it checks.",[22,50,51,52,55],{},"Use only ",[26,53,54],{},"documented seams","; importing the host's private modules is how plugins break.",[22,57,58,59,63,64,67],{},"Install it into the host's environment with ",[60,61,62],"code",{},"pipx inject"," or ",[60,65,66],{},"uv tool install --with",".",[14,69,71],{"id":70},"prerequisites","Prerequisites",[10,73,74,75,78],{},"A host CLI that documents an entry-point group (for example ",[60,76,77],{},"mytool.plugins","), and a package of\nyour own to put the commands in.",[14,80,82],{"id":81},"what-a-plugin-package-looks-like","What a plugin package looks like",[84,85],"inline-diagram",{"name":86},"plugin-package-layout",[88,89,94],"pre",{"className":90,"code":91,"language":92,"meta":93,"style":93},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[project]\nname = \"mytool-deploy\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\"mytool>=1.4,\u003C2.0\"]      # the host, with a range\n\n[project.entry-points.\"mytool.plugins\"]\ndeploy = \"mytool_deploy.cli:plugin\"\n","toml","",[60,95,96,105,119,129,138,147,162,169,193],{"__ignoreMap":93},[97,98,101],"span",{"class":99,"line":100},"line",1,[97,102,104],{"class":103},"sJ8bj","# pyproject.toml\n",[97,106,108,112,116],{"class":99,"line":107},2,[97,109,111],{"class":110},"sVt8B","[",[97,113,115],{"class":114},"sScJk","project",[97,117,118],{"class":110},"]\n",[97,120,122,125],{"class":99,"line":121},3,[97,123,124],{"class":110},"name = ",[97,126,128],{"class":127},"sZZnC","\"mytool-deploy\"\n",[97,130,132,135],{"class":99,"line":131},4,[97,133,134],{"class":110},"version = ",[97,136,137],{"class":127},"\"0.1.0\"\n",[97,139,141,144],{"class":99,"line":140},5,[97,142,143],{"class":110},"requires-python = ",[97,145,146],{"class":127},"\">=3.11\"\n",[97,148,150,153,156,159],{"class":99,"line":149},6,[97,151,152],{"class":110},"dependencies = [",[97,154,155],{"class":127},"\"mytool>=1.4,\u003C2.0\"",[97,157,158],{"class":110},"]      ",[97,160,161],{"class":103},"# the host, with a range\n",[97,163,165],{"class":99,"line":164},7,[97,166,168],{"emptyLinePlaceholder":167},true,"\n",[97,170,172,174,176,178,181,183,186,188,191],{"class":99,"line":171},8,[97,173,111],{"class":110},[97,175,115],{"class":114},[97,177,67],{"class":110},[97,179,180],{"class":114},"entry-points",[97,182,67],{"class":110},[97,184,185],{"class":114},"\"mytool",[97,187,67],{"class":110},[97,189,190],{"class":114},"plugins\"",[97,192,118],{"class":110},[97,194,196,199],{"class":99,"line":195},9,[97,197,198],{"class":110},"deploy = ",[97,200,201],{"class":127},"\"mytool_deploy.cli:plugin\"\n",[88,203,207],{"className":204,"code":205,"language":206,"meta":93,"style":93},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool_deploy\u002Fcli.py\nimport typer\n\nAPI_VERSION = 2            # the contract version the host checks\napp = typer.Typer(help=\"Deployment commands.\")\n\n@app.command()\ndef rollout(environment: str, replicas: int = 3) -> None:\n    \"\"\"Roll out the current build to ENVIRONMENT.\"\"\"\n    ...\n\nclass plugin:              # the object the entry point names\n    api_version = API_VERSION\n    name = \"deploy\"\n\n    @staticmethod\n    def register(host_app: typer.Typer) -> None:\n        host_app.add_typer(app, name=\"deploy\")\n","python",[60,208,209,214,223,227,242,265,269,277,311,316,322,327,342,353,364,369,378,394],{"__ignoreMap":93},[97,210,211],{"class":99,"line":100},[97,212,213],{"class":103},"# src\u002Fmytool_deploy\u002Fcli.py\n",[97,215,216,220],{"class":99,"line":107},[97,217,219],{"class":218},"szBVR","import",[97,221,222],{"class":110}," typer\n",[97,224,225],{"class":99,"line":121},[97,226,168],{"emptyLinePlaceholder":167},[97,228,229,233,236,239],{"class":99,"line":131},[97,230,232],{"class":231},"sj4cs","API_VERSION",[97,234,235],{"class":218}," =",[97,237,238],{"class":231}," 2",[97,240,241],{"class":103},"            # the contract version the host checks\n",[97,243,244,247,250,253,257,259,262],{"class":99,"line":140},[97,245,246],{"class":110},"app ",[97,248,249],{"class":218},"=",[97,251,252],{"class":110}," typer.Typer(",[97,254,256],{"class":255},"s4XuR","help",[97,258,249],{"class":218},[97,260,261],{"class":127},"\"Deployment commands.\"",[97,263,264],{"class":110},")\n",[97,266,267],{"class":99,"line":149},[97,268,168],{"emptyLinePlaceholder":167},[97,270,271,274],{"class":99,"line":164},[97,272,273],{"class":114},"@app.command",[97,275,276],{"class":110},"()\n",[97,278,279,282,285,288,291,294,297,299,302,305,308],{"class":99,"line":171},[97,280,281],{"class":218},"def",[97,283,284],{"class":114}," rollout",[97,286,287],{"class":110},"(environment: ",[97,289,290],{"class":231},"str",[97,292,293],{"class":110},", replicas: ",[97,295,296],{"class":231},"int",[97,298,235],{"class":218},[97,300,301],{"class":231}," 3",[97,303,304],{"class":110},") -> ",[97,306,307],{"class":231},"None",[97,309,310],{"class":110},":\n",[97,312,313],{"class":99,"line":195},[97,314,315],{"class":127},"    \"\"\"Roll out the current build to ENVIRONMENT.\"\"\"\n",[97,317,319],{"class":99,"line":318},10,[97,320,321],{"class":231},"    ...\n",[97,323,325],{"class":99,"line":324},11,[97,326,168],{"emptyLinePlaceholder":167},[97,328,330,333,336,339],{"class":99,"line":329},12,[97,331,332],{"class":218},"class",[97,334,335],{"class":114}," plugin",[97,337,338],{"class":110},":              ",[97,340,341],{"class":103},"# the object the entry point names\n",[97,343,345,348,350],{"class":99,"line":344},13,[97,346,347],{"class":110},"    api_version ",[97,349,249],{"class":218},[97,351,352],{"class":231}," API_VERSION\n",[97,354,356,359,361],{"class":99,"line":355},14,[97,357,358],{"class":110},"    name ",[97,360,249],{"class":218},[97,362,363],{"class":127}," \"deploy\"\n",[97,365,367],{"class":99,"line":366},15,[97,368,168],{"emptyLinePlaceholder":167},[97,370,372,375],{"class":99,"line":371},16,[97,373,374],{"class":114},"    @",[97,376,377],{"class":231},"staticmethod\n",[97,379,381,384,387,390,392],{"class":99,"line":380},17,[97,382,383],{"class":218},"    def",[97,385,386],{"class":114}," register",[97,388,389],{"class":110},"(host_app: typer.Typer) -> ",[97,391,307],{"class":231},[97,393,310],{"class":110},[97,395,397,400,403,405,408],{"class":99,"line":396},18,[97,398,399],{"class":110},"        host_app.add_typer(app, ",[97,401,402],{"class":255},"name",[97,404,249],{"class":218},[97,406,407],{"class":127},"\"deploy\"",[97,409,264],{"class":110},[10,411,412,413,416],{},"The exact shape of the object depends on the host's contract — some want a Typer app, some a\ncallable, some a class with a ",[60,414,415],{},"register"," method. What is universal is that the entry point names an\nimportable target and the host decides what to do with it.",[14,418,420],{"id":419},"installing-into-the-hosts-environment","Installing into the host's environment",[84,422],{"name":423},"plugin-lifecycle-sequence",[10,425,426,427,430],{},"A plugin must be installed ",[34,428,429],{},"where the host is",", which for an isolated install means injecting into\nthat environment rather than installing globally:",[88,432,436],{"className":433,"code":434,"language":435,"meta":93,"style":93},"language-bash shiki shiki-themes github-light github-dark","pipx inject mytool mytool-deploy               # into the host's own venv\nuv tool install mytool --with mytool-deploy    # or declare it at install time\npipx inject mytool -e .\u002Fmytool-deploy          # editable, while developing\n","bash",[60,437,438,455,476],{"__ignoreMap":93},[97,439,440,443,446,449,452],{"class":99,"line":100},[97,441,442],{"class":114},"pipx",[97,444,445],{"class":127}," inject",[97,447,448],{"class":127}," mytool",[97,450,451],{"class":127}," mytool-deploy",[97,453,454],{"class":103},"               # into the host's own venv\n",[97,456,457,460,463,466,468,471,473],{"class":99,"line":107},[97,458,459],{"class":114},"uv",[97,461,462],{"class":127}," tool",[97,464,465],{"class":127}," install",[97,467,448],{"class":127},[97,469,470],{"class":231}," --with",[97,472,451],{"class":127},[97,474,475],{"class":103},"    # or declare it at install time\n",[97,477,478,480,482,484,487,490],{"class":99,"line":121},[97,479,442],{"class":114},[97,481,445],{"class":127},[97,483,448],{"class":127},[97,485,486],{"class":231}," -e",[97,488,489],{"class":127}," .\u002Fmytool-deploy",[97,491,492],{"class":103},"          # editable, while developing\n",[10,494,495,496,499],{},"That last form is the development loop: an editable install means your edits take effect\nimmediately, and ",[60,497,498],{},"mytool --help"," shows your commands straight away. If the plugin does not appear,\nthe usual causes are installing it into the wrong environment, a typo in the group name, or a host\nthat caches its plugin list.",[14,501,503],{"id":502},"keeping-the-plugin-working","Keeping the plugin working",[84,505],{"name":506},"plugin-compat-checklist",[10,508,509],{},"Three habits decide whether your plugin survives the host's next release.",[10,511,512,515,516,519],{},[26,513,514],{},"Depend on a range, not a pin."," ",[60,517,518],{},"mytool>=1.4,\u003C2.0"," says what you actually support. A hard pin\nmakes your plugin uninstallable alongside a slightly newer host; no constraint at all means it\nbreaks silently when the host makes a major change.",[10,521,522,525,526,529,530,533],{},[26,523,524],{},"Declare the API version the host asks for."," If the host checks ",[60,527,528],{},"api_version",", that integer is\nthe contract. When it bumps, the host will skip your plugin with a message rather than half-loading\nit — which is exactly what you want, because a clear \"needs API v3\" is far better than an\n",[60,531,532],{},"AttributeError"," in a user's terminal.",[10,535,536,539,540,543],{},[26,537,538],{},"Use documented seams only."," Importing ",[60,541,542],{},"mytool.internal.registry"," works today and is not part of\nanything the host promised. The same applies to monkeypatching host functions at import time, which\nturns your plugin into something the host cannot refactor around.",[10,545,546],{},"When you genuinely need something the host does not expose, ask for it upstream. A documented\nextension point benefits every plugin author, including future-you.",[14,548,550],{"id":549},"designing-the-commands-you-add","Designing the commands you add",[10,552,553],{},"Two conventions make a plugin feel like part of the tool rather than bolted on.",[10,555,556,515,559,562,563,566],{},[26,557,558],{},"Namespace your commands unless the host says otherwise.",[60,560,561],{},"mytool deploy rollout"," is unambiguous;\na top-level ",[60,564,565],{},"mytool rollout"," competes with the host's own naming and with every other plugin. Most\nhosts that document a plugin group also document where plugin commands should live.",[10,568,569,572,573,576],{},[26,570,571],{},"Follow the host's conventions"," for output, exit codes and configuration. If the host emits JSON\nunder ",[60,574,575],{},"--json"," with a versioned envelope, your commands should too; if it uses exit code 78 for a\nconfiguration problem, use the same. Users do not care which package a command came from, and\ninconsistency reads as a bug in the tool.",[10,578,579],{},"Read configuration through whatever the host provides — usually a settings object on the context —\nrather than loading your own file. Two configuration systems in one CLI is a support burden that\nlands on the host's maintainer.",[14,581,583],{"id":582},"ux-considerations","UX considerations",[10,585,586,515,589,592],{},[26,587,588],{},"Name the package after the host.",[60,590,591],{},"mytool-deploy"," tells a user immediately what it extends and\ngroups your plugin with others in search results.",[10,594,595,601],{},[26,596,597,598,67],{},"Document the injection command, not ",[60,599,600],{},"pip install"," A user who installs your plugin globally\nwhile the host is in a pipx environment gets nothing, and the failure is silent — no error, just\nmissing commands.",[10,603,604,607],{},[26,605,606],{},"Say which host versions you support"," in the README, and keep it accurate. Users debug \"my plugin\ndisappeared\" far faster when the answer is a version range they can check.",[10,609,610,613],{},[26,611,612],{},"Fail helpfully."," If your commands need credentials or a service the host does not, produce a\nclear error naming what is missing, exactly as the host would.",[14,615,617],{"id":616},"testing-the-behaviour","Testing the behaviour",[10,619,620,621,624],{},"The valuable test invokes the ",[34,622,623],{},"host"," and asserts your commands are reachable, because that\nexercises discovery rather than mocking it:",[88,626,628],{"className":204,"code":627,"language":206,"meta":93,"style":93},"from typer.testing import CliRunner\nfrom mytool.cli import app          # the host application\n\nrunner = CliRunner()\n\ndef test_plugin_commands_are_registered():\n    result = runner.invoke(app, [\"--help\"])\n    assert result.exit_code == 0\n    assert \"deploy\" in result.stdout\n\ndef test_rollout_runs_through_the_host(monkeypatch):\n    seen = {}\n    monkeypatch.setattr(\"mytool_deploy.cli.core.rollout\", lambda **kw: seen.update(kw))\n\n    result = runner.invoke(app, [\"deploy\", \"rollout\", \"prod\", \"--replicas\", \"5\"])\n\n    assert result.exit_code == 0\n    assert seen == {\"environment\": \"prod\", \"replicas\": 5}\n",[60,629,630,643,658,662,672,676,686,702,716,729,733,743,753,773,777,809,813,823],{"__ignoreMap":93},[97,631,632,635,638,640],{"class":99,"line":100},[97,633,634],{"class":218},"from",[97,636,637],{"class":110}," typer.testing ",[97,639,219],{"class":218},[97,641,642],{"class":110}," CliRunner\n",[97,644,645,647,650,652,655],{"class":99,"line":107},[97,646,634],{"class":218},[97,648,649],{"class":110}," mytool.cli ",[97,651,219],{"class":218},[97,653,654],{"class":110}," app          ",[97,656,657],{"class":103},"# the host application\n",[97,659,660],{"class":99,"line":121},[97,661,168],{"emptyLinePlaceholder":167},[97,663,664,667,669],{"class":99,"line":131},[97,665,666],{"class":110},"runner ",[97,668,249],{"class":218},[97,670,671],{"class":110}," CliRunner()\n",[97,673,674],{"class":99,"line":140},[97,675,168],{"emptyLinePlaceholder":167},[97,677,678,680,683],{"class":99,"line":149},[97,679,281],{"class":218},[97,681,682],{"class":114}," test_plugin_commands_are_registered",[97,684,685],{"class":110},"():\n",[97,687,688,691,693,696,699],{"class":99,"line":164},[97,689,690],{"class":110},"    result ",[97,692,249],{"class":218},[97,694,695],{"class":110}," runner.invoke(app, [",[97,697,698],{"class":127},"\"--help\"",[97,700,701],{"class":110},"])\n",[97,703,704,707,710,713],{"class":99,"line":171},[97,705,706],{"class":218},"    assert",[97,708,709],{"class":110}," result.exit_code ",[97,711,712],{"class":218},"==",[97,714,715],{"class":231}," 0\n",[97,717,718,720,723,726],{"class":99,"line":195},[97,719,706],{"class":218},[97,721,722],{"class":127}," \"deploy\"",[97,724,725],{"class":218}," in",[97,727,728],{"class":110}," result.stdout\n",[97,730,731],{"class":99,"line":318},[97,732,168],{"emptyLinePlaceholder":167},[97,734,735,737,740],{"class":99,"line":324},[97,736,281],{"class":218},[97,738,739],{"class":114}," test_rollout_runs_through_the_host",[97,741,742],{"class":110},"(monkeypatch):\n",[97,744,745,748,750],{"class":99,"line":329},[97,746,747],{"class":110},"    seen ",[97,749,249],{"class":218},[97,751,752],{"class":110}," {}\n",[97,754,755,758,761,764,767,770],{"class":99,"line":344},[97,756,757],{"class":110},"    monkeypatch.setattr(",[97,759,760],{"class":127},"\"mytool_deploy.cli.core.rollout\"",[97,762,763],{"class":110},", ",[97,765,766],{"class":218},"lambda",[97,768,769],{"class":218}," **",[97,771,772],{"class":110},"kw: seen.update(kw))\n",[97,774,775],{"class":99,"line":355},[97,776,168],{"emptyLinePlaceholder":167},[97,778,779,781,783,785,787,789,792,794,797,799,802,804,807],{"class":99,"line":366},[97,780,690],{"class":110},[97,782,249],{"class":218},[97,784,695],{"class":110},[97,786,407],{"class":127},[97,788,763],{"class":110},[97,790,791],{"class":127},"\"rollout\"",[97,793,763],{"class":110},[97,795,796],{"class":127},"\"prod\"",[97,798,763],{"class":110},[97,800,801],{"class":127},"\"--replicas\"",[97,803,763],{"class":110},[97,805,806],{"class":127},"\"5\"",[97,808,701],{"class":110},[97,810,811],{"class":99,"line":371},[97,812,168],{"emptyLinePlaceholder":167},[97,814,815,817,819,821],{"class":99,"line":380},[97,816,706],{"class":218},[97,818,709],{"class":110},[97,820,712],{"class":218},[97,822,715],{"class":231},[97,824,825,827,830,832,835,838,841,843,845,848,850,853],{"class":99,"line":396},[97,826,706],{"class":218},[97,828,829],{"class":110}," seen ",[97,831,712],{"class":218},[97,833,834],{"class":110}," {",[97,836,837],{"class":127},"\"environment\"",[97,839,840],{"class":110},": ",[97,842,796],{"class":127},[97,844,763],{"class":110},[97,846,847],{"class":127},"\"replicas\"",[97,849,840],{"class":110},[97,851,852],{"class":231},"5",[97,854,855],{"class":110},"}\n",[10,857,858,859,862],{},"For that to work, the test environment needs both packages installed — the host as a dependency and\nyour plugin editable, which ",[60,860,861],{},"uv sync"," gives you when the plugin is the project. Add the oldest\nsupported host version to your CI matrix; that is the test that catches a contract change you would\notherwise meet in a user's bug report.",[14,864,866],{"id":865},"conclusion","Conclusion",[10,868,869],{},"A plugin is a small package with one line of metadata. The work is not in the mechanism but in the\ndiscipline around it: a version range, a declared API version, documented seams only, and\nconventions borrowed from the host. Do those four and your plugin keeps working through releases\nyou were not part of.",[14,871,873],{"id":872},"frequently-asked-questions","Frequently asked questions",[875,876,878],"h3",{"id":877},"why-does-the-host-not-see-my-plugin","Why does the host not see my plugin?",[10,880,881,882,63,884,886,887,67],{},"Almost always the environment. An isolated host install has its own virtual environment, and a\nplugin installed elsewhere is invisible to it — use ",[60,883,62],{},[60,885,66],{},". After\nthat, check the group name character by character, and confirm the entry point resolves with\n",[60,888,889],{},"python -c \"from mytool_deploy.cli import plugin\"",[875,891,893],{"id":892},"can-a-plugin-override-a-built-in-command","Can a plugin override a built-in command?",[10,895,896],{},"Most hosts refuse, and rightly. If yours allows it, do not: users expect a documented command to do\nwhat the documentation says, and a plugin silently replacing it is the hardest kind of bug to\ndiagnose. Add a distinct command instead.",[875,898,900],{"id":899},"should-the-plugin-depend-on-the-host-at-all","Should the plugin depend on the host at all?",[10,902,903],{},"Yes, with a range. You import its types and extension points, so the dependency is real, and\ndeclaring it means the resolver refuses an incompatible combination rather than letting a user\ndiscover it at run time.",[875,905,907],{"id":906},"how-do-i-develop-against-an-unreleased-host-version","How do I develop against an unreleased host version?",[10,909,910],{},"Install both editable in one environment: the host from a checkout, your plugin from yours. That is\nalso the arrangement for contributing an extension point upstream — you can develop both sides in\none loop and split the changes into two pull requests afterwards.",[875,912,914],{"id":913},"what-if-the-host-has-no-plugin-system","What if the host has no plugin system?",[10,916,917],{},"Then your options are a wrapper CLI that shells out, or a pull request adding an extension point.\nThe wrapper is quick and fragile — it depends on the host's output format and flags — so if the\nhost is maintained, proposing an entry-point group is usually the better investment.",[875,919,921],{"id":920},"how-should-a-plugin-be-versioned-relative-to-the-host","How should a plugin be versioned relative to the host?",[10,923,924],{},"Independently. Your plugin has its own release cycle and its own semantic version; the relationship\nto the host is expressed by the dependency range, not by matching numbers. Matching versions looks\ntidy for one release and becomes a lie the moment either side needs a patch the other does not.",[875,926,928],{"id":927},"can-two-plugins-depend-on-incompatible-versions-of-a-shared-library","Can two plugins depend on incompatible versions of a shared library?",[10,930,931,932,934],{},"Yes, and it is the failure mode isolated installs make visible: ",[60,933,62],{}," resolves both into\none environment, so a genuine conflict surfaces at install time rather than at run time. That is\nthe right place for it — but it means keeping your own dependency ranges wide enough to coexist\nwith other plugins is part of being a good citizen.",[14,936,938],{"id":937},"related","Related",[19,940,941,949,956,962,969],{},[22,942,943,944],{},"Up: ",[945,946,948],"a",{"href":947},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002F","Plugin architectures for extensible CLIs",[22,950,951,952],{},"Sideways: ",[945,953,955],{"href":954},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","Best practices for Python CLI entry points",[22,957,951,958],{},[945,959,961],{"href":960},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands\u002F","Dependency injection patterns for CLI commands",[22,963,964,965],{},"Related: ",[945,966,968],{"href":967},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx\u002F","Installing and distributing CLIs with pipx",[22,970,964,971],{},[945,972,974],{"href":973},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","Packaging Python CLIs for distribution",[976,977,978],"style",{},"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 .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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":93,"searchDepth":107,"depth":107,"links":980},[981,982,983,984,985,986,987,988,989,990,999],{"id":16,"depth":107,"text":17},{"id":70,"depth":107,"text":71},{"id":81,"depth":107,"text":82},{"id":419,"depth":107,"text":420},{"id":502,"depth":107,"text":503},{"id":549,"depth":107,"text":550},{"id":582,"depth":107,"text":583},{"id":616,"depth":107,"text":617},{"id":865,"depth":107,"text":866},{"id":872,"depth":107,"text":873,"children":991},[992,993,994,995,996,997,998],{"id":877,"depth":121,"text":878},{"id":892,"depth":121,"text":893},{"id":899,"depth":121,"text":900},{"id":906,"depth":121,"text":907},{"id":913,"depth":121,"text":914},{"id":920,"depth":121,"text":921},{"id":927,"depth":121,"text":928},{"id":937,"depth":107,"text":938},"2026-08-01","Extend someone else's Python CLI - declare an entry point, add commands from your own package, version the contract, and test against the host.","advanced",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli",{"title":5,"description":1001},"modern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli\u002Findex",[1010,180,1011,1012],"plugins","packaging","extensibility","TgAhIv0SJBvhc2Q_EjoV5eAWEgQ-brNRj6q7Yjm71b4",[1015,1018,1021,1024,1027,1030,1033,1036,1039,1042,1045,1048,1051,1054,1057,1060,1063,1066,1069,1072,1075,1078,1081,1084,1087,1090,1093,1096,1099,1102,1105,1108,1111,1114,1117,1120,1123,1126,1129,1132,1135,1138,1141,1142,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,1219,1222,1225,1228,1231,1234,1237,1240,1243,1246,1249,1252,1255,1258],{"path":1016,"title":1017},"\u002Fabout","About Python CLI Toolcraft",{"path":1019,"title":1020},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1022,"title":1023},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1025,"title":1026},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1028,"title":1029},"\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":1031,"title":1032},"\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":1034,"title":1035},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1037,"title":1038},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1040,"title":1041},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1043,"title":1044},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1046,"title":1047},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1049,"title":1050},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1052,"title":1053},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1055,"title":1056},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1058,"title":1059},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1061,"title":1062},"\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":1064,"title":1065},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1067,"title":1068},"\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":1070,"title":1071},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1073,"title":1074},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1076,"title":1077},"\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":1079,"title":1080},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1082,"title":1083},"\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":1085,"title":1086},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1088,"title":1089},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1091,"title":1092},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1094,"title":1095},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1097,"title":1098},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1100,"title":1101},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1103,"title":1104},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1106,"title":1107},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1109,"title":1110},"\u002F","Python CLI Toolcraft",{"path":1112,"title":1113},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1115,"title":1116},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1118,"title":1119},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1121,"title":1122},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1124,"title":1125},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1127,"title":1128},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1130,"title":1131},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1133,"title":1134},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1136,"title":1137},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1139,"title":1140},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1006,"title":5},{"path":1143,"title":955},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points",{"path":1145,"title":1146},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1148,"title":1149},"\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":1151,"title":1152},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1154,"title":1155},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1157,"title":1158},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1160,"title":1161},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1163,"title":1164},"\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":1166,"title":1167},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1169,"title":1170},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1172,"title":1173},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1175,"title":1176},"\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":1178,"title":1179},"\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":1181,"title":1182},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1184,"title":1185},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1187,"title":1188},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1190,"title":1191},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1193,"title":1194},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1196,"title":1197},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1199,"title":1200},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1202,"title":1203},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1205,"title":1206},"\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":1208,"title":1209},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1211,"title":1212},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1214,"title":1215},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1217,"title":1218},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1220,"title":1221},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1223,"title":1224},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1226,"title":1227},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1229,"title":1230},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1232,"title":1233},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1235,"title":1236},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1238,"title":1239},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1241,"title":1242},"\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":1244,"title":1245},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1247,"title":1248},"\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":1250,"title":1251},"\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":1253,"title":1254},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1256,"title":1257},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1259,"title":1260},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690032]