[{"data":1,"prerenderedAt":1408},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F":3,"content-directory":1162},{"id":4,"title":5,"body":6,"date":1146,"description":1147,"difficulty":1148,"draft":1149,"extension":1150,"meta":1151,"navigation":209,"path":1152,"seo":1153,"stem":1154,"tags":1155,"updated":1146,"__hash__":1161},"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":1128},"minimark",[9,18,23,93,97,100,104,130,139,142,146,158,314,320,355,384,388,398,401,480,503,507,513,538,541,629,660,664,671,674,757,768,805,815,819,826,829,881,888,948,964,968,1044,1048,1053,1056,1060,1063,1067,1070,1074,1077,1081,1084,1088,1124],[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.",[101,102],"inline-diagram",{"name":103},"wheel-sdist-anatomy",[10,105,106,107,110,111,114,115,88,118,121,122,125,126,129],{},"A ",[30,108,109],{},"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,112,113],{},"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,116,117],{},"...-py3-none-any.whl",[14,119,120],{},"py3"," (any Python 3), ",[14,123,124],{},"none"," (no ABI\nconstraint), ",[14,127,128],{},"any"," (any OS) — that installs everywhere.",[10,131,106,132,135,136,138],{},[30,133,134],{},"source distribution",", or sdist, is a ",[14,137,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,140,141],{},"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,143,145],{"id":144},"configure-the-build-backend","Configure the build backend",[10,147,148,149,151,152,154,155,157],{},"The build is driven by two tables in ",[14,150,72],{},": ",[14,153,64],{}," names the backend that\nturns your source into artifacts, and ",[14,156,68],{}," supplies the metadata. Hatchling is a good\nmodern default; setuptools is the venerable alternative.",[159,160,165],"pre",{"className":161,"code":162,"language":163,"meta":164,"style":164},"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,166,167,183,195,204,211,221,230,239,248,257,266,275,286,291,305],{"__ignoreMap":164},[168,169,172,176,180],"span",{"class":170,"line":171},"line",1,[168,173,175],{"class":174},"sVt8B","[",[168,177,179],{"class":178},"sScJk","build-system",[168,181,182],{"class":174},"]\n",[168,184,186,189,193],{"class":170,"line":185},2,[168,187,188],{"class":174},"requires = [",[168,190,192],{"class":191},"sZZnC","\"hatchling\"",[168,194,182],{"class":174},[168,196,198,201],{"class":170,"line":197},3,[168,199,200],{"class":174},"build-backend = ",[168,202,203],{"class":191},"\"hatchling.build\"\n",[168,205,207],{"class":170,"line":206},4,[168,208,210],{"emptyLinePlaceholder":209},true,"\n",[168,212,214,216,219],{"class":170,"line":213},5,[168,215,175],{"class":174},[168,217,218],{"class":178},"project",[168,220,182],{"class":174},[168,222,224,227],{"class":170,"line":223},6,[168,225,226],{"class":174},"name = ",[168,228,229],{"class":191},"\"greet-cli\"\n",[168,231,233,236],{"class":170,"line":232},7,[168,234,235],{"class":174},"version = ",[168,237,238],{"class":191},"\"0.1.0\"\n",[168,240,242,245],{"class":170,"line":241},8,[168,243,244],{"class":174},"description = ",[168,246,247],{"class":191},"\"A friendly greeting CLI\"\n",[168,249,251,254],{"class":170,"line":250},9,[168,252,253],{"class":174},"readme = ",[168,255,256],{"class":191},"\"README.md\"\n",[168,258,260,263],{"class":170,"line":259},10,[168,261,262],{"class":174},"license = ",[168,264,265],{"class":191},"\"MIT\"\n",[168,267,269,272],{"class":170,"line":268},11,[168,270,271],{"class":174},"requires-python = ",[168,273,274],{"class":191},"\">=3.11\"\n",[168,276,278,281,284],{"class":170,"line":277},12,[168,279,280],{"class":174},"dependencies = [",[168,282,283],{"class":191},"\"click>=8.1\"",[168,285,182],{"class":174},[168,287,289],{"class":170,"line":288},13,[168,290,210],{"emptyLinePlaceholder":209},[168,292,294,296,298,300,303],{"class":170,"line":293},14,[168,295,175],{"class":174},[168,297,218],{"class":178},[168,299,38],{"class":174},[168,301,302],{"class":178},"scripts",[168,304,182],{"class":174},[168,306,308,311],{"class":170,"line":307},15,[168,309,310],{"class":174},"greet = ",[168,312,313],{"class":191},"\"greet_cli.__main__:main\"\n",[10,315,316,317,319],{},"The equivalent with setuptools only changes ",[14,318,64],{},":",[159,321,323],{"className":161,"code":322,"language":163,"meta":164,"style":164},"[build-system]\nrequires = [\"setuptools>=68\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n",[14,324,325,333,348],{"__ignoreMap":164},[168,326,327,329,331],{"class":170,"line":171},[168,328,175],{"class":174},[168,330,179],{"class":178},[168,332,182],{"class":174},[168,334,335,337,340,343,346],{"class":170,"line":185},[168,336,188],{"class":174},[168,338,339],{"class":191},"\"setuptools>=68\"",[168,341,342],{"class":174},", ",[168,344,345],{"class":191},"\"wheel\"",[168,347,182],{"class":174},[168,349,350,352],{"class":170,"line":197},[168,351,200],{"class":174},[168,353,354],{"class":191},"\"setuptools.build_meta\"\n",[10,356,357,358,361,362,365,366,369,370,373,374,377,378,383],{},"With a ",[14,359,360],{},"src\u002F"," layout, hatchling finds ",[14,363,364],{},"src\u002Fgreet_cli\u002F"," automatically; setuptools needs\n",[14,367,368],{},"[tool.setuptools.packages.find] where = [\"src\"]",". The ",[14,371,372],{},"[project.scripts]"," entry is what makes\nthe installed wheel expose a ",[14,375,376],{},"greet"," command — see\n",[379,380,382],"a",{"href":381},"\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,385,387],{"id":386},"run-python-m-build","Run python -m build",[10,389,390,393,394,397],{},[14,391,392],{},"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,395,396],{},"[build-system].requires"," you declared, not whatever happens to be in your current environment.",[101,399],{"name":400},"build-command-terminal",[159,402,406],{"className":403,"code":404,"language":405,"meta":164,"style":164},"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,407,408,426,440,449,456,463],{"__ignoreMap":164},[168,409,410,413,416,419,422],{"class":170,"line":171},[168,411,412],{"class":178},"$",[168,414,415],{"class":191}," pip",[168,417,418],{"class":191}," install",[168,420,421],{"class":191}," build",[168,423,425],{"class":424},"sJ8bj","            # or: uv tool install build\n",[168,427,428,430,433,437],{"class":170,"line":185},[168,429,412],{"class":178},[168,431,432],{"class":191}," python",[168,434,436],{"class":435},"sj4cs"," -m",[168,438,439],{"class":191}," build\n",[168,441,442,446],{"class":170,"line":197},[168,443,445],{"class":444},"szBVR","*",[168,447,448],{"class":174}," Creating isolated environment: venv+pip...\n",[168,450,451,453],{"class":170,"line":206},[168,452,445],{"class":444},[168,454,455],{"class":174}," Building sdist...\n",[168,457,458,460],{"class":170,"line":213},[168,459,445],{"class":444},[168,461,462],{"class":174}," Building wheel from sdist...\n",[168,464,465,468,471,474,477],{"class":170,"line":223},[168,466,467],{"class":178},"Successfully",[168,469,470],{"class":191}," built",[168,472,473],{"class":191}," greet_cli-0.1.0.tar.gz",[168,475,476],{"class":191}," and",[168,478,479],{"class":191}," greet_cli-0.1.0-py3-none-any.whl\n",[10,481,482,483,485,486,490,491,494,495,498,499,502],{},"Note the sequence: ",[14,484,392],{}," makes the sdist first, then builds the wheel ",[487,488,489],"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,492,493],{},"python -m build --wheel"," or ",[14,496,497],{},"--sdist"," when you need only one. If you use uv,\n",[14,500,501],{},"uv build"," does the same job with the same output.",[19,504,506],{"id":505},"the-dist-layout-and-inspecting-a-wheel","The dist\u002F layout and inspecting a wheel",[10,508,509,510,512],{},"After a build, ",[14,511,37],{}," holds both artifacts:",[159,514,516],{"className":403,"code":515,"language":405,"meta":164,"style":164},"$ ls dist\u002F\ngreet_cli-0.1.0-py3-none-any.whl\ngreet_cli-0.1.0.tar.gz\n",[14,517,518,528,533],{"__ignoreMap":164},[168,519,520,522,525],{"class":170,"line":171},[168,521,412],{"class":178},[168,523,524],{"class":191}," ls",[168,526,527],{"class":191}," dist\u002F\n",[168,529,530],{"class":170,"line":185},[168,531,532],{"class":178},"greet_cli-0.1.0-py3-none-any.whl\n",[168,534,535],{"class":170,"line":197},[168,536,537],{"class":178},"greet_cli-0.1.0.tar.gz\n",[10,539,540],{},"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:",[159,542,544],{"className":403,"code":543,"language":405,"meta":164,"style":164},"$ 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,545,546,559,567,575,583,591,599,607,615,622],{"__ignoreMap":164},[168,547,548,550,553,556],{"class":170,"line":171},[168,549,412],{"class":178},[168,551,552],{"class":191}," unzip",[168,554,555],{"class":435}," -l",[168,557,558],{"class":191}," dist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n",[168,560,561,564],{"class":170,"line":185},[168,562,563],{"class":178},"Archive:",[168,565,566],{"class":191},"  dist\u002Fgreet_cli-0.1.0-py3-none-any.whl\n",[168,568,569,572],{"class":170,"line":197},[168,570,571],{"class":178},"    Length",[168,573,574],{"class":191},"      Name\n",[168,576,577,580],{"class":170,"line":206},[168,578,579],{"class":178},"   --------",[168,581,582],{"class":435},"      ----\n",[168,584,585,588],{"class":170,"line":213},[168,586,587],{"class":178},"        312",[168,589,590],{"class":191},"      greet_cli\u002F__init__.py\n",[168,592,593,596],{"class":170,"line":223},[168,594,595],{"class":178},"        684",[168,597,598],{"class":191},"      greet_cli\u002F__main__.py\n",[168,600,601,604],{"class":170,"line":232},[168,602,603],{"class":178},"       1024",[168,605,606],{"class":191},"      greet_cli-0.1.0.dist-info\u002FMETADATA\n",[168,608,609,612],{"class":170,"line":241},[168,610,611],{"class":435},"        ...",[168,613,614],{"class":191},"      greet_cli-0.1.0.dist-info\u002FRECORD\n",[168,616,617,619],{"class":170,"line":250},[168,618,611],{"class":435},[168,620,621],{"class":191},"      greet_cli-0.1.0.dist-info\u002FWHEEL\n",[168,623,624,626],{"class":170,"line":259},[168,625,611],{"class":435},[168,627,628],{"class":191},"      greet_cli-0.1.0.dist-info\u002Fentry_points.txt\n",[10,630,631,632,635,636,639,640,642,643,646,647,649,650,653,654,656,657,38],{},"The ",[14,633,634],{},".dist-info\u002F"," directory holds the metadata: ",[14,637,638],{},"METADATA"," (your ",[14,641,68],{}," fields),\n",[14,644,645],{},"entry_points.txt"," (your console script — check the ",[14,648,376],{}," line is there), and ",[14,651,652],{},"RECORD"," (a\nmanifest with hashes). If your command is missing from ",[14,655,645],{},", 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,658,659],{},"tar tzf dist\u002Fgreet_cli-0.1.0.tar.gz",[19,661,663],{"id":662},"including-package-data","Including package data",[10,665,666,667,670],{},"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,668,669],{},"py.typed"," marker, tell the backend to include them.",[10,672,673],{},"With hatchling, force-include files or whole directories:",[159,675,677],{"className":161,"code":676,"language":163,"meta":164,"style":164},"[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,678,679,706,716,720,749],{"__ignoreMap":164},[168,680,681,683,686,688,691,693,695,697,700,702,704],{"class":170,"line":171},[168,682,175],{"class":174},[168,684,685],{"class":178},"tool",[168,687,38],{"class":174},[168,689,690],{"class":178},"hatch",[168,692,38],{"class":174},[168,694,392],{"class":178},[168,696,38],{"class":174},[168,698,699],{"class":178},"targets",[168,701,38],{"class":174},[168,703,109],{"class":178},[168,705,182],{"class":174},[168,707,708,711,714],{"class":170,"line":185},[168,709,710],{"class":174},"include = [",[168,712,713],{"class":191},"\"src\u002Fgreet_cli\"",[168,715,182],{"class":174},[168,717,718],{"class":170,"line":197},[168,719,210],{"emptyLinePlaceholder":209},[168,721,722,724,726,728,730,732,734,736,738,740,742,744,747],{"class":170,"line":206},[168,723,175],{"class":174},[168,725,685],{"class":178},[168,727,38],{"class":174},[168,729,690],{"class":178},[168,731,38],{"class":174},[168,733,392],{"class":178},[168,735,38],{"class":174},[168,737,699],{"class":178},[168,739,38],{"class":174},[168,741,109],{"class":178},[168,743,38],{"class":174},[168,745,746],{"class":178},"force-include",[168,748,182],{"class":174},[168,750,751,754],{"class":170,"line":213},[168,752,753],{"class":174},"\"src\u002Fgreet_cli\u002Ftemplates\" = ",[168,755,756],{"class":191},"\"greet_cli\u002Ftemplates\"\n",[10,758,759,760,763,764,767],{},"With setuptools, use ",[14,761,762],{},"package-data"," (and a ",[14,765,766],{},"MANIFEST.in"," for what goes into the sdist):",[159,769,771],{"className":161,"code":770,"language":163,"meta":164,"style":164},"[tool.setuptools.package-data]\ngreet_cli = [\"templates\u002F*.txt\", \"py.typed\"]\n",[14,772,773,790],{"__ignoreMap":164},[168,774,775,777,779,781,784,786,788],{"class":170,"line":171},[168,776,175],{"class":174},[168,778,685],{"class":178},[168,780,38],{"class":174},[168,782,783],{"class":178},"setuptools",[168,785,38],{"class":174},[168,787,762],{"class":178},[168,789,182],{"class":174},[168,791,792,795,798,800,803],{"class":170,"line":185},[168,793,794],{"class":174},"greet_cli = [",[168,796,797],{"class":191},"\"templates\u002F*.txt\"",[168,799,342],{"class":174},[168,801,802],{"class":191},"\"py.typed\"",[168,804,182],{"class":174},[10,806,807,808,810,811,814],{},"Then always confirm with ",[14,809,91],{}," that the data actually landed in the wheel. At runtime,\nread those files with ",[14,812,813],{},"importlib.resources",", never a relative filesystem path — the installed\npackage may live inside a ZIP or a location you cannot guess.",[19,816,818],{"id":817},"verify-with-twine-check-and-a-smoke-test","Verify with twine check and a smoke test",[10,820,821,822,825],{},"Two cheap checks catch most release failures. First, ",[14,823,824],{},"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:",[101,827],{"name":828},"build-verification-flow",[159,830,832],{"className":403,"code":831,"language":405,"meta":164,"style":164},"$ 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,833,834,845,861,872],{"__ignoreMap":164},[168,835,836,838,840,842],{"class":170,"line":171},[168,837,412],{"class":178},[168,839,415],{"class":191},[168,841,418],{"class":191},[168,843,844],{"class":191}," twine\n",[168,846,847,849,852,855,858],{"class":170,"line":185},[168,848,412],{"class":178},[168,850,851],{"class":191}," twine",[168,853,854],{"class":191}," check",[168,856,857],{"class":191}," dist\u002F",[168,859,860],{"class":435},"*\n",[168,862,863,866,869],{"class":170,"line":197},[168,864,865],{"class":178},"Checking",[168,867,868],{"class":191}," dist\u002Fgreet_cli-0.1.0-py3-none-any.whl:",[168,870,871],{"class":191}," PASSED\n",[168,873,874,876,879],{"class":170,"line":206},[168,875,865],{"class":178},[168,877,878],{"class":191}," dist\u002Fgreet_cli-0.1.0.tar.gz:",[168,880,871],{"class":191},[10,882,883,884,887],{},"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 ",[487,885,886],{},"built artifact"," works; only installing it does.",[159,889,891],{"className":403,"code":890,"language":405,"meta":164,"style":164},"$ 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,892,893,907,918,928,936],{"__ignoreMap":164},[168,894,895,897,899,901,904],{"class":170,"line":171},[168,896,412],{"class":178},[168,898,432],{"class":191},[168,900,436],{"class":435},[168,902,903],{"class":191}," venv",[168,905,906],{"class":191}," \u002Ftmp\u002Fsmoke\n",[168,908,909,911,914,916],{"class":170,"line":185},[168,910,412],{"class":178},[168,912,913],{"class":191}," \u002Ftmp\u002Fsmoke\u002Fbin\u002Fpip",[168,915,418],{"class":191},[168,917,558],{"class":191},[168,919,920,922,925],{"class":170,"line":197},[168,921,412],{"class":178},[168,923,924],{"class":191}," \u002Ftmp\u002Fsmoke\u002Fbin\u002Fgreet",[168,926,927],{"class":191}," World\n",[168,929,930,933],{"class":170,"line":206},[168,931,932],{"class":178},"Hello,",[168,934,935],{"class":191}," World!\n",[168,937,938,940,943,946],{"class":170,"line":213},[168,939,412],{"class":178},[168,941,942],{"class":191}," rm",[168,944,945],{"class":435}," -rf",[168,947,906],{"class":191},[10,949,950,951,954,955,959,960,38],{},"If you use pipx, ",[14,952,953],{},"pipx install .\u002Fdist\u002Fgreet_cli-0.1.0-py3-none-any.whl"," is an even quicker\nisolated smoke test — see\n",[379,956,958],{"href":957},"\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",[379,961,963],{"href":962},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi\u002F","publish to PyPI",[19,965,967],{"id":966},"production-notes","Production notes",[24,969,970,986,996,1021,1034],{},[27,971,972,78,978,981,982,985],{},[30,973,974,975,977],{},"Clean ",[14,976,37],{}," between builds.",[14,979,980],{},"twine upload dist\u002F*"," uploads everything in the directory,\nso a stale wheel from a previous version can sneak into a release. ",[14,983,984],{},"rm -rf dist\u002F"," before\nbuilding, or upload a specific file.",[27,987,988,991,992,995],{},[30,989,990],{},"Reproducibility."," Set ",[14,993,994],{},"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,997,998,1001,1002,1005,1006,1008,1009,1012,1013,1016,1017,38],{},[30,999,1000],{},"Version single-sourcing."," Rather than hand-editing ",[14,1003,1004],{},"version"," in ",[14,1007,72],{},", derive it\nfrom a git tag with ",[14,1010,1011],{},"hatch-vcs"," (or ",[14,1014,1015],{},"setuptools-scm",") so the built artifact's version always\nmatches the tag. This dovetails with\n",[379,1018,1020],{"href":1019},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning and changelogs",[27,1022,1023,78,1026,1029,1030,1033],{},[30,1024,1025],{},"Pin the backend.",[14,1027,1028],{},"requires = [\"hatchling\"]"," without a floor can pull a new major backend\nthat changes defaults; pin a lower bound (",[14,1031,1032],{},"hatchling>=1.25",") once you find a version that\nworks.",[27,1035,1036,1039,1040,1043],{},[30,1037,1038],{},"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,1041,1042],{},"cibuildwheel"," — a different and larger topic.",[19,1045,1047],{"id":1046},"frequently-asked-questions","Frequently asked questions",[1049,1050,1052],"h3",{"id":1051},"do-i-have-to-publish-an-sdist-as-well-as-a-wheel","Do I have to publish an sdist as well as a wheel?",[10,1054,1055],{},"You should. The wheel is what everyone installs, but the sdist is what lets a distribution packager, an auditor or a user on an unusual platform rebuild your tool from source. It costs nothing extra — python -m build produces both in one command — and PyPI keeps them side by side.",[1049,1057,1059],{"id":1058},"what-does-py3-none-any-in-the-filename-mean","What does py3-none-any in the filename mean?",[10,1061,1062],{},"It is the compatibility tag: pure Python 3, no ABI requirement, any platform. That is what a CLI written in Python should produce. If you see a platform tag such as cp312-manylinux, your project is compiling an extension, which changes distribution completely — you then need a wheel per platform and a build matrix in CI.",[1049,1064,1066],{"id":1065},"why-does-my-wheel-install-but-the-command-not-run","Why does my wheel install but the command not run?",[10,1068,1069],{},"Nine times in ten the module named by the entry point is not in the wheel. Run unzip -l on the artifact and look for your package directory; if it is missing or half there, the build backend's package discovery is not finding your source layout. The fix is in pyproject.toml, and the test that catches it is installing the wheel into an empty environment.",[1049,1071,1073],{"id":1072},"should-dist-be-committed-or-cleaned-between-builds","Should dist\u002F be committed or cleaned between builds?",[10,1075,1076],{},"Never commit it, and always clean it. A stale wheel from an earlier version sitting in dist\u002F is the classic way to upload the wrong artifact, because twine uploads everything it is given. Delete the directory in the build step and let CI produce it fresh.",[1049,1078,1080],{"id":1079},"what-does-twine-check-actually-verify","What does twine check actually verify?",[10,1082,1083],{},"That the metadata is well formed and the long description will render on PyPI — nothing more. It will not tell you the package is importable or the entry point resolves. Treat it as a lint for your project page, and pair it with an install-and-run smoke test for everything it does not cover.",[19,1085,1087],{"id":1086},"related","Related",[24,1089,1090,1097,1103,1109,1118],{},[27,1091,1092,1096],{},[379,1093,1095],{"href":1094},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","Packaging Python CLIs for Distribution"," — the overview this guide sits under.",[27,1098,1099,1102],{},[379,1100,1101],{"href":962},"Publishing a Python CLI to PyPI"," — upload the artifacts you just built and verified.",[27,1104,1105,1108],{},[379,1106,1107],{"href":957},"Installing and distributing CLIs with pipx"," — install a local wheel as a quick isolated smoke test.",[27,1110,1111,88,1115,1117],{},[379,1112,1114],{"href":1113},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002F","uv for Python CLI dependency management",[14,1116,501],{}," produces the same wheel and sdist.",[27,1119,1120,1123],{},[379,1121,1122],{"href":1019},"Managing CLI versioning and changelogs"," — single-source the version your build stamps in.",[1125,1126,1127],"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":164,"searchDepth":185,"depth":185,"links":1129},[1130,1131,1132,1133,1134,1135,1136,1137,1138,1145],{"id":21,"depth":185,"text":22},{"id":95,"depth":185,"text":96},{"id":144,"depth":185,"text":145},{"id":386,"depth":185,"text":387},{"id":505,"depth":185,"text":506},{"id":662,"depth":185,"text":663},{"id":817,"depth":185,"text":818},{"id":966,"depth":185,"text":967},{"id":1046,"depth":185,"text":1047,"children":1139},[1140,1141,1142,1143,1144],{"id":1051,"depth":197,"text":1052},{"id":1058,"depth":197,"text":1059},{"id":1065,"depth":197,"text":1066},{"id":1072,"depth":197,"text":1073},{"id":1079,"depth":197,"text":1080},{"id":1086,"depth":185,"text":1087},"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":1147},"project-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002Findex",[1156,1157,1158,1159,1160],"wheels","packaging","distribution","cli","pypi","FlAzE6OG5GYoIdy5QG-ajvQvVnLjQjyVwLeouhendqk",[1163,1166,1169,1172,1175,1178,1181,1184,1187,1190,1193,1196,1199,1202,1205,1208,1211,1214,1217,1220,1223,1226,1229,1232,1235,1238,1241,1244,1247,1250,1253,1256,1259,1262,1265,1268,1271,1274,1277,1280,1283,1286,1289,1292,1295,1298,1301,1304,1307,1310,1313,1316,1319,1322,1325,1328,1331,1334,1337,1340,1343,1346,1349,1352,1355,1358,1361,1364,1367,1370,1371,1373,1376,1378,1381,1384,1387,1390,1393,1396,1399,1402,1405],{"path":1164,"title":1165},"\u002Fabout","About Python CLI Toolcraft",{"path":1167,"title":1168},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1170,"title":1171},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1173,"title":1174},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1176,"title":1177},"\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":1179,"title":1180},"\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":1182,"title":1183},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1185,"title":1186},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1188,"title":1189},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1191,"title":1192},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1194,"title":1195},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1197,"title":1198},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1200,"title":1201},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1203,"title":1204},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1206,"title":1207},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1209,"title":1210},"\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":1212,"title":1213},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1215,"title":1216},"\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":1218,"title":1219},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1221,"title":1222},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1224,"title":1225},"\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":1227,"title":1228},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1230,"title":1231},"\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":1233,"title":1234},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1236,"title":1237},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1239,"title":1240},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1242,"title":1243},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1245,"title":1246},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1248,"title":1249},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1251,"title":1252},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1254,"title":1255},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1257,"title":1258},"\u002F","Python CLI Toolcraft",{"path":1260,"title":1261},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1263,"title":1264},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1266,"title":1267},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1269,"title":1270},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1272,"title":1273},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1275,"title":1276},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1278,"title":1279},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1281,"title":1282},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1284,"title":1285},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1287,"title":1288},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1290,"title":1291},"\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":1293,"title":1294},"\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":1296,"title":1297},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1299,"title":1300},"\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":1302,"title":1303},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1305,"title":1306},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1308,"title":1309},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1311,"title":1312},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1314,"title":1315},"\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":1317,"title":1318},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1320,"title":1321},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1323,"title":1324},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1326,"title":1327},"\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":1329,"title":1330},"\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":1332,"title":1333},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1335,"title":1336},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1338,"title":1339},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1341,"title":1342},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1344,"title":1345},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1347,"title":1348},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1350,"title":1351},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1353,"title":1354},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1356,"title":1357},"\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":1359,"title":1360},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1362,"title":1363},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1365,"title":1366},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1368,"title":1369},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1152,"title":5},{"path":1372,"title":1095},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution",{"path":1374,"title":1375},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1377,"title":1101},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi",{"path":1379,"title":1380},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1382,"title":1383},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1385,"title":1386},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1388,"title":1389},"\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":1391,"title":1392},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1394,"title":1395},"\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":1397,"title":1398},"\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":1400,"title":1401},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1403,"title":1404},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1406,"title":1407},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690035]