[{"data":1,"prerenderedAt":1532},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002F":3,"content-directory":1285},{"id":4,"title":5,"body":6,"date":1271,"description":1272,"difficulty":1273,"draft":1274,"extension":1275,"meta":1276,"navigation":210,"path":1277,"seo":1278,"stem":1279,"tags":1280,"updated":1271,"__hash__":1284},"content\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002Findex.md","Bundling a Python CLI with PyInstaller",{"type":7,"value":8,"toc":1249},"minimark",[9,18,23,82,86,89,122,125,129,163,167,177,264,267,485,502,512,516,519,536,539,591,663,670,692,696,699,706,798,803,866,873,877,880,893,906,919,923,926,936,998,1010,1019,1023,1026,1092,1095,1099,1105,1109,1114,1123,1127,1130,1134,1149,1153,1159,1163,1166,1170,1184,1188,1191,1195,1206,1210,1245],[10,11,12,13,17],"p",{},"PyInstaller bundles your code, its dependencies and the Python interpreter into one executable, so\na user with no Python can run your tool. The build itself is one command; making it ",[14,15,16],"em",{},"reliable","\ncomes down to two things static analysis cannot see — dynamic imports and file paths.",[19,20,22],"h2",{"id":21},"tldr","TL;DR",[24,25,26,38,46,60,72],"ul",{},[27,28,29,33,34,37],"li",{},[30,31,32],"code",{},"pyinstaller --onefile --name mytool src\u002Fmytool\u002F__main__.py"," produces ",[30,35,36],{},"dist\u002Fmytool",".",[27,39,40,41,45],{},"Commit a ",[42,43,44],"strong",{},"spec file"," rather than repeating flags; it is the only place complex builds stay\nreadable.",[27,47,48,49,54,55,37],{},"Declare anything imported dynamically in ",[42,50,51],{},[30,52,53],{},"hiddenimports",", and bundle data in ",[42,56,57],{},[30,58,59],{},"datas",[27,61,62,63,68,69,37],{},"Read bundled files with ",[42,64,65],{},[30,66,67],{},"importlib.resources",", never from ",[30,70,71],{},"__file__",[27,73,74,77,78,81],{},[30,75,76],{},"--onefile"," re-extracts on every run; ",[30,79,80],{},"--onedir"," starts faster and is easier to debug.",[19,83,85],{"id":84},"prerequisites","Prerequisites",[10,87,88],{},"A working CLI installed in a virtual environment, and PyInstaller in that same environment:",[90,91,96],"pre",{"className":92,"code":93,"language":94,"meta":95,"style":95},"language-bash shiki shiki-themes github-light github-dark","uv add --dev pyinstaller        # or: pip install pyinstaller\n","bash","",[30,97,98],{"__ignoreMap":95},[99,100,103,107,111,115,118],"span",{"class":101,"line":102},"line",1,[99,104,106],{"class":105},"sScJk","uv",[99,108,110],{"class":109},"sZZnC"," add",[99,112,114],{"class":113},"sj4cs"," --dev",[99,116,117],{"class":109}," pyinstaller",[99,119,121],{"class":120},"sJ8bj","        # or: pip install pyinstaller\n",[10,123,124],{},"Build from an environment containing exactly your runtime dependencies. Freezing from a\nkitchen-sink environment produces a much larger binary and hides missing declarations, because\neverything happens to be importable.",[19,126,128],{"id":127},"a-first-build","A first build",[90,130,132],{"className":92,"code":131,"language":94,"meta":95,"style":95},"pyinstaller --onefile --name mytool --clean src\u002Fmytool\u002F__main__.py\n.\u002Fdist\u002Fmytool --version\n",[30,133,134,154],{"__ignoreMap":95},[99,135,136,139,142,145,148,151],{"class":101,"line":102},[99,137,138],{"class":105},"pyinstaller",[99,140,141],{"class":113}," --onefile",[99,143,144],{"class":113}," --name",[99,146,147],{"class":109}," mytool",[99,149,150],{"class":113}," --clean",[99,152,153],{"class":109}," src\u002Fmytool\u002F__main__.py\n",[99,155,157,160],{"class":101,"line":156},2,[99,158,159],{"class":105},".\u002Fdist\u002Fmytool",[99,161,162],{"class":113}," --version\n",[164,165],"inline-diagram",{"name":166},"pyinstaller-build-flow",[10,168,169,170,173,174,176],{},"Point it at ",[30,171,172],{},"__main__.py"," rather than at the console script. The generated shim is an installation\nartifact that assumes a specific environment layout; ",[30,175,172],{}," is your own three-line entry\npoint and freezes cleanly:",[90,178,182],{"className":179,"code":180,"language":181,"meta":95,"style":95},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002F__main__.py\nfrom mytool.cli import app\n\ndef main() -> None:\n    app()\n\nif __name__ == \"__main__\":\n    main()\n","python",[30,183,184,189,205,212,230,236,241,258],{"__ignoreMap":95},[99,185,186],{"class":101,"line":102},[99,187,188],{"class":120},"# src\u002Fmytool\u002F__main__.py\n",[99,190,191,195,199,202],{"class":101,"line":156},[99,192,194],{"class":193},"szBVR","from",[99,196,198],{"class":197},"sVt8B"," mytool.cli ",[99,200,201],{"class":193},"import",[99,203,204],{"class":197}," app\n",[99,206,208],{"class":101,"line":207},3,[99,209,211],{"emptyLinePlaceholder":210},true,"\n",[99,213,215,218,221,224,227],{"class":101,"line":214},4,[99,216,217],{"class":193},"def",[99,219,220],{"class":105}," main",[99,222,223],{"class":197},"() -> ",[99,225,226],{"class":113},"None",[99,228,229],{"class":197},":\n",[99,231,233],{"class":101,"line":232},5,[99,234,235],{"class":197},"    app()\n",[99,237,239],{"class":101,"line":238},6,[99,240,211],{"emptyLinePlaceholder":210},[99,242,244,247,250,253,256],{"class":101,"line":243},7,[99,245,246],{"class":193},"if",[99,248,249],{"class":113}," __name__",[99,251,252],{"class":193}," ==",[99,254,255],{"class":109}," \"__main__\"",[99,257,229],{"class":197},[99,259,261],{"class":101,"line":260},8,[99,262,263],{"class":197},"    main()\n",[10,265,266],{},"Once the build works, move the configuration into a spec file and commit it. The spec is a Python\nscript, so it can compute values — which is how the hidden-import list stays in sync with your\ncommand registry:",[90,268,270],{"className":179,"code":269,"language":181,"meta":95,"style":95},"# mytool.spec\nfrom PyInstaller.utils.hooks import collect_submodules\n\nhiddenimports = collect_submodules(\"mytool.commands\") + [\"mytool.plugins\"]\n\na = Analysis(\n    [\"src\u002Fmytool\u002F__main__.py\"],\n    pathex=[\"src\"],\n    hiddenimports=hiddenimports,\n    datas=[(\"src\u002Fmytool\u002Ftemplates\", \"mytool\u002Ftemplates\")],\n    excludes=[\"tkinter\", \"test\", \"pytest\"],\n)\n\npyz = PYZ(a.pure)\nexe = EXE(pyz, a.scripts, a.binaries, a.datas, name=\"mytool\", console=True, upx=False)\n",[30,271,272,277,289,293,322,326,336,347,363,374,397,422,428,433,444],{"__ignoreMap":95},[99,273,274],{"class":101,"line":102},[99,275,276],{"class":120},"# mytool.spec\n",[99,278,279,281,284,286],{"class":101,"line":156},[99,280,194],{"class":193},[99,282,283],{"class":197}," PyInstaller.utils.hooks ",[99,285,201],{"class":193},[99,287,288],{"class":197}," collect_submodules\n",[99,290,291],{"class":101,"line":207},[99,292,211],{"emptyLinePlaceholder":210},[99,294,295,298,301,304,307,310,313,316,319],{"class":101,"line":214},[99,296,297],{"class":197},"hiddenimports ",[99,299,300],{"class":193},"=",[99,302,303],{"class":197}," collect_submodules(",[99,305,306],{"class":109},"\"mytool.commands\"",[99,308,309],{"class":197},") ",[99,311,312],{"class":193},"+",[99,314,315],{"class":197}," [",[99,317,318],{"class":109},"\"mytool.plugins\"",[99,320,321],{"class":197},"]\n",[99,323,324],{"class":101,"line":232},[99,325,211],{"emptyLinePlaceholder":210},[99,327,328,331,333],{"class":101,"line":238},[99,329,330],{"class":197},"a ",[99,332,300],{"class":193},[99,334,335],{"class":197}," Analysis(\n",[99,337,338,341,344],{"class":101,"line":243},[99,339,340],{"class":197},"    [",[99,342,343],{"class":109},"\"src\u002Fmytool\u002F__main__.py\"",[99,345,346],{"class":197},"],\n",[99,348,349,353,355,358,361],{"class":101,"line":260},[99,350,352],{"class":351},"s4XuR","    pathex",[99,354,300],{"class":193},[99,356,357],{"class":197},"[",[99,359,360],{"class":109},"\"src\"",[99,362,346],{"class":197},[99,364,366,369,371],{"class":101,"line":365},9,[99,367,368],{"class":351},"    hiddenimports",[99,370,300],{"class":193},[99,372,373],{"class":197},"hiddenimports,\n",[99,375,377,380,382,385,388,391,394],{"class":101,"line":376},10,[99,378,379],{"class":351},"    datas",[99,381,300],{"class":193},[99,383,384],{"class":197},"[(",[99,386,387],{"class":109},"\"src\u002Fmytool\u002Ftemplates\"",[99,389,390],{"class":197},", ",[99,392,393],{"class":109},"\"mytool\u002Ftemplates\"",[99,395,396],{"class":197},")],\n",[99,398,400,403,405,407,410,412,415,417,420],{"class":101,"line":399},11,[99,401,402],{"class":351},"    excludes",[99,404,300],{"class":193},[99,406,357],{"class":197},[99,408,409],{"class":109},"\"tkinter\"",[99,411,390],{"class":197},[99,413,414],{"class":109},"\"test\"",[99,416,390],{"class":197},[99,418,419],{"class":109},"\"pytest\"",[99,421,346],{"class":197},[99,423,425],{"class":101,"line":424},12,[99,426,427],{"class":197},")\n",[99,429,431],{"class":101,"line":430},13,[99,432,211],{"emptyLinePlaceholder":210},[99,434,436,439,441],{"class":101,"line":435},14,[99,437,438],{"class":197},"pyz ",[99,440,300],{"class":193},[99,442,443],{"class":197}," PYZ(a.pure)\n",[99,445,447,450,452,455,458,460,463,465,468,470,473,475,478,480,483],{"class":101,"line":446},15,[99,448,449],{"class":197},"exe ",[99,451,300],{"class":193},[99,453,454],{"class":197}," EXE(pyz, a.scripts, a.binaries, a.datas, ",[99,456,457],{"class":351},"name",[99,459,300],{"class":193},[99,461,462],{"class":109},"\"mytool\"",[99,464,390],{"class":197},[99,466,467],{"class":351},"console",[99,469,300],{"class":193},[99,471,472],{"class":113},"True",[99,474,390],{"class":197},[99,476,477],{"class":351},"upx",[99,479,300],{"class":193},[99,481,482],{"class":113},"False",[99,484,427],{"class":197},[90,486,488],{"className":92,"code":487,"language":94,"meta":95,"style":95},"pyinstaller mytool.spec --clean --noconfirm\n",[30,489,490],{"__ignoreMap":95},[99,491,492,494,497,499],{"class":101,"line":102},[99,493,138],{"class":105},[99,495,496],{"class":109}," mytool.spec",[99,498,150],{"class":113},[99,500,501],{"class":113}," --noconfirm\n",[10,503,504,507,508,511],{},[30,505,506],{},"excludes"," is worth populating: ",[30,509,510],{},"tkinter"," alone adds several megabytes to a tool that will never\ndraw a window.",[19,513,515],{"id":514},"the-imports-pyinstaller-cannot-see","The imports PyInstaller cannot see",[164,517],{"name":518},"hidden-imports-checklist",[10,520,521,522,524,525,528,529,531,532,535],{},"Static analysis follows ",[30,523,201],{}," statements. It does not follow ",[30,526,527],{},"importlib.import_module(name)","\nwhere ",[30,530,457],{}," is a string, and it does not know that ",[30,533,534],{},"entry_points()"," will return anything.",[10,537,538],{},"That means the two patterns this handbook otherwise recommends — lazy command loading and\nentry-point plugins — both need declaring:",[90,540,542],{"className":179,"code":541,"language":181,"meta":95,"style":95},"# The registry your LazyGroup reads\nCOMMANDS = {\n    \"sync\": \"mytool.commands.sync:sync\",\n    \"report\": \"mytool.commands.report:report\",\n}\n",[30,543,544,549,560,574,586],{"__ignoreMap":95},[99,545,546],{"class":101,"line":102},[99,547,548],{"class":120},"# The registry your LazyGroup reads\n",[99,550,551,554,557],{"class":101,"line":156},[99,552,553],{"class":113},"COMMANDS",[99,555,556],{"class":193}," =",[99,558,559],{"class":197}," {\n",[99,561,562,565,568,571],{"class":101,"line":207},[99,563,564],{"class":109},"    \"sync\"",[99,566,567],{"class":197},": ",[99,569,570],{"class":109},"\"mytool.commands.sync:sync\"",[99,572,573],{"class":197},",\n",[99,575,576,579,581,584],{"class":101,"line":214},[99,577,578],{"class":109},"    \"report\"",[99,580,567],{"class":197},[99,582,583],{"class":109},"\"mytool.commands.report:report\"",[99,585,573],{"class":197},[99,587,588],{"class":101,"line":232},[99,589,590],{"class":197},"}\n",[90,592,594],{"className":179,"code":593,"language":181,"meta":95,"style":95},"# mytool.spec — derive the list rather than maintaining a second copy\nimport tomllib, pathlib\nfrom mytool.registry import COMMANDS\n\nhiddenimports = sorted({target.split(\":\")[0] for target in COMMANDS.values()})\n",[30,595,596,601,608,620,624],{"__ignoreMap":95},[99,597,598],{"class":101,"line":102},[99,599,600],{"class":120},"# mytool.spec — derive the list rather than maintaining a second copy\n",[99,602,603,605],{"class":101,"line":156},[99,604,201],{"class":193},[99,606,607],{"class":197}," tomllib, pathlib\n",[99,609,610,612,615,617],{"class":101,"line":207},[99,611,194],{"class":193},[99,613,614],{"class":197}," mytool.registry ",[99,616,201],{"class":193},[99,618,619],{"class":113}," COMMANDS\n",[99,621,622],{"class":101,"line":214},[99,623,211],{"emptyLinePlaceholder":210},[99,625,626,628,630,633,636,639,642,645,648,651,654,657,660],{"class":101,"line":232},[99,627,297],{"class":197},[99,629,300],{"class":193},[99,631,632],{"class":113}," sorted",[99,634,635],{"class":197},"({target.split(",[99,637,638],{"class":109},"\":\"",[99,640,641],{"class":197},")[",[99,643,644],{"class":113},"0",[99,646,647],{"class":197},"] ",[99,649,650],{"class":193},"for",[99,652,653],{"class":197}," target ",[99,655,656],{"class":193},"in",[99,658,659],{"class":113}," COMMANDS",[99,661,662],{"class":197},".values()})\n",[10,664,665,666,669],{},"Deriving it is the point. A hand-maintained list in the spec file is a second source of truth that\nfalls behind the day someone adds a command, and the symptom is a ",[30,667,668],{},"ModuleNotFoundError"," that only\nhappens for users.",[10,671,672,673,676,677,680,681,684,685,687,688,691],{},"For third-party packages that load things dynamically — ",[30,674,675],{},"httpx"," transports, database drivers,\n",[30,678,679],{},"rich"," themes — PyInstaller ships ",[14,682,683],{},"hooks"," that already know. When one is missing, the fix is either\na ",[30,686,53],{}," entry or a small hook file of your own in a ",[30,689,690],{},"hookspath"," directory.",[19,693,695],{"id":694},"paths-inside-a-bundle","Paths inside a bundle",[164,697],{"name":698},"frozen-runtime-paths",[10,700,701,702,705],{},"At run time a one-file build extracts itself into a temporary directory and sets ",[30,703,704],{},"sys._MEIPASS"," to\nit. Anything that assumed your source tree exists on disk is now wrong:",[90,707,709],{"className":179,"code":708,"language":181,"meta":95,"style":95},"# breaks once frozen\ntemplate = (Path(__file__).parent \u002F \"templates\" \u002F \"report.html\").read_text()\n\n# works installed, zipped and frozen\nfrom importlib.resources import files\ntemplate = files(\"mytool.templates\").joinpath(\"report.html\").read_text(encoding=\"utf-8\")\n",[30,710,711,716,746,750,755,767],{"__ignoreMap":95},[99,712,713],{"class":101,"line":102},[99,714,715],{"class":120},"# breaks once frozen\n",[99,717,718,721,723,726,728,731,734,737,740,743],{"class":101,"line":156},[99,719,720],{"class":197},"template ",[99,722,300],{"class":193},[99,724,725],{"class":197}," (Path(",[99,727,71],{"class":113},[99,729,730],{"class":197},").parent ",[99,732,733],{"class":193},"\u002F",[99,735,736],{"class":109}," \"templates\"",[99,738,739],{"class":193}," \u002F",[99,741,742],{"class":109}," \"report.html\"",[99,744,745],{"class":197},").read_text()\n",[99,747,748],{"class":101,"line":207},[99,749,211],{"emptyLinePlaceholder":210},[99,751,752],{"class":101,"line":214},[99,753,754],{"class":120},"# works installed, zipped and frozen\n",[99,756,757,759,762,764],{"class":101,"line":232},[99,758,194],{"class":193},[99,760,761],{"class":197}," importlib.resources ",[99,763,201],{"class":193},[99,765,766],{"class":197}," files\n",[99,768,769,771,773,776,779,782,785,788,791,793,796],{"class":101,"line":238},[99,770,720],{"class":197},[99,772,300],{"class":193},[99,774,775],{"class":197}," files(",[99,777,778],{"class":109},"\"mytool.templates\"",[99,780,781],{"class":197},").joinpath(",[99,783,784],{"class":109},"\"report.html\"",[99,786,787],{"class":197},").read_text(",[99,789,790],{"class":351},"encoding",[99,792,300],{"class":193},[99,794,795],{"class":109},"\"utf-8\"",[99,797,427],{"class":197},[10,799,800,802],{},[30,801,67],{}," needs no branch and no knowledge of freezing, which is why it is worth\nadopting long before you bundle anything. When you genuinely need the bundle root — to locate a\nsibling binary you shipped, say — read it explicitly:",[90,804,806],{"className":179,"code":805,"language":181,"meta":95,"style":95},"import sys\nfrom pathlib import Path\n\ndef bundle_root() -> Path:\n    return Path(getattr(sys, \"_MEIPASS\", Path(__file__).resolve().parent))\n",[30,807,808,815,827,831,841],{"__ignoreMap":95},[99,809,810,812],{"class":101,"line":102},[99,811,201],{"class":193},[99,813,814],{"class":197}," sys\n",[99,816,817,819,822,824],{"class":101,"line":156},[99,818,194],{"class":193},[99,820,821],{"class":197}," pathlib ",[99,823,201],{"class":193},[99,825,826],{"class":197}," Path\n",[99,828,829],{"class":101,"line":207},[99,830,211],{"emptyLinePlaceholder":210},[99,832,833,835,838],{"class":101,"line":214},[99,834,217],{"class":193},[99,836,837],{"class":105}," bundle_root",[99,839,840],{"class":197},"() -> Path:\n",[99,842,843,846,849,852,855,858,861,863],{"class":101,"line":232},[99,844,845],{"class":193},"    return",[99,847,848],{"class":197}," Path(",[99,850,851],{"class":113},"getattr",[99,853,854],{"class":197},"(sys, ",[99,856,857],{"class":109},"\"_MEIPASS\"",[99,859,860],{"class":197},", Path(",[99,862,71],{"class":113},[99,864,865],{"class":197},").resolve().parent))\n",[10,867,868,869,872],{},"Note what does ",[14,870,871],{},"not"," change: the user's working directory. Relative paths the user typed still\nresolve against where they ran the command, which is correct and occasionally surprising when you\nare debugging a path problem.",[19,874,876],{"id":875},"one-file-or-one-directory","One file or one directory",[164,878],{"name":879},"onefile-vs-onedir",[10,881,882,884,885,888,889,892],{},[30,883,76],{}," is a single executable that unpacks its whole bundle to a temporary directory on\n",[42,886,887],{},"every"," invocation. For a tool with a large dependency tree that can add hundreds of\nmilliseconds, charged on ",[30,890,891],{},"--help"," and on every tab completion.",[10,894,895,897,898,901,902,905],{},[30,896,80],{}," produces a directory containing the executable and its libraries. It starts much\nfaster, it is far easier to debug — you can look at what was actually included — and it ships as a\n",[30,899,900],{},"tar.gz"," or ",[30,903,904],{},"zip",", which is barely less convenient than a bare file.",[10,907,908,909,911,912,914,915,918],{},"The usual compromise: ",[30,910,80],{}," inside an archive for the download page, and ",[30,913,76],{}," only if\nyou specifically need a ",[30,916,917],{},"curl … | sh","-style install.",[19,920,922],{"id":921},"ux-considerations","UX considerations",[10,924,925],{},"Three details make a frozen tool feel finished.",[10,927,928,931,932,935],{},[42,929,930],{},"Say what version and build this is."," Frozen binaries get copied around and lose their\nprovenance, so ",[30,933,934],{},"--version"," should print more than a number:",[90,937,939],{"className":179,"code":938,"language":181,"meta":95,"style":95},"typer.echo(f\"mytool {get_version()} ({'frozen' if getattr(sys, 'frozen', False) else 'source'})\")\n",[30,940,941],{"__ignoreMap":95},[99,942,943,946,949,952,955,958,961,964,966,969,972,975,977,979,981,983,985,988,991,993,996],{"class":101,"line":102},[99,944,945],{"class":197},"typer.echo(",[99,947,948],{"class":193},"f",[99,950,951],{"class":109},"\"mytool ",[99,953,954],{"class":113},"{",[99,956,957],{"class":197},"get_version()",[99,959,960],{"class":113},"}",[99,962,963],{"class":109}," (",[99,965,954],{"class":113},[99,967,968],{"class":109},"'frozen'",[99,970,971],{"class":193}," if",[99,973,974],{"class":113}," getattr",[99,976,854],{"class":197},[99,978,968],{"class":109},[99,980,390],{"class":197},[99,982,482],{"class":113},[99,984,309],{"class":197},[99,986,987],{"class":193},"else",[99,989,990],{"class":109}," 'source'",[99,992,960],{"class":113},[99,994,995],{"class":109},")\"",[99,997,427],{"class":197},[10,999,1000,1003,1004,1009],{},[42,1001,1002],{},"Keep start-up honest."," If the extraction cost is unavoidable, make sure nothing else is added to\nit — the lazy-import discipline from\n",[1005,1006,1008],"a",{"href":1007},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","startup performance","\nmatters more in a frozen build, not less.",[10,1011,1012,1015,1016,1018],{},[42,1013,1014],{},"Expect the antivirus question."," Unsigned one-file executables are a common false-positive for\nWindows Defender and SmartScreen, because self-extracting binaries look like packers. Signing is\nthe real fix; a ",[30,1017,80],{}," build in a zip is a partial mitigation.",[19,1020,1022],{"id":1021},"testing-the-behaviour","Testing the behaviour",[10,1024,1025],{},"The build succeeding proves almost nothing. What proves the bundle works is running the paths that\nuse dynamic imports and bundled data:",[90,1027,1029],{"className":92,"code":1028,"language":94,"meta":95,"style":95},".\u002Fdist\u002Fmytool --version\n.\u002Fdist\u002Fmytool --help\n.\u002Fdist\u002Fmytool report --period 7d       # a lazily-loaded command\n.\u002Fdist\u002Fmytool render --template report # reads a bundled data file\nenv -i .\u002Fdist\u002Fmytool --version         # empty environment: nothing leaks in from outside\n",[30,1030,1031,1037,1044,1060,1075],{"__ignoreMap":95},[99,1032,1033,1035],{"class":101,"line":102},[99,1034,159],{"class":105},[99,1036,162],{"class":113},[99,1038,1039,1041],{"class":101,"line":156},[99,1040,159],{"class":105},[99,1042,1043],{"class":113}," --help\n",[99,1045,1046,1048,1051,1054,1057],{"class":101,"line":207},[99,1047,159],{"class":105},[99,1049,1050],{"class":109}," report",[99,1052,1053],{"class":113}," --period",[99,1055,1056],{"class":109}," 7d",[99,1058,1059],{"class":120},"       # a lazily-loaded command\n",[99,1061,1062,1064,1067,1070,1072],{"class":101,"line":214},[99,1063,159],{"class":105},[99,1065,1066],{"class":109}," render",[99,1068,1069],{"class":113}," --template",[99,1071,1050],{"class":109},[99,1073,1074],{"class":120}," # reads a bundled data file\n",[99,1076,1077,1080,1083,1086,1089],{"class":101,"line":232},[99,1078,1079],{"class":105},"env",[99,1081,1082],{"class":113}," -i",[99,1084,1085],{"class":109}," .\u002Fdist\u002Fmytool",[99,1087,1088],{"class":113}," --version",[99,1090,1091],{"class":120},"         # empty environment: nothing leaks in from outside\n",[10,1093,1094],{},"Run those in the release job, on the platform that produced the artifact, on a runner that has\nnever had your project installed. A job with your package importable will happily satisfy a\nmissing hidden import from the environment and tell you nothing.",[19,1096,1098],{"id":1097},"conclusion","Conclusion",[10,1100,1101,1102,1104],{},"PyInstaller is reliable once you accept what it cannot infer. Commit a spec file, derive the hidden\nimports from the same registry your code uses, read bundled data through ",[30,1103,67],{},", and\nverify the artifact by running the interesting commands rather than by checking that the build\nexited zero.",[19,1106,1108],{"id":1107},"frequently-asked-questions","Frequently asked questions",[1110,1111,1113],"h3",{"id":1112},"why-does-my-frozen-build-fail-with-modulenotfounderror","Why does my frozen build fail with ModuleNotFoundError?",[10,1115,1116,1117,1119,1120,1122],{},"Something is imported by name rather than by an ",[30,1118,201],{}," statement — a lazily loaded command, a\nplugin, an optional backend. Add it to ",[30,1121,53],{},", or better, derive the list from the registry\nthat drives the dynamic import so the two cannot drift.",[1110,1124,1126],{"id":1125},"can-i-build-a-windows-binary-on-linux","Can I build a Windows binary on Linux?",[10,1128,1129],{},"No. PyInstaller bundles the platform's interpreter and libraries, so each target needs a build on\nthat platform. A CI matrix is the standard answer; Wine is occasionally suggested and is not worth\nthe maintenance.",[1110,1131,1133],{"id":1132},"how-do-i-make-the-binary-smaller","How do I make the binary smaller?",[10,1135,1136,1137,1139,1140,573,1142,1145,1146,1148],{},"Freeze from a minimal environment, populate ",[30,1138,506],{}," with things you never use (",[30,1141,510],{},[30,1143,1144],{},"test",", notebook machinery), and check what got pulled in with ",[30,1147,80],{}," before compressing. UPX\ncompression shrinks the file further but slows start-up and increases antivirus false positives —\nusually a bad trade for a CLI.",[1110,1150,1152],{"id":1151},"does-pyinstaller-work-with-typer-and-rich","Does PyInstaller work with Typer and Rich?",[10,1154,1155,1156,1158],{},"Yes, both freeze without special handling. The one thing to check is any dynamic theme or lexer\nlookup — Pygments styles, for instance, are resolved by name, which is exactly the pattern that\nneeds a ",[30,1157,53],{}," entry.",[1110,1160,1162],{"id":1161},"should-the-spec-file-be-committed","Should the spec file be committed?",[10,1164,1165],{},"Yes. It is the build configuration, it is a Python file so it belongs in review, and it is the only\nreadable way to express hidden imports, bundled data and exclusions. Repeating a dozen command-line\nflags in a CI workflow is how build configuration diverges between machines.",[1110,1167,1169],{"id":1168},"how-do-i-debug-a-frozen-build-that-fails-on-a-users-machine","How do I debug a frozen build that fails on a user's machine?",[10,1171,1172,1173,1175,1176,1179,1180,1183],{},"Build a ",[30,1174,80],{}," version of the same commit and reproduce there first: you can inspect the\ncollected files, and tracebacks reference real paths. Then run the failing command with\nPyInstaller's debug bootloader (",[30,1177,1178],{},"--debug=imports","), which prints every import as it resolves and\nnames the one that is missing. Keeping a ",[30,1181,1182],{},"doctor"," command that prints the bundle root, the\nresolved version and the loaded command list turns most user reports into a single paste.",[1110,1185,1187],{"id":1186},"does-freezing-protect-my-source-code","Does freezing protect my source code?",[10,1189,1190],{},"No. The bundle contains compiled bytecode, which is straightforward to extract and decompile.\nFreezing is a distribution mechanism, not an obfuscation one — if source confidentiality matters,\nthat is a different problem and one Python solves badly.",[1110,1192,1194],{"id":1193},"can-i-bundle-a-tool-that-shells-out-to-another-program","Can I bundle a tool that shells out to another program?",[10,1196,1197,1198,1201,1202,1205],{},"Yes, but decide whether that program travels with you. If it does, add it to ",[30,1199,1200],{},"binaries"," in the spec\nand resolve its path through the bundle root at run time; if it does not, look it up on ",[30,1203,1204],{},"PATH"," and\nfail with a message naming what is missing. What causes support tickets is the middle case, where\nthe tool silently uses whatever version happens to be installed.",[19,1207,1209],{"id":1208},"related","Related",[24,1211,1212,1219,1226,1232,1239],{},[27,1213,1214,1215],{},"Up: ",[1005,1216,1218],{"href":1217},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002F","Distributing CLIs as standalone binaries",[27,1220,1221,1222],{},"Sideways: ",[1005,1223,1225],{"href":1224},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv\u002F","Shipping a CLI as a zipapp with shiv",[27,1227,1221,1228],{},[1005,1229,1231],{"href":1230},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci\u002F","Building cross-platform release binaries in CI",[27,1233,1234,1235],{},"Related: ",[1005,1236,1238],{"href":1237},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F","Building wheels and sdists for Python CLIs",[27,1240,1234,1241],{},[1005,1242,1244],{"href":1243},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002F","Plugin architectures for extensible CLIs",[1246,1247,1248],"style",{},"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 pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":95,"searchDepth":156,"depth":156,"links":1250},[1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1270],{"id":21,"depth":156,"text":22},{"id":84,"depth":156,"text":85},{"id":127,"depth":156,"text":128},{"id":514,"depth":156,"text":515},{"id":694,"depth":156,"text":695},{"id":875,"depth":156,"text":876},{"id":921,"depth":156,"text":922},{"id":1021,"depth":156,"text":1022},{"id":1097,"depth":156,"text":1098},{"id":1107,"depth":156,"text":1108,"children":1261},[1262,1263,1264,1265,1266,1267,1268,1269],{"id":1112,"depth":207,"text":1113},{"id":1125,"depth":207,"text":1126},{"id":1132,"depth":207,"text":1133},{"id":1151,"depth":207,"text":1152},{"id":1161,"depth":207,"text":1162},{"id":1168,"depth":207,"text":1169},{"id":1186,"depth":207,"text":1187},{"id":1193,"depth":207,"text":1194},{"id":1208,"depth":156,"text":1209},"2026-08-01","Freeze a Python CLI into a standalone executable - spec files, hidden imports, bundled data, resource paths, and the start-up cost of one-file builds.","advanced",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller",{"title":5,"description":1272},"project-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002Findex",[138,1281,1282,1283,1200],"packaging","distribution","freezing","Ch5iQ7b3PO6K4YDSofTpXsEHdgjklePi0Pzb99zMzsI",[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,1373,1376,1379,1381,1384,1387,1390,1393,1396,1399,1402,1405,1408,1411,1414,1417,1420,1423,1426,1429,1432,1435,1438,1441,1444,1447,1450,1453,1456,1459,1462,1465,1468,1469,1472,1475,1478,1481,1484,1487,1490,1493,1496,1499,1502,1505,1508,1511,1514,1517,1520,1523,1526,1529],{"path":1287,"title":1288},"\u002Fabout","About Python CLI Toolcraft",{"path":1290,"title":1291},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1293,"title":1294},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1296,"title":1297},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1299,"title":1300},"\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":1302,"title":1303},"\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":1305,"title":1306},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1308,"title":1309},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1311,"title":1312},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1314,"title":1315},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1317,"title":1318},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1320,"title":1321},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1323,"title":1324},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1326,"title":1327},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1329,"title":1330},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1332,"title":1333},"\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":1335,"title":1336},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1338,"title":1339},"\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":1341,"title":1342},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1344,"title":1345},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1347,"title":1348},"\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":1350,"title":1351},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1353,"title":1354},"\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":1356,"title":1357},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1359,"title":1360},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1362,"title":1363},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1365,"title":1366},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1368,"title":1369},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1371,"title":1372},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1374,"title":1375},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1377,"title":1378},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":733,"title":1380},"Python CLI Toolcraft",{"path":1382,"title":1383},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1385,"title":1386},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1388,"title":1389},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1391,"title":1392},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1394,"title":1395},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1397,"title":1398},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1400,"title":1401},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1403,"title":1404},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1406,"title":1407},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1409,"title":1410},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1412,"title":1413},"\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":1415,"title":1416},"\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":1418,"title":1419},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1421,"title":1422},"\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":1424,"title":1425},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1427,"title":1428},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1430,"title":1431},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1433,"title":1434},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1436,"title":1437},"\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":1439,"title":1440},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1442,"title":1443},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1445,"title":1446},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1448,"title":1449},"\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":1451,"title":1452},"\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":1454,"title":1455},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1457,"title":1458},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1460,"title":1461},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1463,"title":1464},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1466,"title":1467},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1277,"title":5},{"path":1470,"title":1471},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1473,"title":1474},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1476,"title":1477},"\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":1479,"title":1480},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1482,"title":1483},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1485,"title":1486},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1488,"title":1489},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1491,"title":1492},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1494,"title":1495},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1497,"title":1498},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1500,"title":1501},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1503,"title":1504},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1506,"title":1507},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1509,"title":1510},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1512,"title":1513},"\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":1515,"title":1516},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1518,"title":1519},"\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":1521,"title":1522},"\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":1524,"title":1525},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1527,"title":1528},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1530,"title":1531},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690034]