[{"data":1,"prerenderedAt":1266},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F":3,"content-directory":1118},{"id":4,"title":5,"body":6,"date":1102,"description":1103,"difficulty":1104,"draft":1105,"extension":1106,"meta":1107,"navigation":211,"path":1108,"seo":1109,"stem":1110,"tags":1111,"updated":1102,"__hash__":1117},"content\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Findex.md","Packaging Python CLIs for Distribution",{"type":7,"value":8,"toc":1091},"minimark",[9,22,27,103,107,111,133,271,296,423,427,430,457,468,472,475,540,547,551,561,710,724,728,735,808,936,940,943,983,987,1052,1056,1087],[10,11,12,13,17,18,21],"p",{},"A script that works on your machine is not a tool other people can use. To hand your CLI to a\nteammate, a CI job, or a stranger on PyPI, you have to turn it into an installable package: a\nsingle artifact that declares its command, its dependencies, and how to expose it on the\n",[14,15,16],"code",{},"PATH",". This overview walks the whole path — from a ",[14,19,20],{},"pyproject.toml"," that names your command,\nthrough building a wheel, to the three ways people will actually install it — and then routes\nyou to the deep guides for each step.",[23,24,26],"h2",{"id":25},"tldr","TL;DR",[28,29,30,45,68,86,100],"ul",{},[31,32,33,37,38,41,42,44],"li",{},[34,35,36],"strong",{},"A \"distributable\" CLI is a package with a console entry point."," Declare it under\n",[14,39,40],{},"[project.scripts]"," in ",[14,43,20],{},"; that generates the launcher on install.",[31,46,47,50,51,55,56,59,60,63,64,67],{},[34,48,49],{},"Two build artifacts."," A ",[52,53,54],"em",{},"wheel"," (",[14,57,58],{},".whl",") is the pre-built install; an ",[52,61,62],{},"sdist","\n(",[14,65,66],{},".tar.gz",") is the source fallback used to build a wheel when none fits.",[31,69,70,73,74,77,78,81,82,85],{},[34,71,72],{},"Three delivery routes."," ",[14,75,76],{},"pipx"," for end users who just want the command; PyPI + ",[14,79,80],{},"pip","\u002F",[14,83,84],{},"uv","\nfor public distribution; a private index for internal tools.",[31,87,88,95,96,99],{},[34,89,90,91,94],{},"Build once with ",[14,92,93],{},"python -m build",","," verify with ",[14,97,98],{},"twine check"," and a smoke install, then\npublish.",[31,101,102],{},"Read on for a minimal end-to-end example, then follow the three deep guides linked at the\nbottom.",[104,105],"inline-diagram",{"name":106},"packaging-distribution-flow",[23,108,110],{"id":109},"what-distributable-actually-means","What \"distributable\" actually means",[10,112,113,114,116,117,120,121,124,125,128,129,132],{},"The difference between a script and a distributable CLI is one table in ",[14,115,20],{},". A\nconsole entry point maps a command name to a callable, and the installer writes a small\nlauncher script into the environment's ",[14,118,119],{},"bin\u002F"," (or ",[14,122,123],{},"Scripts\\"," on Windows) directory that calls\nit. Once the package is installed, typing the command name Just Works — no ",[14,126,127],{},"python path\u002Fto\u002Fscript.py",", no fiddling with ",[14,130,131],{},"PYTHONPATH",".",[134,135,140],"pre",{"className":136,"code":137,"language":138,"meta":139,"style":139},"language-toml shiki shiki-themes github-light github-dark","[project]\nname = \"greet-cli\"\nversion = \"0.1.0\"\ndescription = \"A tiny greeting CLI\"\nrequires-python = \">=3.11\"\ndependencies = [\"click>=8.1\"]\n\n[project.scripts]\ngreet = \"greet_cli.__main__:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n","toml","",[14,141,142,158,168,177,186,195,206,213,227,236,241,251,262],{"__ignoreMap":139},[143,144,147,151,155],"span",{"class":145,"line":146},"line",1,[143,148,150],{"class":149},"sVt8B","[",[143,152,154],{"class":153},"sScJk","project",[143,156,157],{"class":149},"]\n",[143,159,161,164],{"class":145,"line":160},2,[143,162,163],{"class":149},"name = ",[143,165,167],{"class":166},"sZZnC","\"greet-cli\"\n",[143,169,171,174],{"class":145,"line":170},3,[143,172,173],{"class":149},"version = ",[143,175,176],{"class":166},"\"0.1.0\"\n",[143,178,180,183],{"class":145,"line":179},4,[143,181,182],{"class":149},"description = ",[143,184,185],{"class":166},"\"A tiny greeting CLI\"\n",[143,187,189,192],{"class":145,"line":188},5,[143,190,191],{"class":149},"requires-python = ",[143,193,194],{"class":166},"\">=3.11\"\n",[143,196,198,201,204],{"class":145,"line":197},6,[143,199,200],{"class":149},"dependencies = [",[143,202,203],{"class":166},"\"click>=8.1\"",[143,205,157],{"class":149},[143,207,209],{"class":145,"line":208},7,[143,210,212],{"emptyLinePlaceholder":211},true,"\n",[143,214,216,218,220,222,225],{"class":145,"line":215},8,[143,217,150],{"class":149},[143,219,154],{"class":153},[143,221,132],{"class":149},[143,223,224],{"class":153},"scripts",[143,226,157],{"class":149},[143,228,230,233],{"class":145,"line":229},9,[143,231,232],{"class":149},"greet = ",[143,234,235],{"class":166},"\"greet_cli.__main__:main\"\n",[143,237,239],{"class":145,"line":238},10,[143,240,212],{"emptyLinePlaceholder":211},[143,242,244,246,249],{"class":145,"line":243},11,[143,245,150],{"class":149},[143,247,248],{"class":153},"build-system",[143,250,157],{"class":149},[143,252,254,257,260],{"class":145,"line":253},12,[143,255,256],{"class":149},"requires = [",[143,258,259],{"class":166},"\"hatchling\"",[143,261,157],{"class":149},[143,263,265,268],{"class":145,"line":264},13,[143,266,267],{"class":149},"build-backend = ",[143,269,270],{"class":166},"\"hatchling.build\"\n",[10,272,273,274,277,278,281,282,285,286,289,290,295],{},"That ",[14,275,276],{},"greet = \"greet_cli.__main__:main\""," line is the whole trick: the name left of ",[14,279,280],{},"="," becomes\nthe shell command; the value is ",[14,283,284],{},"import.path:function",". The mechanics of choosing that target\n(and why ",[14,287,288],{},"__main__:main"," is a good default) are covered in\n",[291,292,294],"a",{"href":293},"\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",".\nThe matching source module is minimal:",[134,297,301],{"className":298,"code":299,"language":300,"meta":139,"style":139},"language-python shiki shiki-themes github-light github-dark","# src\u002Fgreet_cli\u002F__main__.py\nimport click\n\n@click.command()\n@click.argument(\"name\")\ndef main(name: str) -> None:\n    \"\"\"Say hello to NAME.\"\"\"\n    click.echo(f\"Hello, {name}!\")\n\nif __name__ == \"__main__\":\n    main()\n","python",[14,302,303,309,318,322,330,344,368,373,398,402,418],{"__ignoreMap":139},[143,304,305],{"class":145,"line":146},[143,306,308],{"class":307},"sJ8bj","# src\u002Fgreet_cli\u002F__main__.py\n",[143,310,311,315],{"class":145,"line":160},[143,312,314],{"class":313},"szBVR","import",[143,316,317],{"class":149}," click\n",[143,319,320],{"class":145,"line":170},[143,321,212],{"emptyLinePlaceholder":211},[143,323,324,327],{"class":145,"line":179},[143,325,326],{"class":153},"@click.command",[143,328,329],{"class":149},"()\n",[143,331,332,335,338,341],{"class":145,"line":188},[143,333,334],{"class":153},"@click.argument",[143,336,337],{"class":149},"(",[143,339,340],{"class":166},"\"name\"",[143,342,343],{"class":149},")\n",[143,345,346,349,352,355,359,362,365],{"class":145,"line":197},[143,347,348],{"class":313},"def",[143,350,351],{"class":153}," main",[143,353,354],{"class":149},"(name: ",[143,356,358],{"class":357},"sj4cs","str",[143,360,361],{"class":149},") -> ",[143,363,364],{"class":357},"None",[143,366,367],{"class":149},":\n",[143,369,370],{"class":145,"line":208},[143,371,372],{"class":166},"    \"\"\"Say hello to NAME.\"\"\"\n",[143,374,375,378,381,384,387,390,393,396],{"class":145,"line":215},[143,376,377],{"class":149},"    click.echo(",[143,379,380],{"class":313},"f",[143,382,383],{"class":166},"\"Hello, ",[143,385,386],{"class":357},"{",[143,388,389],{"class":149},"name",[143,391,392],{"class":357},"}",[143,394,395],{"class":166},"!\"",[143,397,343],{"class":149},[143,399,400],{"class":145,"line":229},[143,401,212],{"emptyLinePlaceholder":211},[143,403,404,407,410,413,416],{"class":145,"line":238},[143,405,406],{"class":313},"if",[143,408,409],{"class":357}," __name__",[143,411,412],{"class":313}," ==",[143,414,415],{"class":166}," \"__main__\"",[143,417,367],{"class":149},[143,419,420],{"class":145,"line":243},[143,421,422],{"class":149},"    main()\n",[23,424,426],{"id":425},"wheel-vs-sdist-at-a-glance","Wheel vs sdist at a glance",[10,428,429],{},"A build produces up to two artifacts, and it helps to know what each one is for before you\never run the build.",[28,431,432,449],{},[31,433,434,440,441,444,445,448],{},[34,435,436,437,439],{},"Wheel (",[14,438,58],{},")"," — a ZIP with a specific name layout, already laid out the way it lands in\n",[14,442,443],{},"site-packages",". Installing it is basically an unzip, so it is fast and needs no build step\non the user's machine. Pure-Python CLIs ship a single ",[14,446,447],{},"...-py3-none-any.whl"," that works\neverywhere.",[31,450,451,456],{},[34,452,453,454,439],{},"Source distribution \u002F sdist (",[14,455,66],{}," — your source tree plus metadata. Installers use\nit when no compatible wheel exists, building a wheel locally first. It is also the\narchival, auditable form of a release.",[10,458,459,460,463,464,132],{},"For a pure-Python CLI you publish both: the wheel is what nearly everyone installs, the sdist\nis the fallback and the thing packagers (Linux distros, conda-forge) build from. The full\nmechanics — the ",[14,461,462],{},"dist\u002F"," layout, inspecting a wheel, including package data — live in\n",[291,465,467],{"href":466},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F","building wheels and sdists for Python CLIs",[23,469,471],{"id":470},"the-three-delivery-routes","The three delivery routes",[10,473,474],{},"How your CLI reaches users shapes how you package and document it. There are three common\nroutes, and most real tools use more than one.",[476,477,478,500,523],"ol",{},[31,479,480,73,485,488,489,491,492,495,496,132],{},[34,481,482,484],{},[14,483,76],{}," (end users who want the command, not the library).",[14,486,487],{},"pipx install"," drops the CLI\ninto its own isolated virtual environment and links the command onto the ",[14,490,16],{},", so tools\nnever fight over dependency versions. This is the right recommendation in your README for\nanyone who just wants to ",[52,493,494],{},"run"," your tool. See\n",[291,497,499],{"href":498},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx\u002F","installing and distributing CLIs with pipx",[31,501,502,510,511,514,515,518,519,132],{},[34,503,504,505,81,507,509],{},"PyPI + ",[14,506,80],{},[14,508,84],{}," (public distribution)."," Upload to the Python Package Index and anyone\ncan ",[14,512,513],{},"pip install your-cli"," or add it as a dependency. This is table stakes for an\nopen-source tool; it is what makes ",[14,516,517],{},"pipx install your-cli"," resolve at all. See\n",[291,520,522],{"href":521},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi\u002F","publishing a Python CLI to PyPI",[31,524,525,528,529,532,533,536,537,539],{},[34,526,527],{},"A private\u002Finternal index."," For company-internal tools, run or rent a package index\n(Artifactory, a self-hosted ",[14,530,531],{},"devpi",", GitHub\u002FGitLab package registries) and point ",[14,534,535],{},"pip install --index-url"," or ",[14,538,84],{}," at it. The build and entry-point mechanics are identical; only\nthe upload target and credentials change.",[10,541,542,543,546],{},"A fourth route worth knowing: you do not need an index at all for a quick handoff. A freshly\nbuilt wheel installs straight from disk with ",[14,544,545],{},"pipx install .\u002Fdist\u002Fyour_cli-0.1.0-py3-none-any.whl",",\nwhich is perfect for sharing a pre-release with a colleague over Slack.",[23,548,550],{"id":549},"a-minimal-end-to-end-example","A minimal end-to-end example",[10,552,553,554,556,557,560],{},"Here is the whole loop, from a project directory to a working global command, with nothing\npublished anywhere. Assume the ",[14,555,20],{}," and ",[14,558,559],{},"src\u002Fgreet_cli\u002F__main__.py"," from above.",[134,562,566],{"className":563,"code":564,"language":565,"meta":139,"style":139},"language-bash shiki shiki-themes github-light github-dark","$ pip install build            # or: uv tool install build\n$ python -m build              # produces dist\u002F*.whl and dist\u002F*.tar.gz\n$ ls dist\u002F\ngreet_cli-0.1.0-py3-none-any.whl  greet_cli-0.1.0.tar.gz\n\n$ pipx install .\u002Fdist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n  installed package greet-cli 0.1.0, installed using Python 3.12.3\n  These apps are now globally available\n    - greet\n\n$ greet World\nHello, World!\n","bash",[14,567,568,585,600,610,618,622,634,660,680,688,692,702],{"__ignoreMap":139},[143,569,570,573,576,579,582],{"class":145,"line":146},[143,571,572],{"class":153},"$",[143,574,575],{"class":166}," pip",[143,577,578],{"class":166}," install",[143,580,581],{"class":166}," build",[143,583,584],{"class":307},"            # or: uv tool install build\n",[143,586,587,589,592,595,597],{"class":145,"line":160},[143,588,572],{"class":153},[143,590,591],{"class":166}," python",[143,593,594],{"class":357}," -m",[143,596,581],{"class":166},[143,598,599],{"class":307},"              # produces dist\u002F*.whl and dist\u002F*.tar.gz\n",[143,601,602,604,607],{"class":145,"line":170},[143,603,572],{"class":153},[143,605,606],{"class":166}," ls",[143,608,609],{"class":166}," dist\u002F\n",[143,611,612,615],{"class":145,"line":179},[143,613,614],{"class":153},"greet_cli-0.1.0-py3-none-any.whl",[143,616,617],{"class":166},"  greet_cli-0.1.0.tar.gz\n",[143,619,620],{"class":145,"line":188},[143,621,212],{"emptyLinePlaceholder":211},[143,623,624,626,629,631],{"class":145,"line":197},[143,625,572],{"class":153},[143,627,628],{"class":166}," pipx",[143,630,578],{"class":166},[143,632,633],{"class":166}," .\u002Fdist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n",[143,635,636,639,642,645,648,651,654,657],{"class":145,"line":208},[143,637,638],{"class":153},"  installed",[143,640,641],{"class":166}," package",[143,643,644],{"class":166}," greet-cli",[143,646,647],{"class":166}," 0.1.0,",[143,649,650],{"class":166}," installed",[143,652,653],{"class":166}," using",[143,655,656],{"class":166}," Python",[143,658,659],{"class":357}," 3.12.3\n",[143,661,662,665,668,671,674,677],{"class":145,"line":215},[143,663,664],{"class":153},"  These",[143,666,667],{"class":166}," apps",[143,669,670],{"class":166}," are",[143,672,673],{"class":166}," now",[143,675,676],{"class":166}," globally",[143,678,679],{"class":166}," available\n",[143,681,682,685],{"class":145,"line":229},[143,683,684],{"class":153},"    -",[143,686,687],{"class":166}," greet\n",[143,689,690],{"class":145,"line":238},[143,691,212],{"emptyLinePlaceholder":211},[143,693,694,696,699],{"class":145,"line":243},[143,695,572],{"class":153},[143,697,698],{"class":166}," greet",[143,700,701],{"class":166}," World\n",[143,703,704,707],{"class":145,"line":253},[143,705,706],{"class":153},"Hello,",[143,708,709],{"class":166}," World!\n",[10,711,712,713,715,716,719,720,723],{},"Four commands and the tool is on your ",[14,714,16],{},", isolated from every other Python tool you have\ninstalled. Swap the last two steps for a ",[14,717,718],{},"twine upload"," and users run ",[14,721,722],{},"pipx install greet-cli","\ninstead of pointing at a local file — same wheel, same entry point, public reach.",[23,725,727],{"id":726},"versioning-and-metadata-for-a-good-listing","Versioning and metadata for a good listing",[10,729,730,731,734],{},"Packaging is not only mechanics; the metadata in ",[14,732,733],{},"[project]"," is your product page on PyPI and\nthe contract users depend on. Get these right before your first upload, because names and\nreleased version numbers are effectively permanent:",[28,736,737,751,771,789,800],{},[31,738,739,743,744,556,747,750],{},[34,740,741],{},[14,742,389],{}," — must be globally unique on PyPI and is normalized (case- and separator-\ninsensitive: ",[14,745,746],{},"Greet_CLI",[14,748,749],{},"greet-cli"," collide). Check availability before you commit to\nit.",[31,752,753,758,759,765,766,770],{},[34,754,755],{},[14,756,757],{},"version"," — follow ",[291,760,764],{"href":761,"rel":762},"https:\u002F\u002Fsemver.org\u002F",[763],"nofollow","semantic versioning"," and never reuse a number;\nPyPI rejects re-uploads of an existing version. Our guide on\n",[291,767,769],{"href":768},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning and changelogs","\ncovers keeping this in sync with a changelog.",[31,772,773,785,786,132],{},[34,774,775,778,779,778,782],{},[14,776,777],{},"description",", ",[14,780,781],{},"readme",[14,783,784],{},"license"," — the summary line, the long description rendered on\nthe project page, and an SPDX license expression like ",[14,787,788],{},"license = \"MIT\"",[31,790,791,796,797,799],{},[34,792,793],{},[14,794,795],{},"requires-python"," — the interpreter floor. Set it honestly; ",[14,798,80],{}," uses it to refuse\ninstalls on unsupported Pythons instead of failing at runtime.",[31,801,802,807],{},[34,803,804],{},[14,805,806],{},"[project.urls]"," — Homepage, Source, and Changelog links that show up in the PyPI\nsidebar and build trust.",[134,809,811],{"className":136,"code":810,"language":138,"meta":139,"style":139},"[project]\nname = \"greet-cli\"\nversion = \"0.2.0\"\ndescription = \"A friendly greeting CLI\"\nreadme = \"README.md\"\nlicense = \"MIT\"\nrequires-python = \">=3.11\"\nauthors = [{ name = \"Ada Lovelace\", email = \"ada@example.com\" }]\nkeywords = [\"cli\", \"greeting\"]\n\n[project.urls]\nHomepage = \"https:\u002F\u002Fgithub.com\u002Fada\u002Fgreet-cli\"\nSource = \"https:\u002F\u002Fgithub.com\u002Fada\u002Fgreet-cli\"\nChangelog = \"https:\u002F\u002Fgithub.com\u002Fada\u002Fgreet-cli\u002Fblob\u002Fmain\u002FCHANGELOG.md\"\n",[14,812,813,821,827,834,841,849,857,863,880,895,899,912,920,927],{"__ignoreMap":139},[143,814,815,817,819],{"class":145,"line":146},[143,816,150],{"class":149},[143,818,154],{"class":153},[143,820,157],{"class":149},[143,822,823,825],{"class":145,"line":160},[143,824,163],{"class":149},[143,826,167],{"class":166},[143,828,829,831],{"class":145,"line":170},[143,830,173],{"class":149},[143,832,833],{"class":166},"\"0.2.0\"\n",[143,835,836,838],{"class":145,"line":179},[143,837,182],{"class":149},[143,839,840],{"class":166},"\"A friendly greeting CLI\"\n",[143,842,843,846],{"class":145,"line":188},[143,844,845],{"class":149},"readme = ",[143,847,848],{"class":166},"\"README.md\"\n",[143,850,851,854],{"class":145,"line":197},[143,852,853],{"class":149},"license = ",[143,855,856],{"class":166},"\"MIT\"\n",[143,858,859,861],{"class":145,"line":208},[143,860,191],{"class":149},[143,862,194],{"class":166},[143,864,865,868,871,874,877],{"class":145,"line":215},[143,866,867],{"class":149},"authors = [{ name = ",[143,869,870],{"class":166},"\"Ada Lovelace\"",[143,872,873],{"class":149},", email = ",[143,875,876],{"class":166},"\"ada@example.com\"",[143,878,879],{"class":149}," }]\n",[143,881,882,885,888,890,893],{"class":145,"line":229},[143,883,884],{"class":149},"keywords = [",[143,886,887],{"class":166},"\"cli\"",[143,889,778],{"class":149},[143,891,892],{"class":166},"\"greeting\"",[143,894,157],{"class":149},[143,896,897],{"class":145,"line":238},[143,898,212],{"emptyLinePlaceholder":211},[143,900,901,903,905,907,910],{"class":145,"line":243},[143,902,150],{"class":149},[143,904,154],{"class":153},[143,906,132],{"class":149},[143,908,909],{"class":153},"urls",[143,911,157],{"class":149},[143,913,914,917],{"class":145,"line":253},[143,915,916],{"class":149},"Homepage = ",[143,918,919],{"class":166},"\"https:\u002F\u002Fgithub.com\u002Fada\u002Fgreet-cli\"\n",[143,921,922,925],{"class":145,"line":264},[143,923,924],{"class":149},"Source = ",[143,926,919],{"class":166},[143,928,930,933],{"class":145,"line":929},14,[143,931,932],{"class":149},"Changelog = ",[143,934,935],{"class":166},"\"https:\u002F\u002Fgithub.com\u002Fada\u002Fgreet-cli\u002Fblob\u002Fmain\u002FCHANGELOG.md\"\n",[23,937,939],{"id":938},"where-to-go-next","Where to go next",[10,941,942],{},"Each step above has a dedicated deep guide. Read them in order the first time; jump straight\nto one when you already know the rest:",[28,944,945,956,972],{},[31,946,947,952,953,955],{},[34,948,949],{},[291,950,951],{"href":466},"Building wheels and sdists for Python CLIs"," —\n",[14,954,93],{},", what each artifact contains, and smoke-testing before you release.",[31,957,958,963,964,81,966,81,968,971],{},[34,959,960],{},[291,961,962],{"href":498},"Installing and distributing CLIs with pipx"," —\nthe isolation model, ",[14,965,487],{},[14,967,494],{},[14,969,970],{},"inject",", and installing from a wheel, git, or PyPI.",[31,973,974,979,980,982],{},[34,975,976],{},[291,977,978],{"href":521},"Publishing a Python CLI to PyPI"," —\nnames, TestPyPI, API tokens, ",[14,981,718],{},", and trusted publishing from CI.",[23,984,986],{"id":985},"production-notes","Production notes",[28,988,989,1007,1026,1046],{},[31,990,991,998,999,1001,1002,1006],{},[34,992,993,994,997],{},"Use a ",[14,995,996],{},"src\u002F"," layout."," Putting your package under ",[14,1000,996],{}," stops the build from accidentally\nimporting your working tree instead of the installed package, which is exactly the kind of\nbug that only appears after you ship. Most scaffolds, including\n",[291,1003,1005],{"href":1004},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002F","CLI project scaffolding with Cookiecutter",", default to it.",[31,1008,1009,73,1012,1015,1016,778,1019,778,1022,1025],{},[34,1010,1011],{},"Pin your build backend, not just your deps.",[14,1013,1014],{},"[build-system].requires"," should name a\nbackend (",[14,1017,1018],{},"hatchling",[14,1020,1021],{},"setuptools",[14,1023,1024],{},"flit-core",") — backend defaults drift across versions,\nand a floating backend is the classic cause of a build that worked last month and not today.",[31,1027,1028,1031,1032,1035,1036,1040,1041,1045],{},[34,1029,1030],{},"Decide dependency strategy up front."," A library pins loosely (",[14,1033,1034],{},"click>=8.1",") so it composes\nin others' environments; an application distributed via pipx can afford tighter pins because\nit lives in its own isolated venv. See\n",[291,1037,1039],{"href":1038},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002F","uv for Python CLI dependency management","\nand ",[291,1042,1044],{"href":1043},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002F","Poetry workflows for CLI development","\nfor two ways to manage that lockfile.",[31,1047,1048,1051],{},[34,1049,1050],{},"Test the artifact, not the repo."," A green test suite against your source tree does not\nprove the wheel installs and its entry point runs. Always install the built wheel into a\nthrowaway environment and invoke the command once before tagging a release.",[23,1053,1055],{"id":1054},"related","Related",[28,1057,1058,1065,1070,1075,1081],{},[31,1059,1060,1064],{},[291,1061,1063],{"href":1062},"\u002Fproject-setup-dependency-management\u002F","Project Setup & Dependency Management"," — the parent track: environments, dependencies, and release hygiene.",[31,1066,1067,1069],{},[291,1068,1039],{"href":1038}," — the fast resolver and installer that also builds and publishes.",[31,1071,1072,1074],{},[291,1073,1044],{"href":1043}," — an all-in-one alternative that manages deps, builds, and uploads.",[31,1076,1077,1080],{},[291,1078,1079],{"href":293},"Best practices for Python CLI entry points"," — how the command name maps to your code.",[31,1082,1083,1086],{},[291,1084,1085],{"href":768},"Managing CLI versioning and changelogs"," — keep versions and release notes honest before you publish.",[1088,1089,1090],"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 .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}",{"title":139,"searchDepth":160,"depth":160,"links":1092},[1093,1094,1095,1096,1097,1098,1099,1100,1101],{"id":25,"depth":160,"text":26},{"id":109,"depth":160,"text":110},{"id":425,"depth":160,"text":426},{"id":470,"depth":160,"text":471},{"id":549,"depth":160,"text":550},{"id":726,"depth":160,"text":727},{"id":938,"depth":160,"text":939},{"id":985,"depth":160,"text":986},{"id":1054,"depth":160,"text":1055},"2026-07-05","Turn a Python CLI into an installable package: define entry points in pyproject.toml, build wheels and sdists, install with pipx, and publish to PyPI.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution",{"title":5,"description":1103},"project-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Findex",[1112,1113,76,1114,1115,1116],"packaging","distribution","pypi","wheels","entry-points","m6sDM9t0I8NVfh-kHQqjjYZ1Wl-Ki4vE9S9l4Dmur30",[1119,1122,1125,1128,1131,1134,1137,1140,1143,1146,1149,1152,1155,1158,1161,1164,1167,1170,1173,1175,1178,1181,1184,1187,1190,1193,1196,1199,1201,1204,1207,1210,1213,1216,1219,1222,1224,1227,1230,1233,1234,1237,1239,1242,1245,1248,1251,1254,1257,1260,1263],{"path":1120,"title":1121},"\u002Fabout","About Python CLI Toolcraft",{"path":1123,"title":1124},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1126,"title":1127},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1129,"title":1130},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1132,"title":1133},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1135,"title":1136},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1138,"title":1139},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1141,"title":1142},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1144,"title":1145},"\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":1147,"title":1148},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1150,"title":1151},"\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":1153,"title":1154},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1156,"title":1157},"\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":1159,"title":1160},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1162,"title":1163},"\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":1165,"title":1166},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1168,"title":1169},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1171,"title":1172},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":81,"title":1174},"Python CLI Toolcraft",{"path":1176,"title":1177},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1179,"title":1180},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1182,"title":1183},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1185,"title":1186},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1188,"title":1189},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1191,"title":1192},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1194,"title":1195},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1197,"title":1198},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1200,"title":1079},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points",{"path":1202,"title":1203},"\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":1205,"title":1206},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1208,"title":1209},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1211,"title":1212},"\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":1214,"title":1215},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1217,"title":1218},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1220,"title":1221},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1223,"title":1063},"\u002Fproject-setup-dependency-management",{"path":1225,"title":1226},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1228,"title":1229},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1231,"title":1232},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1108,"title":5},{"path":1235,"title":1236},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1238,"title":978},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi",{"path":1240,"title":1241},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1243,"title":1244},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1246,"title":1247},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1249,"title":1250},"\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":1252,"title":1253},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1255,"title":1256},"\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":1258,"title":1259},"\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":1261,"title":1262},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1264,"title":1265},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867199]