[{"data":1,"prerenderedAt":1255},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F":3,"content-directory":1105},{"id":4,"title":5,"body":6,"date":1089,"description":1090,"difficulty":1091,"draft":1092,"extension":1093,"meta":1094,"navigation":205,"path":1095,"seo":1096,"stem":1097,"tags":1098,"updated":1089,"__hash__":1104},"content\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002Findex.md","Building Wheels and sdists for Python CLIs",{"type":7,"value":8,"toc":1078},"minimark",[9,18,23,93,97,100,126,135,138,142,154,310,316,351,380,384,394,473,496,500,506,531,534,622,653,657,664,667,750,761,798,808,812,819,871,878,938,954,958,1034,1038,1074],[10,11,12,13,17],"p",{},"Before your CLI can reach anyone — via PyPI, pipx, or a colleague's laptop — you have to build\nit into distributable artifacts: a wheel and a source distribution. This is a single command,\n",[14,15,16],"code",{},"python -m build",", but understanding what it produces (and how to verify the results before you\nship) is what separates a release that installs cleanly from one that fails on someone else's\nmachine. This guide covers both artifact types, the build itself, inspecting the output, and\nsmoke-testing a wheel in a throwaway environment.",[19,20,22],"h2",{"id":21},"tldr","TL;DR",[24,25,26,39,56,73,83],"ul",{},[27,28,29,34,35,38],"li",{},[30,31,32],"strong",{},[14,33,16],{}," produces both a wheel and an sdist into ",[14,36,37],{},"dist\u002F",".",[27,40,41,48,49,55],{},[30,42,43,44,47],{},"Wheel (",[14,45,46],{},".whl",")"," is a ZIP that installs by unzip — fast, no build step for the user.\n",[30,50,51,52,47],{},"sdist (",[14,53,54],{},".tar.gz"," is your source plus metadata, the fallback and the archival form.",[27,57,58,61,62,65,66,69,70,38],{},[30,59,60],{},"Configure once"," via ",[14,63,64],{},"[build-system]"," (hatchling or setuptools) and ",[14,67,68],{},"[project]"," in\n",[14,71,72],{},"pyproject.toml",[27,74,75,78,79,82],{},[30,76,77],{},"Verify before release:"," ",[14,80,81],{},"twine check dist\u002F*"," for metadata, then install the wheel into a\nfresh venv and run the command.",[27,84,85,88,89,92],{},[30,86,87],{},"A wheel is just a ZIP"," — ",[14,90,91],{},"unzip -l"," it to see exactly what you are shipping.",[19,94,96],{"id":95},"wheel-vs-sdist-what-each-one-is","Wheel vs sdist: what each one is",[10,98,99],{},"A build yields two files with different jobs.",[10,101,102,103,106,107,110,111,88,114,117,118,121,122,125],{},"A ",[30,104,105],{},"wheel"," is a pre-built distribution: a ZIP archive, named to encode the Python and platform\nit targets, whose contents are already arranged the way they land in ",[14,108,109],{},"site-packages",".\nInstalling a wheel is essentially \"unzip into the environment and write the launcher scripts,\"\nso it is fast and requires no build tooling on the user's machine. A pure-Python CLI produces\none universal wheel named ",[14,112,113],{},"...-py3-none-any.whl",[14,115,116],{},"py3"," (any Python 3), ",[14,119,120],{},"none"," (no ABI\nconstraint), ",[14,123,124],{},"any"," (any OS) — that installs everywhere.",[10,127,102,128,131,132,134],{},[30,129,130],{},"source distribution",", or sdist, is a ",[14,133,54],{}," of your source tree plus the metadata\nneeded to build it. Installers fall back to the sdist when no compatible wheel exists, building\na wheel locally first. It is also the auditable, from-source form that Linux distributions and\nconda-forge repackage. You publish both: nearly everyone installs the wheel; the sdist is the\nsafety net and the source of record.",[10,136,137],{},"For a pure-Python CLI the wheel is trivially portable, so why bother with the sdist? Because it\nis the only artifact that guarantees a build from source is possible — some ecosystems require\nit, and it protects users on a platform or Python you never built a wheel for.",[19,139,141],{"id":140},"configure-the-build-backend","Configure the build backend",[10,143,144,145,147,148,150,151,153],{},"The build is driven by two tables in ",[14,146,72],{},": ",[14,149,64],{}," names the backend that\nturns your source into artifacts, and ",[14,152,68],{}," supplies the metadata. Hatchling is a good\nmodern default; setuptools is the venerable alternative.",[155,156,161],"pre",{"className":157,"code":158,"language":159,"meta":160,"style":160},"language-toml shiki shiki-themes github-light github-dark","[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"greet-cli\"\nversion = \"0.1.0\"\ndescription = \"A friendly greeting CLI\"\nreadme = \"README.md\"\nlicense = \"MIT\"\nrequires-python = \">=3.11\"\ndependencies = [\"click>=8.1\"]\n\n[project.scripts]\ngreet = \"greet_cli.__main__:main\"\n","toml","",[14,162,163,179,191,200,207,217,226,235,244,253,262,271,282,287,301],{"__ignoreMap":160},[164,165,168,172,176],"span",{"class":166,"line":167},"line",1,[164,169,171],{"class":170},"sVt8B","[",[164,173,175],{"class":174},"sScJk","build-system",[164,177,178],{"class":170},"]\n",[164,180,182,185,189],{"class":166,"line":181},2,[164,183,184],{"class":170},"requires = [",[164,186,188],{"class":187},"sZZnC","\"hatchling\"",[164,190,178],{"class":170},[164,192,194,197],{"class":166,"line":193},3,[164,195,196],{"class":170},"build-backend = ",[164,198,199],{"class":187},"\"hatchling.build\"\n",[164,201,203],{"class":166,"line":202},4,[164,204,206],{"emptyLinePlaceholder":205},true,"\n",[164,208,210,212,215],{"class":166,"line":209},5,[164,211,171],{"class":170},[164,213,214],{"class":174},"project",[164,216,178],{"class":170},[164,218,220,223],{"class":166,"line":219},6,[164,221,222],{"class":170},"name = ",[164,224,225],{"class":187},"\"greet-cli\"\n",[164,227,229,232],{"class":166,"line":228},7,[164,230,231],{"class":170},"version = ",[164,233,234],{"class":187},"\"0.1.0\"\n",[164,236,238,241],{"class":166,"line":237},8,[164,239,240],{"class":170},"description = ",[164,242,243],{"class":187},"\"A friendly greeting CLI\"\n",[164,245,247,250],{"class":166,"line":246},9,[164,248,249],{"class":170},"readme = ",[164,251,252],{"class":187},"\"README.md\"\n",[164,254,256,259],{"class":166,"line":255},10,[164,257,258],{"class":170},"license = ",[164,260,261],{"class":187},"\"MIT\"\n",[164,263,265,268],{"class":166,"line":264},11,[164,266,267],{"class":170},"requires-python = ",[164,269,270],{"class":187},"\">=3.11\"\n",[164,272,274,277,280],{"class":166,"line":273},12,[164,275,276],{"class":170},"dependencies = [",[164,278,279],{"class":187},"\"click>=8.1\"",[164,281,178],{"class":170},[164,283,285],{"class":166,"line":284},13,[164,286,206],{"emptyLinePlaceholder":205},[164,288,290,292,294,296,299],{"class":166,"line":289},14,[164,291,171],{"class":170},[164,293,214],{"class":174},[164,295,38],{"class":170},[164,297,298],{"class":174},"scripts",[164,300,178],{"class":170},[164,302,304,307],{"class":166,"line":303},15,[164,305,306],{"class":170},"greet = ",[164,308,309],{"class":187},"\"greet_cli.__main__:main\"\n",[10,311,312,313,315],{},"The equivalent with setuptools only changes ",[14,314,64],{},":",[155,317,319],{"className":157,"code":318,"language":159,"meta":160,"style":160},"[build-system]\nrequires = [\"setuptools>=68\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n",[14,320,321,329,344],{"__ignoreMap":160},[164,322,323,325,327],{"class":166,"line":167},[164,324,171],{"class":170},[164,326,175],{"class":174},[164,328,178],{"class":170},[164,330,331,333,336,339,342],{"class":166,"line":181},[164,332,184],{"class":170},[164,334,335],{"class":187},"\"setuptools>=68\"",[164,337,338],{"class":170},", ",[164,340,341],{"class":187},"\"wheel\"",[164,343,178],{"class":170},[164,345,346,348],{"class":166,"line":193},[164,347,196],{"class":170},[164,349,350],{"class":187},"\"setuptools.build_meta\"\n",[10,352,353,354,357,358,361,362,365,366,369,370,373,374,379],{},"With a ",[14,355,356],{},"src\u002F"," layout, hatchling finds ",[14,359,360],{},"src\u002Fgreet_cli\u002F"," automatically; setuptools needs\n",[14,363,364],{},"[tool.setuptools.packages.find] where = [\"src\"]",". The ",[14,367,368],{},"[project.scripts]"," entry is what makes\nthe installed wheel expose a ",[14,371,372],{},"greet"," command — see\n",[375,376,378],"a",{"href":377},"\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","\nfor how to choose that target.",[19,381,383],{"id":382},"run-python-m-build","Run python -m build",[10,385,386,389,390,393],{},[14,387,388],{},"build"," is a small front-end that creates an isolated environment, installs your declared build\nbackend into it, and invokes it. That isolation is the point: it builds against exactly the\n",[14,391,392],{},"[build-system].requires"," you declared, not whatever happens to be in your current environment.",[155,395,399],{"className":396,"code":397,"language":398,"meta":160,"style":160},"language-bash shiki shiki-themes github-light github-dark","$ pip install build            # or: uv tool install build\n$ python -m build\n* Creating isolated environment: venv+pip...\n* Building sdist...\n* Building wheel from sdist...\nSuccessfully built greet_cli-0.1.0.tar.gz and greet_cli-0.1.0-py3-none-any.whl\n","bash",[14,400,401,419,433,442,449,456],{"__ignoreMap":160},[164,402,403,406,409,412,415],{"class":166,"line":167},[164,404,405],{"class":174},"$",[164,407,408],{"class":187}," pip",[164,410,411],{"class":187}," install",[164,413,414],{"class":187}," build",[164,416,418],{"class":417},"sJ8bj","            # or: uv tool install build\n",[164,420,421,423,426,430],{"class":166,"line":181},[164,422,405],{"class":174},[164,424,425],{"class":187}," python",[164,427,429],{"class":428},"sj4cs"," -m",[164,431,432],{"class":187}," build\n",[164,434,435,439],{"class":166,"line":193},[164,436,438],{"class":437},"szBVR","*",[164,440,441],{"class":170}," Creating isolated environment: venv+pip...\n",[164,443,444,446],{"class":166,"line":202},[164,445,438],{"class":437},[164,447,448],{"class":170}," Building sdist...\n",[164,450,451,453],{"class":166,"line":209},[164,452,438],{"class":437},[164,454,455],{"class":170}," Building wheel from sdist...\n",[164,457,458,461,464,467,470],{"class":166,"line":219},[164,459,460],{"class":174},"Successfully",[164,462,463],{"class":187}," built",[164,465,466],{"class":187}," greet_cli-0.1.0.tar.gz",[164,468,469],{"class":187}," and",[164,471,472],{"class":187}," greet_cli-0.1.0-py3-none-any.whl\n",[10,474,475,476,478,479,483,484,487,488,491,492,495],{},"Note the sequence: ",[14,477,388],{}," makes the sdist first, then builds the wheel ",[480,481,482],"em",{},"from that sdist",". This\nis a feature — it proves your sdist is complete enough to produce a wheel. Restrict to one\nartifact with ",[14,485,486],{},"python -m build --wheel"," or ",[14,489,490],{},"--sdist"," when you need only one. If you use uv,\n",[14,493,494],{},"uv build"," does the same job with the same output.",[19,497,499],{"id":498},"the-dist-layout-and-inspecting-a-wheel","The dist\u002F layout and inspecting a wheel",[10,501,502,503,505],{},"After a build, ",[14,504,37],{}," holds both artifacts:",[155,507,509],{"className":396,"code":508,"language":398,"meta":160,"style":160},"$ ls dist\u002F\ngreet_cli-0.1.0-py3-none-any.whl\ngreet_cli-0.1.0.tar.gz\n",[14,510,511,521,526],{"__ignoreMap":160},[164,512,513,515,518],{"class":166,"line":167},[164,514,405],{"class":174},[164,516,517],{"class":187}," ls",[164,519,520],{"class":187}," dist\u002F\n",[164,522,523],{"class":166,"line":181},[164,524,525],{"class":174},"greet_cli-0.1.0-py3-none-any.whl\n",[164,527,528],{"class":166,"line":193},[164,529,530],{"class":174},"greet_cli-0.1.0.tar.gz\n",[10,532,533],{},"A wheel is a ZIP, so you can look inside without installing it — the fastest way to confirm you\nshipped what you meant to and nothing you didn't:",[155,535,537],{"className":396,"code":536,"language":398,"meta":160,"style":160},"$ unzip -l dist\u002Fgreet_cli-0.1.0-py3-none-any.whl\nArchive:  dist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n    Length      Name\n   --------      ----\n        312      greet_cli\u002F__init__.py\n        684      greet_cli\u002F__main__.py\n       1024      greet_cli-0.1.0.dist-info\u002FMETADATA\n        ...      greet_cli-0.1.0.dist-info\u002FRECORD\n        ...      greet_cli-0.1.0.dist-info\u002FWHEEL\n        ...      greet_cli-0.1.0.dist-info\u002Fentry_points.txt\n",[14,538,539,552,560,568,576,584,592,600,608,615],{"__ignoreMap":160},[164,540,541,543,546,549],{"class":166,"line":167},[164,542,405],{"class":174},[164,544,545],{"class":187}," unzip",[164,547,548],{"class":428}," -l",[164,550,551],{"class":187}," dist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n",[164,553,554,557],{"class":166,"line":181},[164,555,556],{"class":174},"Archive:",[164,558,559],{"class":187},"  dist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n",[164,561,562,565],{"class":166,"line":193},[164,563,564],{"class":174},"    Length",[164,566,567],{"class":187},"      Name\n",[164,569,570,573],{"class":166,"line":202},[164,571,572],{"class":174},"   --------",[164,574,575],{"class":428},"      ----\n",[164,577,578,581],{"class":166,"line":209},[164,579,580],{"class":174},"        312",[164,582,583],{"class":187},"      greet_cli\u002F__init__.py\n",[164,585,586,589],{"class":166,"line":219},[164,587,588],{"class":174},"        684",[164,590,591],{"class":187},"      greet_cli\u002F__main__.py\n",[164,593,594,597],{"class":166,"line":228},[164,595,596],{"class":174},"       1024",[164,598,599],{"class":187},"      greet_cli-0.1.0.dist-info\u002FMETADATA\n",[164,601,602,605],{"class":166,"line":237},[164,603,604],{"class":428},"        ...",[164,606,607],{"class":187},"      greet_cli-0.1.0.dist-info\u002FRECORD\n",[164,609,610,612],{"class":166,"line":246},[164,611,604],{"class":428},[164,613,614],{"class":187},"      greet_cli-0.1.0.dist-info\u002FWHEEL\n",[164,616,617,619],{"class":166,"line":255},[164,618,604],{"class":428},[164,620,621],{"class":187},"      greet_cli-0.1.0.dist-info\u002Fentry_points.txt\n",[10,623,624,625,628,629,632,633,635,636,639,640,642,643,646,647,649,650,38],{},"The ",[14,626,627],{},".dist-info\u002F"," directory holds the metadata: ",[14,630,631],{},"METADATA"," (your ",[14,634,68],{}," fields),\n",[14,637,638],{},"entry_points.txt"," (your console script — check the ",[14,641,372],{}," line is there), and ",[14,644,645],{},"RECORD"," (a\nmanifest with hashes). If your command is missing from ",[14,648,638],{},", the wheel will\ninstall but the command won't exist — catch that here, not from a user's bug report. Peek at\nthe sdist the same way with ",[14,651,652],{},"tar tzf dist\u002Fgreet_cli-0.1.0.tar.gz",[19,654,656],{"id":655},"including-package-data","Including package data",[10,658,659,660,663],{},"Code files are picked up automatically; non-code files are not, and forgetting them is the most\ncommon \"works locally, broken once installed\" bug. If your CLI ships templates, a bundled\nconfig, or a ",[14,661,662],{},"py.typed"," marker, tell the backend to include them.",[10,665,666],{},"With hatchling, force-include files or whole directories:",[155,668,670],{"className":157,"code":669,"language":159,"meta":160,"style":160},"[tool.hatch.build.targets.wheel]\ninclude = [\"src\u002Fgreet_cli\"]\n\n[tool.hatch.build.targets.wheel.force-include]\n\"src\u002Fgreet_cli\u002Ftemplates\" = \"greet_cli\u002Ftemplates\"\n",[14,671,672,699,709,713,742],{"__ignoreMap":160},[164,673,674,676,679,681,684,686,688,690,693,695,697],{"class":166,"line":167},[164,675,171],{"class":170},[164,677,678],{"class":174},"tool",[164,680,38],{"class":170},[164,682,683],{"class":174},"hatch",[164,685,38],{"class":170},[164,687,388],{"class":174},[164,689,38],{"class":170},[164,691,692],{"class":174},"targets",[164,694,38],{"class":170},[164,696,105],{"class":174},[164,698,178],{"class":170},[164,700,701,704,707],{"class":166,"line":181},[164,702,703],{"class":170},"include = [",[164,705,706],{"class":187},"\"src\u002Fgreet_cli\"",[164,708,178],{"class":170},[164,710,711],{"class":166,"line":193},[164,712,206],{"emptyLinePlaceholder":205},[164,714,715,717,719,721,723,725,727,729,731,733,735,737,740],{"class":166,"line":202},[164,716,171],{"class":170},[164,718,678],{"class":174},[164,720,38],{"class":170},[164,722,683],{"class":174},[164,724,38],{"class":170},[164,726,388],{"class":174},[164,728,38],{"class":170},[164,730,692],{"class":174},[164,732,38],{"class":170},[164,734,105],{"class":174},[164,736,38],{"class":170},[164,738,739],{"class":174},"force-include",[164,741,178],{"class":170},[164,743,744,747],{"class":166,"line":209},[164,745,746],{"class":170},"\"src\u002Fgreet_cli\u002Ftemplates\" = ",[164,748,749],{"class":187},"\"greet_cli\u002Ftemplates\"\n",[10,751,752,753,756,757,760],{},"With setuptools, use ",[14,754,755],{},"package-data"," (and a ",[14,758,759],{},"MANIFEST.in"," for what goes into the sdist):",[155,762,764],{"className":157,"code":763,"language":159,"meta":160,"style":160},"[tool.setuptools.package-data]\ngreet_cli = [\"templates\u002F*.txt\", \"py.typed\"]\n",[14,765,766,783],{"__ignoreMap":160},[164,767,768,770,772,774,777,779,781],{"class":166,"line":167},[164,769,171],{"class":170},[164,771,678],{"class":174},[164,773,38],{"class":170},[164,775,776],{"class":174},"setuptools",[164,778,38],{"class":170},[164,780,755],{"class":174},[164,782,178],{"class":170},[164,784,785,788,791,793,796],{"class":166,"line":181},[164,786,787],{"class":170},"greet_cli = [",[164,789,790],{"class":187},"\"templates\u002F*.txt\"",[164,792,338],{"class":170},[164,794,795],{"class":187},"\"py.typed\"",[164,797,178],{"class":170},[10,799,800,801,803,804,807],{},"Then always confirm with ",[14,802,91],{}," that the data actually landed in the wheel. At runtime,\nread those files with ",[14,805,806],{},"importlib.resources",", never a relative filesystem path — the installed\npackage may live inside a ZIP or a location you cannot guess.",[19,809,811],{"id":810},"verify-with-twine-check-and-a-smoke-test","Verify with twine check and a smoke test",[10,813,814,815,818],{},"Two cheap checks catch most release failures. First, ",[14,816,817],{},"twine check"," validates that the metadata\nwill render on PyPI — a malformed README description is a classic reason an upload is accepted\nbut displays as raw text:",[155,820,822],{"className":396,"code":821,"language":398,"meta":160,"style":160},"$ pip install twine\n$ twine check dist\u002F*\nChecking dist\u002Fgreet_cli-0.1.0-py3-none-any.whl: PASSED\nChecking dist\u002Fgreet_cli-0.1.0.tar.gz: PASSED\n",[14,823,824,835,851,862],{"__ignoreMap":160},[164,825,826,828,830,832],{"class":166,"line":167},[164,827,405],{"class":174},[164,829,408],{"class":187},[164,831,411],{"class":187},[164,833,834],{"class":187}," twine\n",[164,836,837,839,842,845,848],{"class":166,"line":181},[164,838,405],{"class":174},[164,840,841],{"class":187}," twine",[164,843,844],{"class":187}," check",[164,846,847],{"class":187}," dist\u002F",[164,849,850],{"class":428},"*\n",[164,852,853,856,859],{"class":166,"line":193},[164,854,855],{"class":174},"Checking",[164,857,858],{"class":187}," dist\u002Fgreet_cli-0.1.0-py3-none-any.whl:",[164,860,861],{"class":187}," PASSED\n",[164,863,864,866,869],{"class":166,"line":202},[164,865,855],{"class":174},[164,867,868],{"class":187}," dist\u002Fgreet_cli-0.1.0.tar.gz:",[164,870,861],{"class":187},[10,872,873,874,877],{},"Second — and this is the check people skip — actually install the wheel into a clean,\nthrowaway environment and run the command. A passing test suite against your source tree does\nnot prove the ",[480,875,876],{},"built artifact"," works; only installing it does.",[155,879,881],{"className":396,"code":880,"language":398,"meta":160,"style":160},"$ python -m venv \u002Ftmp\u002Fsmoke\n$ \u002Ftmp\u002Fsmoke\u002Fbin\u002Fpip install dist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n$ \u002Ftmp\u002Fsmoke\u002Fbin\u002Fgreet World\nHello, World!\n$ rm -rf \u002Ftmp\u002Fsmoke\n",[14,882,883,897,908,918,926],{"__ignoreMap":160},[164,884,885,887,889,891,894],{"class":166,"line":167},[164,886,405],{"class":174},[164,888,425],{"class":187},[164,890,429],{"class":428},[164,892,893],{"class":187}," venv",[164,895,896],{"class":187}," \u002Ftmp\u002Fsmoke\n",[164,898,899,901,904,906],{"class":166,"line":181},[164,900,405],{"class":174},[164,902,903],{"class":187}," \u002Ftmp\u002Fsmoke\u002Fbin\u002Fpip",[164,905,411],{"class":187},[164,907,551],{"class":187},[164,909,910,912,915],{"class":166,"line":193},[164,911,405],{"class":174},[164,913,914],{"class":187}," \u002Ftmp\u002Fsmoke\u002Fbin\u002Fgreet",[164,916,917],{"class":187}," World\n",[164,919,920,923],{"class":166,"line":202},[164,921,922],{"class":174},"Hello,",[164,924,925],{"class":187}," World!\n",[164,927,928,930,933,936],{"class":166,"line":209},[164,929,405],{"class":174},[164,931,932],{"class":187}," rm",[164,934,935],{"class":428}," -rf",[164,937,896],{"class":187},[10,939,940,941,944,945,949,950,38],{},"If you use pipx, ",[14,942,943],{},"pipx install .\u002Fdist\u002Fgreet_cli-0.1.0-py3-none-any.whl"," is an even quicker\nisolated smoke test — see\n",[375,946,948],{"href":947},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx\u002F","installing and distributing CLIs with pipx",".\nOnce both checks pass, you are ready to\n",[375,951,953],{"href":952},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi\u002F","publish to PyPI",[19,955,957],{"id":956},"production-notes","Production notes",[24,959,960,976,986,1011,1024],{},[27,961,962,78,968,971,972,975],{},[30,963,964,965,967],{},"Clean ",[14,966,37],{}," between builds.",[14,969,970],{},"twine upload dist\u002F*"," uploads everything in the directory,\nso a stale wheel from a previous version can sneak into a release. ",[14,973,974],{},"rm -rf dist\u002F"," before\nbuilding, or upload a specific file.",[27,977,978,981,982,985],{},[30,979,980],{},"Reproducibility."," Set ",[14,983,984],{},"SOURCE_DATE_EPOCH"," to pin timestamps if you need byte-identical\nwheels across builds; hatchling and setuptools both honor it. For most CLIs this is optional,\nbut it matters for supply-chain verification.",[27,987,988,991,992,995,996,998,999,1002,1003,1006,1007,38],{},[30,989,990],{},"Version single-sourcing."," Rather than hand-editing ",[14,993,994],{},"version"," in ",[14,997,72],{},", derive it\nfrom a git tag with ",[14,1000,1001],{},"hatch-vcs"," (or ",[14,1004,1005],{},"setuptools-scm",") so the built artifact's version always\nmatches the tag. This dovetails with\n",[375,1008,1010],{"href":1009},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning and changelogs",[27,1012,1013,78,1016,1019,1020,1023],{},[30,1014,1015],{},"Pin the backend.",[14,1017,1018],{},"requires = [\"hatchling\"]"," without a floor can pull a new major backend\nthat changes defaults; pin a lower bound (",[14,1021,1022],{},"hatchling>=1.25",") once you find a version that\nworks.",[27,1025,1026,1029,1030,1033],{},[30,1027,1028],{},"Pure vs compiled."," Everything here assumes a pure-Python CLI, which yields one universal\nwheel. If you add a compiled extension you enter the world of per-platform wheels and\n",[14,1031,1032],{},"cibuildwheel"," — a different and larger topic.",[19,1035,1037],{"id":1036},"related","Related",[24,1039,1040,1047,1053,1059,1068],{},[27,1041,1042,1046],{},[375,1043,1045],{"href":1044},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","Packaging Python CLIs for Distribution"," — the overview this guide sits under.",[27,1048,1049,1052],{},[375,1050,1051],{"href":952},"Publishing a Python CLI to PyPI"," — upload the artifacts you just built and verified.",[27,1054,1055,1058],{},[375,1056,1057],{"href":947},"Installing and distributing CLIs with pipx"," — install a local wheel as a quick isolated smoke test.",[27,1060,1061,88,1065,1067],{},[375,1062,1064],{"href":1063},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002F","uv for Python CLI dependency management",[14,1066,494],{}," produces the same wheel and sdist.",[27,1069,1070,1073],{},[375,1071,1072],{"href":1009},"Managing CLI versioning and changelogs"," — single-source the version your build stamps in.",[1075,1076,1077],"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":160,"searchDepth":181,"depth":181,"links":1079},[1080,1081,1082,1083,1084,1085,1086,1087,1088],{"id":21,"depth":181,"text":22},{"id":95,"depth":181,"text":96},{"id":140,"depth":181,"text":141},{"id":382,"depth":181,"text":383},{"id":498,"depth":181,"text":499},{"id":655,"depth":181,"text":656},{"id":810,"depth":181,"text":811},{"id":956,"depth":181,"text":957},{"id":1036,"depth":181,"text":1037},"2026-07-05","Build wheels and source distributions for a Python CLI with the build tool, understand what each artifact contains, and verify they install before release.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis",{"title":5,"description":1090},"project-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002Findex",[1099,1100,1101,1102,1103],"wheels","packaging","distribution","cli","pypi","DW9nLD2NARYe2rfXG8kc37jD20uHV65Gbw8XJiaBCf8",[1106,1109,1112,1115,1118,1121,1124,1127,1130,1133,1136,1139,1142,1145,1148,1151,1154,1157,1160,1163,1166,1169,1172,1175,1178,1181,1184,1187,1190,1193,1196,1199,1202,1205,1208,1211,1214,1217,1220,1221,1223,1226,1228,1231,1234,1237,1240,1243,1246,1249,1252],{"path":1107,"title":1108},"\u002Fabout","About Python CLI Toolcraft",{"path":1110,"title":1111},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1113,"title":1114},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1116,"title":1117},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1119,"title":1120},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1122,"title":1123},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1125,"title":1126},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1128,"title":1129},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1131,"title":1132},"\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":1134,"title":1135},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1137,"title":1138},"\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":1140,"title":1141},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1143,"title":1144},"\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":1146,"title":1147},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1149,"title":1150},"\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":1152,"title":1153},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1155,"title":1156},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1158,"title":1159},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1161,"title":1162},"\u002F","Python CLI Toolcraft",{"path":1164,"title":1165},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1167,"title":1168},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1170,"title":1171},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1173,"title":1174},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1176,"title":1177},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1179,"title":1180},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1182,"title":1183},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1185,"title":1186},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1188,"title":1189},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":1191,"title":1192},"\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":1194,"title":1195},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1197,"title":1198},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1200,"title":1201},"\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":1203,"title":1204},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1206,"title":1207},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1209,"title":1210},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1212,"title":1213},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1215,"title":1216},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1218,"title":1219},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1095,"title":5},{"path":1222,"title":1045},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution",{"path":1224,"title":1225},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1227,"title":1051},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi",{"path":1229,"title":1230},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1232,"title":1233},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1235,"title":1236},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1238,"title":1239},"\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":1241,"title":1242},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1244,"title":1245},"\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":1247,"title":1248},"\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":1250,"title":1251},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1253,"title":1254},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867199]