[{"data":1,"prerenderedAt":1919},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli\u002F":3,"content-directory":1373},{"id":4,"title":5,"body":6,"date":1359,"description":1360,"difficulty":1361,"draft":1362,"extension":1363,"meta":1364,"navigation":351,"path":1365,"seo":1366,"stem":1367,"tags":1368,"updated":1359,"__hash__":1372},"content\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli\u002Findex.md","Writing pyproject.toml Metadata for a Python CLI",{"type":7,"value":8,"toc":1335},"minimark",[9,34,39,60,67,71,95,99,589,593,616,639,662,665,683,703,724,728,731,745,758,761,794,797,801,804,836,840,843,914,917,1154,1169,1173,1188,1192,1196,1205,1217,1229,1233,1244,1248,1284,1291,1297,1301,1331],[10,11,12,16,17,20,21,24,25,27,28,33],"p",{},[13,14,15],"code",{},"pyproject.toml"," is where a CLI stops being code on your machine and becomes something other people install. The ",[13,18,19],{},"[project]"," table decides what the tool is called on PyPI, which Python versions can install it, what gets pulled in alongside it, what command appears on the user's ",[13,22,23],{},"PATH",", and what people see when they find it. Most fields are simple, but several have consequences that are easy to miss — an upper bound on Python that locks out the next release, an exact pin that conflicts with everything else in the user's environment, a development tool listed as a runtime dependency. This guide walks through a complete ",[13,26,15],{}," for a CLI, field by field, explaining what each decision does for users. It belongs to the ",[29,30,32],"a",{"href":31},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","packaging Python CLIs for distribution topic",".",[35,36,38],"h2",{"id":37},"prerequisites","Prerequisites",[40,41,42,50],"ul",{},[43,44,45,46,49],"li",{},"A CLI in a ",[13,47,48],{},"src\u002F"," layout with a function or Typer\u002FClick app to expose as a command.",[43,51,52,53,56,57,59],{},"A build backend — the examples use ",[13,54,55],{},"hatchling","; everything in ",[13,58,19],{}," is standard (PEP 621) and works with any backend.",[35,61,63,64,66],{"id":62},"the-anatomy-of-the-project-table","The anatomy of the ",[13,65,19],{}," table",[68,69],"inline-diagram",{"name":70},"pp-anatomy",[10,72,73,74,78,79,82,83,86,87,90,91,94],{},"Every field falls into one of three groups: ",[75,76,77],"strong",{},"identity"," (what the package is), ",[75,80,81],{},"installability"," (who can install it and what comes with it), and ",[75,84,85],{},"discoverability"," (how people find it and decide to trust it). For a CLI, two fields decide whether it works at all — ",[13,88,89],{},"requires-python"," and ",[13,92,93],{},"[project.scripts]"," — and the rest decide whether people choose it and whether it coexists with everything else they have installed.",[35,96,98],{"id":97},"the-recipe","The recipe",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-toml shiki shiki-themes github-light github-dark","[project]\nname = \"deploy-helper\"\nversion = \"2.4.0\"\ndescription = \"Deploy static sites to object storage with previews and rollbacks\"\nreadme = \"README.md\"\nlicense = \"MIT\"\nlicense-files = [\"LICENSE\"]\nauthors = [{ name = \"Platform Team\", email = \"platform@example.com\" }]\nrequires-python = \">=3.10\"\nkeywords = [\"deploy\", \"static-site\", \"s3\", \"cli\"]\nclassifiers = [\n  \"Development Status :: 5 - Production\u002FStable\",\n  \"Environment :: Console\",\n  \"Intended Audience :: Developers\",\n  \"Operating System :: OS Independent\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.14\",\n  \"Topic :: Software Development :: Build Tools\",\n]\ndependencies = [\n  \"typer>=0.12\",\n  \"httpx>=0.27\",\n  \"rich>=13.7\",\n]\n\n[project.optional-dependencies]\ns3 = [\"boto3>=1.34\"]\ngcs = [\"google-cloud-storage>=2.16\"]\n\n[project.scripts]\ndeploy-helper = \"deploy_helper.cli:app\"\ndh = \"deploy_helper.cli:app\"\n\n[project.entry-points.\"deploy_helper.backends\"]\nlocal = \"deploy_helper.backends.local:LocalBackend\"\n\n[project.urls]\nHomepage = \"https:\u002F\u002Fgithub.com\u002Facme\u002Fdeploy-helper\"\nDocumentation = \"https:\u002F\u002Facme.github.io\u002Fdeploy-helper\u002F\"\nChangelog = \"https:\u002F\u002Fgithub.com\u002Facme\u002Fdeploy-helper\u002Fblob\u002Fmain\u002FCHANGELOG.md\"\nIssues = \"https:\u002F\u002Fgithub.com\u002Facme\u002Fdeploy-helper\u002Fissues\"\n\n[dependency-groups]\ndev = [\"pytest>=8\", \"ruff>=0.6\", \"mypy>=1.10\"]\n\n[build-system]\nrequires = [\"hatchling>=1.27\"]\nbuild-backend = \"hatchling.build\"\n","toml","",[13,107,108,124,134,143,152,161,170,181,199,208,235,241,250,258,266,274,282,290,298,306,311,317,325,333,341,346,353,367,378,389,394,408,417,425,430,454,463,468,482,491,500,509,518,523,533,554,559,569,580],{"__ignoreMap":105},[109,110,113,117,121],"span",{"class":111,"line":112},"line",1,[109,114,116],{"class":115},"sVt8B","[",[109,118,120],{"class":119},"sScJk","project",[109,122,123],{"class":115},"]\n",[109,125,127,130],{"class":111,"line":126},2,[109,128,129],{"class":115},"name = ",[109,131,133],{"class":132},"sZZnC","\"deploy-helper\"\n",[109,135,137,140],{"class":111,"line":136},3,[109,138,139],{"class":115},"version = ",[109,141,142],{"class":132},"\"2.4.0\"\n",[109,144,146,149],{"class":111,"line":145},4,[109,147,148],{"class":115},"description = ",[109,150,151],{"class":132},"\"Deploy static sites to object storage with previews and rollbacks\"\n",[109,153,155,158],{"class":111,"line":154},5,[109,156,157],{"class":115},"readme = ",[109,159,160],{"class":132},"\"README.md\"\n",[109,162,164,167],{"class":111,"line":163},6,[109,165,166],{"class":115},"license = ",[109,168,169],{"class":132},"\"MIT\"\n",[109,171,173,176,179],{"class":111,"line":172},7,[109,174,175],{"class":115},"license-files = [",[109,177,178],{"class":132},"\"LICENSE\"",[109,180,123],{"class":115},[109,182,184,187,190,193,196],{"class":111,"line":183},8,[109,185,186],{"class":115},"authors = [{ name = ",[109,188,189],{"class":132},"\"Platform Team\"",[109,191,192],{"class":115},", email = ",[109,194,195],{"class":132},"\"platform@example.com\"",[109,197,198],{"class":115}," }]\n",[109,200,202,205],{"class":111,"line":201},9,[109,203,204],{"class":115},"requires-python = ",[109,206,207],{"class":132},"\">=3.10\"\n",[109,209,211,214,217,220,223,225,228,230,233],{"class":111,"line":210},10,[109,212,213],{"class":115},"keywords = [",[109,215,216],{"class":132},"\"deploy\"",[109,218,219],{"class":115},", ",[109,221,222],{"class":132},"\"static-site\"",[109,224,219],{"class":115},[109,226,227],{"class":132},"\"s3\"",[109,229,219],{"class":115},[109,231,232],{"class":132},"\"cli\"",[109,234,123],{"class":115},[109,236,238],{"class":111,"line":237},11,[109,239,240],{"class":115},"classifiers = [\n",[109,242,244,247],{"class":111,"line":243},12,[109,245,246],{"class":132},"  \"Development Status :: 5 - Production\u002FStable\"",[109,248,249],{"class":115},",\n",[109,251,253,256],{"class":111,"line":252},13,[109,254,255],{"class":132},"  \"Environment :: Console\"",[109,257,249],{"class":115},[109,259,261,264],{"class":111,"line":260},14,[109,262,263],{"class":132},"  \"Intended Audience :: Developers\"",[109,265,249],{"class":115},[109,267,269,272],{"class":111,"line":268},15,[109,270,271],{"class":132},"  \"Operating System :: OS Independent\"",[109,273,249],{"class":115},[109,275,277,280],{"class":111,"line":276},16,[109,278,279],{"class":132},"  \"Programming Language :: Python :: 3\"",[109,281,249],{"class":115},[109,283,285,288],{"class":111,"line":284},17,[109,286,287],{"class":132},"  \"Programming Language :: Python :: 3.10\"",[109,289,249],{"class":115},[109,291,293,296],{"class":111,"line":292},18,[109,294,295],{"class":132},"  \"Programming Language :: Python :: 3.14\"",[109,297,249],{"class":115},[109,299,301,304],{"class":111,"line":300},19,[109,302,303],{"class":132},"  \"Topic :: Software Development :: Build Tools\"",[109,305,249],{"class":115},[109,307,309],{"class":111,"line":308},20,[109,310,123],{"class":115},[109,312,314],{"class":111,"line":313},21,[109,315,316],{"class":115},"dependencies = [\n",[109,318,320,323],{"class":111,"line":319},22,[109,321,322],{"class":132},"  \"typer>=0.12\"",[109,324,249],{"class":115},[109,326,328,331],{"class":111,"line":327},23,[109,329,330],{"class":132},"  \"httpx>=0.27\"",[109,332,249],{"class":115},[109,334,336,339],{"class":111,"line":335},24,[109,337,338],{"class":132},"  \"rich>=13.7\"",[109,340,249],{"class":115},[109,342,344],{"class":111,"line":343},25,[109,345,123],{"class":115},[109,347,349],{"class":111,"line":348},26,[109,350,352],{"emptyLinePlaceholder":351},true,"\n",[109,354,356,358,360,362,365],{"class":111,"line":355},27,[109,357,116],{"class":115},[109,359,120],{"class":119},[109,361,33],{"class":115},[109,363,364],{"class":119},"optional-dependencies",[109,366,123],{"class":115},[109,368,370,373,376],{"class":111,"line":369},28,[109,371,372],{"class":115},"s3 = [",[109,374,375],{"class":132},"\"boto3>=1.34\"",[109,377,123],{"class":115},[109,379,381,384,387],{"class":111,"line":380},29,[109,382,383],{"class":115},"gcs = [",[109,385,386],{"class":132},"\"google-cloud-storage>=2.16\"",[109,388,123],{"class":115},[109,390,392],{"class":111,"line":391},30,[109,393,352],{"emptyLinePlaceholder":351},[109,395,397,399,401,403,406],{"class":111,"line":396},31,[109,398,116],{"class":115},[109,400,120],{"class":119},[109,402,33],{"class":115},[109,404,405],{"class":119},"scripts",[109,407,123],{"class":115},[109,409,411,414],{"class":111,"line":410},32,[109,412,413],{"class":115},"deploy-helper = ",[109,415,416],{"class":132},"\"deploy_helper.cli:app\"\n",[109,418,420,423],{"class":111,"line":419},33,[109,421,422],{"class":115},"dh = ",[109,424,416],{"class":132},[109,426,428],{"class":111,"line":427},34,[109,429,352],{"emptyLinePlaceholder":351},[109,431,433,435,437,439,442,444,447,449,452],{"class":111,"line":432},35,[109,434,116],{"class":115},[109,436,120],{"class":119},[109,438,33],{"class":115},[109,440,441],{"class":119},"entry-points",[109,443,33],{"class":115},[109,445,446],{"class":119},"\"deploy_helper",[109,448,33],{"class":115},[109,450,451],{"class":119},"backends\"",[109,453,123],{"class":115},[109,455,457,460],{"class":111,"line":456},36,[109,458,459],{"class":115},"local = ",[109,461,462],{"class":132},"\"deploy_helper.backends.local:LocalBackend\"\n",[109,464,466],{"class":111,"line":465},37,[109,467,352],{"emptyLinePlaceholder":351},[109,469,471,473,475,477,480],{"class":111,"line":470},38,[109,472,116],{"class":115},[109,474,120],{"class":119},[109,476,33],{"class":115},[109,478,479],{"class":119},"urls",[109,481,123],{"class":115},[109,483,485,488],{"class":111,"line":484},39,[109,486,487],{"class":115},"Homepage = ",[109,489,490],{"class":132},"\"https:\u002F\u002Fgithub.com\u002Facme\u002Fdeploy-helper\"\n",[109,492,494,497],{"class":111,"line":493},40,[109,495,496],{"class":115},"Documentation = ",[109,498,499],{"class":132},"\"https:\u002F\u002Facme.github.io\u002Fdeploy-helper\u002F\"\n",[109,501,503,506],{"class":111,"line":502},41,[109,504,505],{"class":115},"Changelog = ",[109,507,508],{"class":132},"\"https:\u002F\u002Fgithub.com\u002Facme\u002Fdeploy-helper\u002Fblob\u002Fmain\u002FCHANGELOG.md\"\n",[109,510,512,515],{"class":111,"line":511},42,[109,513,514],{"class":115},"Issues = ",[109,516,517],{"class":132},"\"https:\u002F\u002Fgithub.com\u002Facme\u002Fdeploy-helper\u002Fissues\"\n",[109,519,521],{"class":111,"line":520},43,[109,522,352],{"emptyLinePlaceholder":351},[109,524,526,528,531],{"class":111,"line":525},44,[109,527,116],{"class":115},[109,529,530],{"class":119},"dependency-groups",[109,532,123],{"class":115},[109,534,536,539,542,544,547,549,552],{"class":111,"line":535},45,[109,537,538],{"class":115},"dev = [",[109,540,541],{"class":132},"\"pytest>=8\"",[109,543,219],{"class":115},[109,545,546],{"class":132},"\"ruff>=0.6\"",[109,548,219],{"class":115},[109,550,551],{"class":132},"\"mypy>=1.10\"",[109,553,123],{"class":115},[109,555,557],{"class":111,"line":556},46,[109,558,352],{"emptyLinePlaceholder":351},[109,560,562,564,567],{"class":111,"line":561},47,[109,563,116],{"class":115},[109,565,566],{"class":119},"build-system",[109,568,123],{"class":115},[109,570,572,575,578],{"class":111,"line":571},48,[109,573,574],{"class":115},"requires = [",[109,576,577],{"class":132},"\"hatchling>=1.27\"",[109,579,123],{"class":115},[109,581,583,586],{"class":111,"line":582},49,[109,584,585],{"class":115},"build-backend = ",[109,587,588],{"class":132},"\"hatchling.build\"\n",[590,591,592],"h3",{"id":77},"Identity",[10,594,595,600,601,604,605,608,609,219,612,615],{},[75,596,597],{},[13,598,599],{},"name"," is the distribution name on PyPI and what users type in ",[13,602,603],{},"pipx install deploy-helper",". It does not have to match the importable package (",[13,606,607],{},"deploy_helper",") or the command (",[13,610,611],{},"deploy-helper",[13,613,614],{},"dh","), but keeping them aligned saves confusion. Check PyPI for collisions before you commit to one; renaming later is painful.",[10,617,618,623,624,627,628,632,633,638],{},[75,619,620],{},[13,621,622],{},"description"," is the one line people see in search results and ",[13,625,626],{},"pip show",". Lead with the verb — what the tool ",[629,630,631],"em",{},"does"," — rather than \"A CLI for...\". ",[75,634,635],{},[13,636,637],{},"readme"," becomes the PyPI project page; make sure it shows the command being run in its first screen.",[10,640,641,646,647,651,652,657,658,661],{},[75,642,643],{},[13,644,645],{},"version"," can be static, as here, or derived from git tags; see ",[29,648,650],{"href":649},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs\u002F","deriving versions from git tags with hatch-vcs",". ",[75,653,654],{},[13,655,656],{},"license"," uses an SPDX expression (PEP 639), and ",[13,659,660],{},"license-files"," includes the text in the distribution.",[590,663,664],{"id":81},"Installability",[10,666,667,671,672,674,675,678,679,682],{},[75,668,669],{},[13,670,89],{}," states the oldest supported Python. Installers use it to choose a compatible release: a user on Python 3.9 asking for ",[13,673,611],{}," gets the newest release that still supports 3.9, not a broken install. Keep it in sync with your CI matrix. ",[75,676,677],{},"Do not add an upper bound"," such as ",[13,680,681],{},"\u003C3.14",": it does not make your tool work on 3.14, it only stops people trying — including when your tool would have worked perfectly — and resolvers treat it in surprising ways.",[10,684,685,690,691,694,695,698,699,702],{},[75,686,687],{},[13,688,689],{},"dependencies"," should be ",[75,692,693],{},"ranges with lower bounds",", not exact pins. ",[13,696,697],{},"typer>=0.12"," means \"we need features from 0.12\"; ",[13,700,701],{},"typer==0.12.3"," means \"we conflict with every other tool that wants a different patch release\". Your lockfile pins exact versions for development and CI; the published metadata should be as permissive as is true. Add an upper bound only for a known incompatibility, and document why.",[10,704,705,710,711,714,715,719,720,723],{},[75,706,707],{},[13,708,709],{},"[project.optional-dependencies]"," defines extras that users opt into: ",[13,712,713],{},"pipx install \"deploy-helper[s3]\"",". Use them for heavy, backend-specific dependencies most users do not need, and import those dependencies lazily so the base tool stays small, as in ",[29,716,718],{"href":717},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002F","reducing CLI dependency weight",". Development tools never belong here — they go in ",[13,721,722],{},"[dependency-groups]",", which are not published at all.",[590,725,727],{"id":726},"the-command-itself","The command itself",[68,729],{"name":730},"pp-scripts-matrix",[10,732,733,737,738,740,741,33],{},[75,734,735],{},[13,736,93],{}," is what makes this a CLI. Each entry creates an executable on the user's ",[13,739,23],{}," at install time — a small launcher that imports the named object and calls it. Two entries pointing at the same app give users a long name and a short alias. The object can be a Typer app, a Click group, or any callable; the conventions are covered in ",[29,742,744],{"href":743},"\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",[10,746,747,752,753,757],{},[75,748,749],{},[13,750,751],{},"[project.entry-points.\"group\"]"," installs no command; it publishes metadata that your CLI can discover at runtime. This is how plugins announce themselves, as described in ",[29,754,756],{"href":755},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points\u002F","discovering plugins with entry points",". Declaring your own built-in backends through the same mechanism means built-ins and third-party plugins are loaded the same way.",[590,759,760],{"id":85},"Discoverability",[10,762,763,766,767,770,771,774,775,780,781,786,787,90,790,793],{},[75,764,765],{},"Classifiers"," feed PyPI's filters. ",[13,768,769],{},"Environment :: Console"," marks a command-line tool. The ",[13,772,773],{},"Programming Language :: Python :: 3.x"," entries should match your tested versions. ",[75,776,777],{},[13,778,779],{},"keywords"," help search, and ",[75,782,783],{},[13,784,785],{},"[project.urls]"," populate the sidebar of the PyPI page — ",[13,788,789],{},"Changelog",[13,791,792],{},"Issues"," in particular are what users look for before upgrading.",[68,795],{"name":796},"pp-metadata-checklist",[35,798,800],{"id":799},"ux-considerations","UX considerations",[10,802,803],{},"Metadata is user experience that happens before the first run:",[40,805,806,812,818,827],{},[43,807,808,811],{},[75,809,810],{},"Name the command for typing."," Short, lowercase, hyphenated, unlikely to collide with system tools. Offer a short alias if the canonical name is long.",[43,813,814,817],{},[75,815,816],{},"Keep base installs light."," Every runtime dependency is installed for every user and imported on every run you do not make lazy. Push optional backends into extras.",[43,819,820,823,824,826],{},[75,821,822],{},"Make the PyPI page useful."," The README's first screen should show installation (",[13,825,603],{},") and one real command with its output. People decide in seconds.",[43,828,829,832,833,835],{},[75,830,831],{},"Tell installers the truth about Python support."," A correct ",[13,834,89],{}," means users on old interpreters get an older working release instead of a confusing failure.",[35,837,839],{"id":838},"testing-the-behaviour","Testing the behaviour",[10,841,842],{},"Metadata mistakes are cheap to catch before release:",[100,844,848],{"className":845,"code":846,"language":847,"meta":105,"style":105},"language-bash shiki shiki-themes github-light github-dark","uvx validate-pyproject pyproject.toml          # schema check for [project] and friends\nuv build                                       # does it build?\nuvx twine check dist\u002F*                         # will PyPI render the README?\nunzip -p dist\u002F*.whl '*.dist-info\u002Fentry_points.txt'\n","bash",[13,849,850,865,876,896],{"__ignoreMap":105},[109,851,852,855,858,861],{"class":111,"line":112},[109,853,854],{"class":119},"uvx",[109,856,857],{"class":132}," validate-pyproject",[109,859,860],{"class":132}," pyproject.toml",[109,862,864],{"class":863},"sJ8bj","          # schema check for [project] and friends\n",[109,866,867,870,873],{"class":111,"line":126},[109,868,869],{"class":119},"uv",[109,871,872],{"class":132}," build",[109,874,875],{"class":863},"                                       # does it build?\n",[109,877,878,880,883,886,889,893],{"class":111,"line":136},[109,879,854],{"class":119},[109,881,882],{"class":132}," twine",[109,884,885],{"class":132}," check",[109,887,888],{"class":132}," dist\u002F",[109,890,892],{"class":891},"sj4cs","*",[109,894,895],{"class":863},"                         # will PyPI render the README?\n",[109,897,898,901,904,906,908,911],{"class":111,"line":145},[109,899,900],{"class":119},"unzip",[109,902,903],{"class":891}," -p",[109,905,888],{"class":132},[109,907,892],{"class":891},[109,909,910],{"class":132},".whl",[109,912,913],{"class":132}," '*.dist-info\u002Fentry_points.txt'\n",[10,915,916],{},"And a test that the declared command actually exists and runs, using the metadata of the installed package rather than the source tree:",[100,918,922],{"className":919,"code":920,"language":921,"meta":105,"style":105},"language-python shiki shiki-themes github-light github-dark","# tests\u002Ftest_metadata.py\nfrom importlib.metadata import distribution\n\n\ndef test_console_scripts_are_declared():\n    eps = distribution(\"deploy-helper\").entry_points\n    scripts = {ep.name: ep.value for ep in eps if ep.group == \"console_scripts\"}\n    assert scripts == {\"deploy-helper\": \"deploy_helper.cli:app\", \"dh\": \"deploy_helper.cli:app\"}\n    for ep in eps:\n        if ep.group == \"console_scripts\":\n            assert callable(ep.load())\n\n\ndef test_requires_python_matches_classifiers():\n    meta = distribution(\"deploy-helper\").metadata\n    assert meta[\"Requires-Python\"] == \">=3.10\"\n    assert \"Programming Language :: Python :: 3.10\" in meta.get_all(\"Classifier\")\n","python",[13,923,924,929,944,948,952,963,980,1017,1049,1061,1075,1086,1090,1094,1103,1117,1135],{"__ignoreMap":105},[109,925,926],{"class":111,"line":112},[109,927,928],{"class":863},"# tests\u002Ftest_metadata.py\n",[109,930,931,935,938,941],{"class":111,"line":126},[109,932,934],{"class":933},"szBVR","from",[109,936,937],{"class":115}," importlib.metadata ",[109,939,940],{"class":933},"import",[109,942,943],{"class":115}," distribution\n",[109,945,946],{"class":111,"line":136},[109,947,352],{"emptyLinePlaceholder":351},[109,949,950],{"class":111,"line":145},[109,951,352],{"emptyLinePlaceholder":351},[109,953,954,957,960],{"class":111,"line":154},[109,955,956],{"class":933},"def",[109,958,959],{"class":119}," test_console_scripts_are_declared",[109,961,962],{"class":115},"():\n",[109,964,965,968,971,974,977],{"class":111,"line":163},[109,966,967],{"class":115},"    eps ",[109,969,970],{"class":933},"=",[109,972,973],{"class":115}," distribution(",[109,975,976],{"class":132},"\"deploy-helper\"",[109,978,979],{"class":115},").entry_points\n",[109,981,982,985,987,990,993,996,999,1002,1005,1008,1011,1014],{"class":111,"line":172},[109,983,984],{"class":115},"    scripts ",[109,986,970],{"class":933},[109,988,989],{"class":115}," {ep.name: ep.value ",[109,991,992],{"class":933},"for",[109,994,995],{"class":115}," ep ",[109,997,998],{"class":933},"in",[109,1000,1001],{"class":115}," eps ",[109,1003,1004],{"class":933},"if",[109,1006,1007],{"class":115}," ep.group ",[109,1009,1010],{"class":933},"==",[109,1012,1013],{"class":132}," \"console_scripts\"",[109,1015,1016],{"class":115},"}\n",[109,1018,1019,1022,1025,1027,1030,1032,1035,1038,1040,1043,1045,1047],{"class":111,"line":183},[109,1020,1021],{"class":933},"    assert",[109,1023,1024],{"class":115}," scripts ",[109,1026,1010],{"class":933},[109,1028,1029],{"class":115}," {",[109,1031,976],{"class":132},[109,1033,1034],{"class":115},": ",[109,1036,1037],{"class":132},"\"deploy_helper.cli:app\"",[109,1039,219],{"class":115},[109,1041,1042],{"class":132},"\"dh\"",[109,1044,1034],{"class":115},[109,1046,1037],{"class":132},[109,1048,1016],{"class":115},[109,1050,1051,1054,1056,1058],{"class":111,"line":201},[109,1052,1053],{"class":933},"    for",[109,1055,995],{"class":115},[109,1057,998],{"class":933},[109,1059,1060],{"class":115}," eps:\n",[109,1062,1063,1066,1068,1070,1072],{"class":111,"line":210},[109,1064,1065],{"class":933},"        if",[109,1067,1007],{"class":115},[109,1069,1010],{"class":933},[109,1071,1013],{"class":132},[109,1073,1074],{"class":115},":\n",[109,1076,1077,1080,1083],{"class":111,"line":237},[109,1078,1079],{"class":933},"            assert",[109,1081,1082],{"class":891}," callable",[109,1084,1085],{"class":115},"(ep.load())\n",[109,1087,1088],{"class":111,"line":243},[109,1089,352],{"emptyLinePlaceholder":351},[109,1091,1092],{"class":111,"line":252},[109,1093,352],{"emptyLinePlaceholder":351},[109,1095,1096,1098,1101],{"class":111,"line":260},[109,1097,956],{"class":933},[109,1099,1100],{"class":119}," test_requires_python_matches_classifiers",[109,1102,962],{"class":115},[109,1104,1105,1108,1110,1112,1114],{"class":111,"line":268},[109,1106,1107],{"class":115},"    meta ",[109,1109,970],{"class":933},[109,1111,973],{"class":115},[109,1113,976],{"class":132},[109,1115,1116],{"class":115},").metadata\n",[109,1118,1119,1121,1124,1127,1130,1132],{"class":111,"line":276},[109,1120,1021],{"class":933},[109,1122,1123],{"class":115}," meta[",[109,1125,1126],{"class":132},"\"Requires-Python\"",[109,1128,1129],{"class":115},"] ",[109,1131,1010],{"class":933},[109,1133,1134],{"class":132}," \">=3.10\"\n",[109,1136,1137,1139,1142,1145,1148,1151],{"class":111,"line":284},[109,1138,1021],{"class":933},[109,1140,1141],{"class":132}," \"Programming Language :: Python :: 3.10\"",[109,1143,1144],{"class":933}," in",[109,1146,1147],{"class":115}," meta.get_all(",[109,1149,1150],{"class":132},"\"Classifier\"",[109,1152,1153],{"class":115},")\n",[10,1155,1156,1157,1160,1161,1164,1165,33],{},"Loading each entry point catches typos in module paths — ",[13,1158,1159],{},"deploy_helper.cli:ap"," instead of ",[13,1162,1163],{},":app"," — that otherwise surface only when a user types the command. The full artefact-level check is in ",[29,1166,1168],{"href":1167},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F","smoke-testing the built wheel in CI",[35,1170,1172],{"id":1171},"conclusion","Conclusion",[10,1174,1175,1176,1178,1179,1181,1182,90,1184,1187],{},"A CLI's ",[13,1177,15],{}," decides who can install it, what comes with it, what command appears and how people find it. Use lower-bounded ranges rather than pins, set ",[13,1180,89],{}," to your oldest tested version without an upper bound, put optional backends in extras and tooling in dependency groups, declare commands and plugin entry points under ",[13,1183,93],{},[13,1185,1186],{},"[project.entry-points]",", and fill in the description, classifiers and URLs that make the PyPI page useful. Validate it, build it, and load the entry points in a test before every release.",[35,1189,1191],{"id":1190},"frequently-asked-questions","Frequently asked questions",[590,1193,1195],{"id":1194},"should-the-command-name-match-the-package-name","Should the command name match the package name?",[10,1197,1198,1199,1201,1202,1204],{},"It is convenient but not required. Choose the command for typing (",[13,1200,614],{},") and the distribution name for searching (",[13,1203,611],{},"); many tools have both, via two script entries.",[590,1206,1208,1209,1212,1213,1216],{"id":1207},"do-i-need-setuppy-or-setupcfg","Do I need ",[13,1210,1211],{},"setup.py"," or ",[13,1214,1215],{},"setup.cfg","?",[10,1218,1219,1220,1222,1223,1225,1226,1228],{},"No. Every modern backend reads ",[13,1221,19],{}," from ",[13,1224,15],{},". A ",[13,1227,1211],{}," is only needed for unusual custom build steps, which most CLIs do not have.",[590,1230,1232],{"id":1231},"how-do-i-include-non-python-files-like-templates","How do I include non-Python files like templates?",[10,1234,1235,1236,1239,1240,33],{},"Put them inside the package directory and read them with ",[13,1237,1238],{},"importlib.resources","; most backends include package files by default. Details and backend differences are in ",[29,1241,1243],{"href":1242},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources\u002F","bundling data files with importlib.resources",[590,1245,1247],{"id":1246},"can-i-read-the-metadata-from-inside-the-cli","Can I read the metadata from inside the CLI?",[10,1249,1250,1251,1254,1255,1258,1259,1262,1263,1266,1267,90,1270,1273,1274,1276,1277,1279,1280,33],{},"Yes, and it is the right way to avoid duplicating information. ",[13,1252,1253],{},"importlib.metadata.metadata(\"deploy-helper\")"," returns the installed metadata, so ",[13,1256,1257],{},"--version"," can read ",[13,1260,1261],{},"Version",", an ",[13,1264,1265],{},"about"," command can print the ",[13,1268,1269],{},"Summary",[13,1271,1272],{},"Project-URL"," entries, and an error message can point users at the ",[13,1275,792],{}," URL — all from the single source of truth in ",[13,1278,15],{},". The runtime side is covered in ",[29,1281,1283],{"href":1282},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata\u002F","exposing version info and build metadata",[590,1285,1287,1288,1290],{"id":1286},"what-is-license-files-for-and-do-i-need-it","What is ",[13,1289,660],{}," for, and do I need it?",[10,1292,1293,1294,1296],{},"It lists license text files to include in the distribution's metadata directory, alongside the SPDX ",[13,1295,656],{}," expression. Include it: many organisations' compliance tooling reads license files from installed packages.",[35,1298,1300],{"id":1299},"related","Related",[40,1302,1303,1309,1315,1320,1326],{},[43,1304,1305,1306],{},"Up: ",[29,1307,1308],{"href":31},"Packaging Python CLIs for distribution",[43,1310,1311],{},[29,1312,1314],{"href":1313},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F","Building wheels and sdists for Python CLIs",[43,1316,1317],{},[29,1318,1319],{"href":1242},"Bundling data files with importlib.resources",[43,1321,1322],{},[29,1323,1325],{"href":1324},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi\u002F","Publishing a Python CLI to PyPI",[43,1327,1328],{},[29,1329,1330],{"href":743},"Best practices for Python CLI entry points",[1332,1333,1334],"style",{},"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 .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}",{"title":105,"searchDepth":126,"depth":126,"links":1336},[1337,1338,1340,1346,1347,1348,1349,1358],{"id":37,"depth":126,"text":38},{"id":62,"depth":126,"text":1339},"The anatomy of the [project] table",{"id":97,"depth":126,"text":98,"children":1341},[1342,1343,1344,1345],{"id":77,"depth":136,"text":592},{"id":81,"depth":136,"text":664},{"id":726,"depth":136,"text":727},{"id":85,"depth":136,"text":760},{"id":799,"depth":126,"text":800},{"id":838,"depth":126,"text":839},{"id":1171,"depth":126,"text":1172},{"id":1190,"depth":126,"text":1191,"children":1350},[1351,1352,1354,1355,1356],{"id":1194,"depth":136,"text":1195},{"id":1207,"depth":136,"text":1353},"Do I need setup.py or setup.cfg?",{"id":1231,"depth":136,"text":1232},{"id":1246,"depth":136,"text":1247},{"id":1286,"depth":136,"text":1357},"What is license-files for, and do I need it?",{"id":1299,"depth":126,"text":1300},"2026-09-18","A complete, annotated pyproject.toml for a Python CLI: name and description, requires-python, dependency ranges, console scripts, extras, classifiers and URLs.","beginner",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli",{"title":5,"description":1360},"project-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli\u002Findex",[1369,1370,1371,441],"pyproject","packaging","metadata","s4VkUlawTVHO2bfuMB6MM5OwhRo87GoG4b46uc5Y92E",[1374,1377,1380,1383,1386,1389,1392,1395,1398,1401,1404,1407,1410,1413,1416,1419,1422,1425,1428,1431,1434,1437,1440,1443,1446,1449,1452,1455,1458,1461,1464,1467,1470,1473,1476,1479,1482,1485,1488,1491,1494,1497,1500,1503,1506,1509,1512,1515,1518,1521,1524,1527,1530,1533,1536,1539,1542,1545,1548,1551,1554,1557,1560,1563,1566,1569,1572,1575,1578,1581,1584,1587,1590,1593,1596,1599,1602,1605,1608,1611,1614,1617,1620,1623,1626,1629,1632,1635,1638,1641,1644,1647,1650,1653,1656,1659,1662,1665,1668,1671,1674,1677,1680,1683,1686,1689,1692,1695,1698,1701,1704,1707,1710,1713,1716,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,1814,1817,1820,1823,1826,1829,1832,1835,1838,1841,1844,1847,1850,1853,1856,1859,1862,1865,1867,1868,1871,1874,1877,1880,1883,1886,1889,1892,1895,1898,1901,1904,1907,1910,1913,1916],{"path":1375,"title":1376},"\u002Fabout","About Python CLI Toolcraft",{"path":1378,"title":1379},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1381,"title":1382},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1384,"title":1385},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1387,"title":1388},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1390,"title":1391},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1393,"title":1394},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":1396,"title":1397},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":1399,"title":1400},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1402,"title":1403},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1405,"title":1406},"\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":1408,"title":1409},"\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":1411,"title":1412},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1414,"title":1415},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1417,"title":1418},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1420,"title":1421},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1423,"title":1424},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1426,"title":1427},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":1429,"title":1430},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1432,"title":1433},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1435,"title":1436},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1438,"title":1439},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":1441,"title":1442},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1444,"title":1445},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1447,"title":1448},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1450,"title":1451},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":1453,"title":1454},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1456,"title":1457},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":1459,"title":1460},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1462,"title":1463},"\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":1465,"title":1466},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1468,"title":1469},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1471,"title":1472},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1474,"title":1475},"\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":1477,"title":1478},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1480,"title":1481},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1483,"title":1484},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1486,"title":1487},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1489,"title":1490},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1492,"title":1493},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":1495,"title":1496},"\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":1498,"title":1499},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1501,"title":1502},"\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":1504,"title":1505},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1507,"title":1508},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":1510,"title":1511},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1513,"title":1514},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1516,"title":1517},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1519,"title":1520},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":1522,"title":1523},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1525,"title":1526},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1528,"title":1529},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1531,"title":1532},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1534,"title":1535},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":1537,"title":1538},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1540,"title":1541},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":1543,"title":1544},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1546,"title":1547},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1549,"title":1550},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1552,"title":1553},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1555,"title":1556},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":1558,"title":1559},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":1561,"title":1562},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1564,"title":1565},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1567,"title":1568},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1570,"title":1571},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1573,"title":1574},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":1576,"title":1577},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1579,"title":1580},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1582,"title":1583},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1585,"title":1586},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1588,"title":1589},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1591,"title":1592},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1594,"title":1595},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1597,"title":1598},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":1600,"title":1601},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1603,"title":1604},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":1606,"title":1607},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1609,"title":1610},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":1612,"title":1613},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1615,"title":1616},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1618,"title":1619},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1621,"title":1622},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1624,"title":1625},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1627,"title":1628},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":1630,"title":1631},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1633,"title":1634},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1636,"title":1637},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":1639,"title":1640},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":1642,"title":1643},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1645,"title":1646},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1648,"title":1649},"\u002F","Python CLI Toolcraft",{"path":1651,"title":1652},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":1654,"title":1655},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1657,"title":1658},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1660,"title":1661},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1663,"title":1664},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1666,"title":1667},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1669,"title":1670},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1672,"title":1673},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1675,"title":1676},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1678,"title":1679},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1681,"title":1682},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1684,"title":1685},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":1687,"title":1688},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":1690,"title":1691},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":1693,"title":1694},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1696,"title":1697},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1699,"title":1700},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1702,"title":1703},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1705,"title":1706},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1708,"title":1709},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1711,"title":1712},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1714,"title":1715},"\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":1717,"title":1330},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points",{"path":1719,"title":1720},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1722,"title":1723},"\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":1725,"title":1726},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1728,"title":1729},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1731,"title":1732},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1734,"title":1735},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":1737,"title":1738},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1740,"title":1741},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1743,"title":1744},"\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":1746,"title":1747},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1749,"title":1750},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1752,"title":1753},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1755,"title":1756},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1758,"title":1759},"\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":1761,"title":1762},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1764,"title":1765},"\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":1767,"title":1768},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1770,"title":1771},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1773,"title":1774},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":1776,"title":1777},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":1779,"title":1780},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":1782,"title":1783},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":1785,"title":1786},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":1788,"title":1789},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":1791,"title":1792},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":1794,"title":1795},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":1797,"title":1798},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1800,"title":1801},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1803,"title":1804},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":1806,"title":1807},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1809,"title":1810},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1812,"title":1813},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1815,"title":1816},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1818,"title":1819},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":1821,"title":1822},"\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":1824,"title":1825},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1827,"title":1828},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":1830,"title":1831},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":1833,"title":1834},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":1836,"title":1837},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":1839,"title":1840},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1842,"title":1843},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":1845,"title":1846},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1848,"title":1849},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1851,"title":1852},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":1854,"title":1855},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1857,"title":1858},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":1860,"title":1861},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1863,"title":1864},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1866,"title":1325},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi",{"path":1365,"title":5},{"path":1869,"title":1870},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1872,"title":1873},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":1875,"title":1876},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":1878,"title":1879},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1881,"title":1882},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1884,"title":1885},"\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":1887,"title":1888},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":1890,"title":1891},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":1893,"title":1894},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1896,"title":1897},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":1899,"title":1900},"\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":1902,"title":1903},"\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":1905,"title":1906},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":1908,"title":1909},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1911,"title":1912},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1914,"title":1915},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":1917,"title":1918},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907509]