[{"data":1,"prerenderedAt":1440},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002F":3,"content-directory":1195},{"id":4,"title":5,"body":6,"date":1180,"description":1181,"difficulty":1182,"draft":1183,"extension":1184,"meta":1185,"navigation":403,"path":1186,"seo":1187,"stem":1188,"tags":1189,"updated":1180,"__hash__":1194},"content\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Findex.md","Distributing CLIs as Standalone Binaries",{"type":7,"value":8,"toc":1154},"minimark",[9,17,22,62,66,70,73,76,82,85,92,96,99,159,162,165,172,176,179,217,220,233,320,327,334,338,341,348,352,355,358,361,365,368,380,484,494,587,601,692,701,705,708,766,769,772,793,796,800,803,814,824,840,843,847,850,923,934,941,945,948,954,964,970,973,977,1007,1011,1031,1035,1040,1048,1052,1059,1063,1066,1070,1073,1077,1088,1092,1095,1099,1106,1110,1113,1117,1150],[10,11,12,16],"p",{},[13,14,15],"code",{},"pipx install mytool"," is the right answer for almost every Python CLI, and it stops being the\nright answer at exactly one point: when your users do not have Python, cannot install it, or\nshould not have to think about it. Ops engineers pulling a binary onto a locked-down host,\ndesigners running a build tool, a CI image that should stay small — those are the cases this\nsection is about.",[18,19,21],"h2",{"id":20},"tldr","TL;DR",[23,24,25,37,43,49,56],"ul",{},[26,27,28,29,36],"li",{},"Try hard to stay on ",[30,31,32,33],"strong",{},"pipx or ",[13,34,35],{},"uv tool install","; every route below is a permanent maintenance\ncommitment.",[26,38,39,42],{},[30,40,41],{},"Zipapps"," (shiv) are one file and still need a Python on the machine.",[26,44,45,48],{},[30,46,47],{},"Frozen binaries"," (PyInstaller) bundle the interpreter: no Python required, at the cost of\nsize and start-up time.",[26,50,51,52,55],{},"Binaries ",[30,53,54],{},"cannot be cross-compiled"," — build each one on the platform it targets, in CI.",[26,57,58,61],{},[30,59,60],{},"Publish checksums",", and sign anything users download directly on macOS or Windows.",[63,64],"inline-diagram",{"name":65},"binary-routes-map",[18,67,69],{"id":68},"choosing-a-route","Choosing a route",[63,71],{"name":72},"binary-tradeoff-matrix",[10,74,75],{},"The honest ordering is: pipx, then a package manager the audience already uses, then a zipapp,\nthen freezing. Each step down that list adds artifacts to build, verify, sign and publish on every\nrelease — and adds a class of bug that only appears in the packaged form.",[10,77,78,79,81],{},"A useful test for whether you need to go further than pipx: can you write install instructions\nthat a target user would follow without asking a question? \"Install pipx, then run ",[13,80,15],{},"\" is fine for developers and a barrier for everyone else. If your audience is the second\ngroup, read on.",[63,83],{"name":84},"binary-size-bars",[10,86,87,88,91],{},"Size is the visible cost; start-up is the one people feel. A frozen one-file build re-extracts its\nbundle on every invocation, which can add hundreds of milliseconds to a tool that was previously\ninstant — and that is charged on ",[13,89,90],{},"--help"," and on every tab completion.",[18,93,95],{"id":94},"zipapps-one-file-still-python","Zipapps: one file, still Python",[10,97,98],{},"A zipapp is a zip archive of your application and its dependencies with a shebang line prepended,\nso the operating system runs it with the interpreter named there.",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-bash shiki shiki-themes github-light github-dark","pip install shiv\nshiv -c mytool -o dist\u002Fmytool.pyz .        # -c names the console script to run\n.\u002Fdist\u002Fmytool.pyz --help\n","bash","",[13,107,108,124,150],{"__ignoreMap":105},[109,110,113,117,121],"span",{"class":111,"line":112},"line",1,[109,114,116],{"class":115},"sScJk","pip",[109,118,120],{"class":119},"sZZnC"," install",[109,122,123],{"class":119}," shiv\n",[109,125,127,130,134,137,140,143,146],{"class":111,"line":126},2,[109,128,129],{"class":115},"shiv",[109,131,133],{"class":132},"sj4cs"," -c",[109,135,136],{"class":119}," mytool",[109,138,139],{"class":132}," -o",[109,141,142],{"class":119}," dist\u002Fmytool.pyz",[109,144,145],{"class":119}," .",[109,147,149],{"class":148},"sJ8bj","        # -c names the console script to run\n",[109,151,153,156],{"class":111,"line":152},3,[109,154,155],{"class":115},".\u002Fdist\u002Fmytool.pyz",[109,157,158],{"class":132}," --help\n",[10,160,161],{},"One file, no installation, and it works anywhere a compatible Python exists. The catch is the\nfirst run: shiv unpacks the dependency tree into a cache directory before it can import anything.",[10,163,164],{},"Subsequent runs are fast, but that first invocation can take several seconds for a large\ndependency tree, and it happens at exactly the wrong moment — when someone is trying the tool for\nthe first time. Warm the cache in your installer if you have one, or say so in the output.",[10,166,167,168,171],{},"The standard library's own ",[13,169,170],{},"zipapp"," module produces something similar without the caching layer,\nwhich is fine for a tool whose dependencies are small or absent.",[18,173,175],{"id":174},"frozen-binaries-no-python-required","Frozen binaries: no Python required",[10,177,178],{},"PyInstaller (and Nuitka, and PyOxidizer) go further: they bundle the interpreter itself, so the\nresult runs on a machine with no Python at all.",[100,180,182],{"className":102,"code":181,"language":104,"meta":105,"style":105},"pip install pyinstaller\npyinstaller --onefile --name mytool src\u002Fmytool\u002F__main__.py\n.\u002Fdist\u002Fmytool --version\n",[13,183,184,193,209],{"__ignoreMap":105},[109,185,186,188,190],{"class":111,"line":112},[109,187,116],{"class":115},[109,189,120],{"class":119},[109,191,192],{"class":119}," pyinstaller\n",[109,194,195,198,201,204,206],{"class":111,"line":126},[109,196,197],{"class":115},"pyinstaller",[109,199,200],{"class":132}," --onefile",[109,202,203],{"class":132}," --name",[109,205,136],{"class":119},[109,207,208],{"class":119}," src\u002Fmytool\u002F__main__.py\n",[109,210,211,214],{"class":111,"line":152},[109,212,213],{"class":115},".\u002Fdist\u002Fmytool",[109,215,216],{"class":132}," --version\n",[10,218,219],{},"The mechanism that makes this work — static analysis of your imports — is also its main\nlimitation. Anything imported dynamically is invisible to it.",[10,221,222,223,228,229,232],{},"That includes the entry-point plugin discovery described in\n",[224,225,227],"a",{"href":226},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002F","plugin architectures",",\nanything loaded with ",[13,230,231],{},"importlib.import_module",", and the lazy command loading that keeps startup\nfast. Each has to be declared:",[100,234,238],{"className":235,"code":236,"language":237,"meta":105,"style":105},"language-python shiki shiki-themes github-light github-dark","# mytool.spec\na = Analysis(\n    [\"src\u002Fmytool\u002F__main__.py\"],\n    hiddenimports=[\"mytool.commands.sync\", \"mytool.commands.report\"],\n    datas=[(\"src\u002Fmytool\u002Ftemplates\", \"mytool\u002Ftemplates\")],\n)\n","python",[13,239,240,245,258,269,292,314],{"__ignoreMap":105},[109,241,242],{"class":111,"line":112},[109,243,244],{"class":148},"# mytool.spec\n",[109,246,247,251,255],{"class":111,"line":126},[109,248,250],{"class":249},"sVt8B","a ",[109,252,254],{"class":253},"szBVR","=",[109,256,257],{"class":249}," Analysis(\n",[109,259,260,263,266],{"class":111,"line":152},[109,261,262],{"class":249},"    [",[109,264,265],{"class":119},"\"src\u002Fmytool\u002F__main__.py\"",[109,267,268],{"class":249},"],\n",[109,270,272,276,278,281,284,287,290],{"class":111,"line":271},4,[109,273,275],{"class":274},"s4XuR","    hiddenimports",[109,277,254],{"class":253},[109,279,280],{"class":249},"[",[109,282,283],{"class":119},"\"mytool.commands.sync\"",[109,285,286],{"class":249},", ",[109,288,289],{"class":119},"\"mytool.commands.report\"",[109,291,268],{"class":249},[109,293,295,298,300,303,306,308,311],{"class":111,"line":294},5,[109,296,297],{"class":274},"    datas",[109,299,254],{"class":253},[109,301,302],{"class":249},"[(",[109,304,305],{"class":119},"\"src\u002Fmytool\u002Ftemplates\"",[109,307,286],{"class":249},[109,309,310],{"class":119},"\"mytool\u002Ftemplates\"",[109,312,313],{"class":249},")],\n",[109,315,317],{"class":111,"line":316},6,[109,318,319],{"class":249},")\n",[10,321,322,323,326],{},"The second gotcha is paths. Inside a frozen build your package has been extracted to a temporary\ndirectory, so anything computed from ",[13,324,325],{},"__file__"," points somewhere that did not exist a second ago.",[10,328,329,330,333],{},"Read bundled files with ",[13,331,332],{},"importlib.resources"," and the problem disappears — which is a good reason\nto use it from the start, long before anyone mentions freezing.",[18,335,337],{"id":336},"building-for-every-platform","Building for every platform",[10,339,340],{},"There is no cross-compilation. A macOS binary is built on macOS, a Windows binary on Windows, and\na Linux binary on a distribution old enough that its libc works everywhere you care about.",[10,342,343,344,347],{},"The pipeline shape that works: one job builds the wheel, a matrix of jobs each produce and\n",[30,345,346],{},"verify"," a binary on their own platform, and a final job attaches everything to the release with\nchecksums. Verification inside the matrix is the part people skip, and it is what stops a broken\nmacOS artifact shipping alongside three good ones.",[18,349,351],{"id":350},"package-managers-users-already-have","Package managers users already have",[10,353,354],{},"For developer tools, a Homebrew tap or a Scoop bucket is often a better investment than a raw\ndownload link: the manager handles trust, upgrades and uninstalls, and users already know the\ncommands.",[63,356],{"name":357},"package-manager-matrix",[10,359,360],{},"The maintenance question is the important one. Every channel is a step in your release process\nforever, and a tap that lags two versions behind is worse than no tap at all. Automate the version\nand hash bump from the release workflow, or do not offer the channel.",[18,362,364],{"id":363},"preparing-a-codebase-for-bundling","Preparing a codebase for bundling",[10,366,367],{},"Most of the pain in freezing a Python application comes from three habits that are harmless in a\nnormal install and fatal in a bundle. Fixing them costs nothing and pays off long before you\nfreeze anything.",[10,369,370,376,377,379],{},[30,371,372,373,375],{},"Read data through ",[13,374,332],{},"."," Anything computed from ",[13,378,325],{}," assumes your source\ntree exists on disk in the layout you wrote it in, which stops being true inside a bundle, a\nzipapp or a wheel installed as a zip:",[100,381,383],{"className":235,"code":382,"language":237,"meta":105,"style":105},"from importlib.resources import files\n\n# works installed, frozen, zipped\ntemplate = files(\"mytool.templates\").joinpath(\"report.html\").read_text(encoding=\"utf-8\")\n\n# works only in development\ntemplate = (Path(__file__).parent \u002F \"templates\" \u002F \"report.html\").read_text()   # avoid\n",[13,384,385,399,405,410,442,446,451],{"__ignoreMap":105},[109,386,387,390,393,396],{"class":111,"line":112},[109,388,389],{"class":253},"from",[109,391,392],{"class":249}," importlib.resources ",[109,394,395],{"class":253},"import",[109,397,398],{"class":249}," files\n",[109,400,401],{"class":111,"line":126},[109,402,404],{"emptyLinePlaceholder":403},true,"\n",[109,406,407],{"class":111,"line":152},[109,408,409],{"class":148},"# works installed, frozen, zipped\n",[109,411,412,415,417,420,423,426,429,432,435,437,440],{"class":111,"line":271},[109,413,414],{"class":249},"template ",[109,416,254],{"class":253},[109,418,419],{"class":249}," files(",[109,421,422],{"class":119},"\"mytool.templates\"",[109,424,425],{"class":249},").joinpath(",[109,427,428],{"class":119},"\"report.html\"",[109,430,431],{"class":249},").read_text(",[109,433,434],{"class":274},"encoding",[109,436,254],{"class":253},[109,438,439],{"class":119},"\"utf-8\"",[109,441,319],{"class":249},[109,443,444],{"class":111,"line":294},[109,445,404],{"emptyLinePlaceholder":403},[109,447,448],{"class":111,"line":316},[109,449,450],{"class":148},"# works only in development\n",[109,452,454,456,458,461,463,466,469,472,475,478,481],{"class":111,"line":453},7,[109,455,414],{"class":249},[109,457,254],{"class":253},[109,459,460],{"class":249}," (Path(",[109,462,325],{"class":132},[109,464,465],{"class":249},").parent ",[109,467,468],{"class":253},"\u002F",[109,470,471],{"class":119}," \"templates\"",[109,473,474],{"class":253}," \u002F",[109,476,477],{"class":119}," \"report.html\"",[109,479,480],{"class":249},").read_text()   ",[109,482,483],{"class":148},"# avoid\n",[10,485,486,489,490,493],{},[30,487,488],{},"Keep dynamic imports declarable."," Lazy command loading and entry-point plugins are both good\npatterns, and both are invisible to static analysis. Keep the registry of dotted paths in one\nmodule so that generating the ",[13,491,492],{},"hiddenimports"," list is mechanical rather than archaeological:",[100,495,497],{"className":235,"code":496,"language":237,"meta":105,"style":105},"COMMANDS = {\n    \"sync\": \"mytool.commands.sync:sync\",\n    \"report\": \"mytool.commands.report:report\",\n}\n\n# in the spec file\nhiddenimports = [target.split(\":\")[0] for target in COMMANDS.values()]\n",[13,498,499,510,524,536,541,545,550],{"__ignoreMap":105},[109,500,501,504,507],{"class":111,"line":112},[109,502,503],{"class":132},"COMMANDS",[109,505,506],{"class":253}," =",[109,508,509],{"class":249}," {\n",[109,511,512,515,518,521],{"class":111,"line":126},[109,513,514],{"class":119},"    \"sync\"",[109,516,517],{"class":249},": ",[109,519,520],{"class":119},"\"mytool.commands.sync:sync\"",[109,522,523],{"class":249},",\n",[109,525,526,529,531,534],{"class":111,"line":152},[109,527,528],{"class":119},"    \"report\"",[109,530,517],{"class":249},[109,532,533],{"class":119},"\"mytool.commands.report:report\"",[109,535,523],{"class":249},[109,537,538],{"class":111,"line":271},[109,539,540],{"class":249},"}\n",[109,542,543],{"class":111,"line":294},[109,544,404],{"emptyLinePlaceholder":403},[109,546,547],{"class":111,"line":316},[109,548,549],{"class":148},"# in the spec file\n",[109,551,552,555,557,560,563,566,569,572,575,578,581,584],{"class":111,"line":453},[109,553,554],{"class":249},"hiddenimports ",[109,556,254],{"class":253},[109,558,559],{"class":249}," [target.split(",[109,561,562],{"class":119},"\":\"",[109,564,565],{"class":249},")[",[109,567,568],{"class":132},"0",[109,570,571],{"class":249},"] ",[109,573,574],{"class":253},"for",[109,576,577],{"class":249}," target ",[109,579,580],{"class":253},"in",[109,582,583],{"class":132}," COMMANDS",[109,585,586],{"class":249},".values()]\n",[10,588,589,592,593,596,597,600],{},[30,590,591],{},"Detect the bundled case explicitly"," wherever behaviour must differ. PyInstaller sets\n",[13,594,595],{},"sys.frozen",", and the unpack directory is ",[13,598,599],{},"sys._MEIPASS",":",[100,602,604],{"className":235,"code":603,"language":237,"meta":105,"style":105},"import sys\nfrom pathlib import Path\n\ndef bundle_root() -> Path:\n    if getattr(sys, \"frozen\", False):\n        return Path(sys._MEIPASS)        # the temporary extraction directory\n    return Path(__file__).resolve().parent\n",[13,605,606,613,625,629,640,662,679],{"__ignoreMap":105},[109,607,608,610],{"class":111,"line":112},[109,609,395],{"class":253},[109,611,612],{"class":249}," sys\n",[109,614,615,617,620,622],{"class":111,"line":126},[109,616,389],{"class":253},[109,618,619],{"class":249}," pathlib ",[109,621,395],{"class":253},[109,623,624],{"class":249}," Path\n",[109,626,627],{"class":111,"line":152},[109,628,404],{"emptyLinePlaceholder":403},[109,630,631,634,637],{"class":111,"line":271},[109,632,633],{"class":253},"def",[109,635,636],{"class":115}," bundle_root",[109,638,639],{"class":249},"() -> Path:\n",[109,641,642,645,648,651,654,656,659],{"class":111,"line":294},[109,643,644],{"class":253},"    if",[109,646,647],{"class":132}," getattr",[109,649,650],{"class":249},"(sys, ",[109,652,653],{"class":119},"\"frozen\"",[109,655,286],{"class":249},[109,657,658],{"class":132},"False",[109,660,661],{"class":249},"):\n",[109,663,664,667,670,673,676],{"class":111,"line":316},[109,665,666],{"class":253},"        return",[109,668,669],{"class":249}," Path(sys.",[109,671,672],{"class":132},"_MEIPASS",[109,674,675],{"class":249},")        ",[109,677,678],{"class":148},"# the temporary extraction directory\n",[109,680,681,684,687,689],{"class":111,"line":453},[109,682,683],{"class":253},"    return",[109,685,686],{"class":249}," Path(",[109,688,325],{"class":132},[109,690,691],{"class":249},").resolve().parent\n",[10,693,694,695,697,698,700],{},"Use it sparingly. Every branch keyed on ",[13,696,595],{}," is a path your ordinary tests never take, so\nthe fewer of them the better — which is another argument for ",[13,699,332],{},", since it needs\nno branch at all.",[18,702,704],{"id":703},"verifying-an-artifact-before-anyone-downloads-it","Verifying an artifact before anyone downloads it",[10,706,707],{},"A bundled build can succeed and still be broken, because the failure is a missing import that only\noccurs on a code path the build never exercised. The verification that catches this is a real\ninvocation, on a clean machine, of the paths users take.",[100,709,711],{"className":102,"code":710,"language":104,"meta":105,"style":105},"# in the release job, on the platform that produced the artifact\n.\u002Fdist\u002Fmytool --version\n.\u002Fdist\u002Fmytool --help\n.\u002Fdist\u002Fmytool sync --dry-run .\u002Ffixtures       # a command that touches the plugin machinery\necho '{\"replicas\": 2}' | .\u002Fdist\u002Fmytool apply -\n",[13,712,713,718,724,730,746],{"__ignoreMap":105},[109,714,715],{"class":111,"line":112},[109,716,717],{"class":148},"# in the release job, on the platform that produced the artifact\n",[109,719,720,722],{"class":111,"line":126},[109,721,213],{"class":115},[109,723,216],{"class":132},[109,725,726,728],{"class":111,"line":152},[109,727,213],{"class":115},[109,729,158],{"class":132},[109,731,732,734,737,740,743],{"class":111,"line":271},[109,733,213],{"class":115},[109,735,736],{"class":119}," sync",[109,738,739],{"class":132}," --dry-run",[109,741,742],{"class":119}," .\u002Ffixtures",[109,744,745],{"class":148},"       # a command that touches the plugin machinery\n",[109,747,748,751,754,757,760,763],{"class":111,"line":294},[109,749,750],{"class":132},"echo",[109,752,753],{"class":119}," '{\"replicas\": 2}'",[109,755,756],{"class":253}," |",[109,758,759],{"class":115}," .\u002Fdist\u002Fmytool",[109,761,762],{"class":119}," apply",[109,764,765],{"class":119}," -\n",[10,767,768],{},"Four invocations, a few seconds, and between them they exercise metadata reading, command\nregistration, at least one dynamic import and stdin handling. Run them on a runner that has never\nhad your project installed — a job that inherits a populated virtual environment will happily\nimport from it and tell you nothing.",[10,770,771],{},"For a frozen binary specifically, add one check that the bundle is self-contained:",[100,773,775],{"className":102,"code":774,"language":104,"meta":105,"style":105},"env -i .\u002Fdist\u002Fmytool --version        # empty environment: no PATH, no PYTHONPATH\n",[13,776,777],{"__ignoreMap":105},[109,778,779,782,785,787,790],{"class":111,"line":112},[109,780,781],{"class":115},"env",[109,783,784],{"class":132}," -i",[109,786,759],{"class":119},[109,788,789],{"class":132}," --version",[109,791,792],{"class":148},"        # empty environment: no PATH, no PYTHONPATH\n",[10,794,795],{},"If that fails, something in the bundle is still reaching outside it.",[18,797,799],{"id":798},"keeping-the-release-honest","Keeping the release honest",[10,801,802],{},"Whatever routes you offer, three properties make the difference between a distribution users trust\nand one they work around.",[10,804,805,808,809,813],{},[30,806,807],{},"One version, everywhere."," The wheel, the binary, the tap formula and the Scoop manifest should\nall report the same number, derived from the same tag. That falls out naturally if the version is\nsingle-sourced, as described in\n",[224,810,812],{"href":811},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning"," —\nand drifts immediately if any channel is updated by hand.",[10,815,816,819,820,823],{},[30,817,818],{},"Checksums published alongside every artifact."," A binary downloaded over HTTPS from a release\npage is still an opaque blob; a published ",[13,821,822],{},"checksums.txt"," is what lets a security-conscious user\nadopt it, and what lets a package manager verify what it fetched.",[10,825,826,829,830,523,833,286,836,839],{},[30,827,828],{},"A documented uninstall."," Every route needs one and they all differ: ",[13,831,832],{},"pipx uninstall mytool",[13,834,835],{},"brew uninstall mytool",[13,837,838],{},"rm ~\u002F.local\u002Fbin\u002Fmytool"," for a raw binary, and for shiv also the cache\ndirectory. Users notice tools that cannot be removed cleanly.",[10,841,842],{},"Finally, be deliberate about how many channels you take on. Each one is a step in every future\nrelease, a set of instructions to keep accurate, and a support surface. Two well-maintained routes\nbeat five that lag behind — and the two that serve most audiences are pipx for people who have\nPython and one platform package manager for the people who do not.",[18,844,846],{"id":845},"writing-install-instructions-for-several-channels","Writing install instructions for several channels",[10,848,849],{},"Once a tool has more than one route, the README becomes the place where users decide whether to\nbother. The arrangement that works is one recommended path and a short list of alternatives, in\ndescending order of how much the reader is expected to already have.",[100,851,855],{"className":852,"code":853,"language":854,"meta":105,"style":105},"language-markdown shiki shiki-themes github-light github-dark","## Install\n\n    pipx install mytool          # recommended, needs Python 3.11+\n\nOther options:\n\n    brew install you\u002Ftap\u002Fmytool  # macOS and Linux\n    scoop install mytool         # Windows\n    curl -sSL https:\u002F\u002Fexample.com\u002Fmytool\u002Flatest\u002Fmytool-linux-x86_64.tar.gz | tar xz\n\nVerify a downloaded archive against the published checksums:\n\n    sha256sum -c checksums.txt --ignore-missing\n","markdown",[13,856,857,862,866,871,875,880,884,889,895,901,906,912,917],{"__ignoreMap":105},[109,858,859],{"class":111,"line":112},[109,860,861],{},"## Install\n",[109,863,864],{"class":111,"line":126},[109,865,404],{"emptyLinePlaceholder":403},[109,867,868],{"class":111,"line":152},[109,869,870],{},"    pipx install mytool          # recommended, needs Python 3.11+\n",[109,872,873],{"class":111,"line":271},[109,874,404],{"emptyLinePlaceholder":403},[109,876,877],{"class":111,"line":294},[109,878,879],{},"Other options:\n",[109,881,882],{"class":111,"line":316},[109,883,404],{"emptyLinePlaceholder":403},[109,885,886],{"class":111,"line":453},[109,887,888],{},"    brew install you\u002Ftap\u002Fmytool  # macOS and Linux\n",[109,890,892],{"class":111,"line":891},8,[109,893,894],{},"    scoop install mytool         # Windows\n",[109,896,898],{"class":111,"line":897},9,[109,899,900],{},"    curl -sSL https:\u002F\u002Fexample.com\u002Fmytool\u002Flatest\u002Fmytool-linux-x86_64.tar.gz | tar xz\n",[109,902,904],{"class":111,"line":903},10,[109,905,404],{"emptyLinePlaceholder":403},[109,907,909],{"class":111,"line":908},11,[109,910,911],{},"Verify a downloaded archive against the published checksums:\n",[109,913,915],{"class":111,"line":914},12,[109,916,404],{"emptyLinePlaceholder":403},[109,918,920],{"class":111,"line":919},13,[109,921,922],{},"    sha256sum -c checksums.txt --ignore-missing\n",[10,924,925,926,929,930,933],{},"Three details are worth copying. The recommended route is ",[30,927,928],{},"first and named as such",", so a reader\nwho has Python does not go hunting through binary options. The direct download shows the\n",[30,931,932],{},"verification command"," immediately after, rather than in a separate security section nobody\nreads. And each line says who it is for, so nobody installs Homebrew in order to run a Python tool\non a server.",[10,935,936,937,940],{},"Keep the URLs stable across releases if you can — a ",[13,938,939],{},"latest"," path that redirects to the current\nversion means documentation and scripts do not need updating every time you ship.",[18,942,944],{"id":943},"the-cost-side-honestly","The cost side, honestly",[10,946,947],{},"It is worth being explicit about what taking on binary distribution means over a year, because the\ndecision is usually made once and paid for repeatedly.",[10,949,950,953],{},[30,951,952],{},"Every release grows."," A wheel-only release is one job. Adding four platforms means five build\njobs, four verification steps, a checksum step, a release-assets step and two package-manager\nbumps. Each is small; together they are the difference between a release taking two minutes of\nattention and twenty.",[10,955,956,959,960,963],{},[30,957,958],{},"Failures become platform-specific."," A bug report that says \"the macOS binary crashes on start\"\ncannot be reproduced on your Linux machine, and the frozen build is the least debuggable form of\nyour program. Keeping a ",[13,961,962],{},"--debug"," path that prints the resolved bundle root and the loaded plugin\nlist turns those reports from mysteries into one-line answers.",[10,965,966,969],{},[30,967,968],{},"Security expectations rise."," A binary people download is something they are trusting more\ndirectly than a package from an index with a lockfile. Checksums are the minimum; signing is\nexpected on macOS; and a supply-chain question about how the artifact was built is entirely\nreasonable, which is an argument for building only in CI, from a tag, with a workflow anyone can\nread.",[10,971,972],{},"None of that is a reason to avoid binaries when your audience needs them. It is a reason to be\nsure they do, and to automate everything the moment you commit — because the alternative is a\nrelease process that quietly depends on one person remembering four manual steps.",[18,974,976],{"id":975},"where-to-go-next","Where to go next",[23,978,979,986,993,1000],{},[26,980,981,985],{},[224,982,984],{"href":983},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002F","Bundling a Python CLI with PyInstaller","\n— spec files, hidden imports, resource paths and start-up cost.",[26,987,988,992],{},[224,989,991],{"href":990},"\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","\n— one-file distribution for machines that already have Python.",[26,994,995,999],{},[224,996,998],{"href":997},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci\u002F","Building cross-platform release binaries in CI","\n— the release matrix, verification and checksums.",[26,1001,1002,1006],{},[224,1003,1005],{"href":1004},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis\u002F","Homebrew and Scoop packaging for Python CLIs","\n— formulas, manifests and automating the bump.",[18,1008,1010],{"id":1009},"key-takeaways","Key takeaways",[23,1012,1013,1016,1019,1025,1028],{},[26,1014,1015],{},"Standalone distribution is a response to a real constraint, not an upgrade over pipx.",[26,1017,1018],{},"A zipapp removes the install step; only freezing removes the Python requirement.",[26,1020,1021,1022,1024],{},"Dynamic imports and ",[13,1023,325],{}," paths are the two things that break when a tool is bundled.",[26,1026,1027],{},"Build and verify each artifact on its own platform, in CI, on a tag.",[26,1029,1030],{},"Publish checksums, sign direct downloads, and automate any package-manager channel you offer.",[18,1032,1034],{"id":1033},"frequently-asked-questions","Frequently asked questions",[1036,1037,1039],"h3",{"id":1038},"is-a-frozen-binary-faster-than-an-installed-package","Is a frozen binary faster than an installed package?",[10,1041,1042,1043,1047],{},"No — usually slower. Freezing does not remove interpreter start-up or your imports, and a one-file\nbuild adds extraction on every run. It solves distribution to machines without Python, which is a\ndifferent problem from latency. If start-up is your concern, the\n",[224,1044,1046],{"href":1045},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","startup performance guide","\nis the right place.",[1036,1049,1051],{"id":1050},"will-a-binary-built-on-ubuntu-run-on-other-linux-distributions","Will a binary built on Ubuntu run on other Linux distributions?",[10,1053,1054,1055,1058],{},"Only if the glibc on the target is at least as new as the one it was built against. The convention\nis to build on the oldest distribution you intend to support, or inside a ",[13,1056,1057],{},"manylinux"," container.\nAlpine and other musl-based systems need their own build entirely.",[1036,1060,1062],{"id":1061},"how-large-should-i-expect-the-binary-to-be","How large should I expect the binary to be?",[10,1064,1065],{},"For a typical CLI with Typer, Rich and an HTTP client, 30–50 MB frozen. Add a scientific stack and\nit grows quickly — NumPy and pandas alone can push it past 150 MB. Excluding unused modules in the\nspec file helps at the margin; it does not change the order of magnitude.",[1036,1067,1069],{"id":1068},"can-i-ship-a-single-binary-that-works-on-every-platform","Can I ship a single binary that works on every platform?",[10,1071,1072],{},"No. Each platform needs its own artifact, which is why the release matrix exists. What you can\nshare across all of them is the wheel — every route in this section builds from the same package,\nso the code itself never forks.",[1036,1074,1076],{"id":1075},"should-i-still-publish-to-pypi-if-i-ship-binaries","Should I still publish to PyPI if I ship binaries?",[10,1078,1079,1080,1083,1084,1087],{},"Yes. PyPI is how other Python projects depend on you, how ",[13,1081,1082],{},"pipx"," and ",[13,1085,1086],{},"uv tool"," install you, and how\nanyone builds from source. Binaries are an additional channel for a specific audience, not a\nreplacement for the package.",[1036,1089,1091],{"id":1090},"what-about-nuitka-or-pyoxidizer-instead-of-pyinstaller","What about Nuitka or PyOxidizer instead of PyInstaller?",[10,1093,1094],{},"Both are credible. Nuitka compiles to C and can produce faster, smaller results at the cost of a\nlonger, more fragile build; PyOxidizer embeds the interpreter differently and has its own\nconfiguration model. PyInstaller remains the most widely used and best documented, which matters a\ngreat deal when a bundling problem needs debugging.",[1036,1096,1098],{"id":1097},"can-i-ship-a-docker-image-instead-of-a-binary","Can I ship a Docker image instead of a binary?",[10,1100,1101,1102,1105],{},"For server and CI audiences it is often the better answer: ",[13,1103,1104],{},"docker run yourorg\u002Fmytool"," needs no\nPython, no install and no platform matrix, and the image is versioned like any other artifact. It\nis a poor fit for a tool people run interactively against local files, where volume mounts and\nuser permissions turn a one-word command into a paragraph.",[1036,1107,1109],{"id":1108},"how-do-i-keep-the-frozen-build-from-breaking-silently-between-releases","How do I keep the frozen build from breaking silently between releases?",[10,1111,1112],{},"Run the same verification invocations in CI on every tag, not just when the bundling configuration\nchanges. A dependency upgrade is the usual cause of a newly missing hidden import, and it arrives\nin a release that touched nothing about packaging at all.",[18,1114,1116],{"id":1115},"related","Related",[23,1118,1119,1126,1133,1139,1145],{},[26,1120,1121,1122],{},"Up: ",[224,1123,1125],{"href":1124},"\u002Fproject-setup-dependency-management\u002F","Project Setup & Dependency Management",[26,1127,1128,1129],{},"Sideways: ",[224,1130,1132],{"href":1131},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","Packaging Python CLIs for distribution",[26,1134,1128,1135],{},[224,1136,1138],{"href":1137},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx\u002F","Installing and distributing CLIs with pipx",[26,1140,1141,1142],{},"Related: ",[224,1143,1144],{"href":811},"Managing CLI versioning and changelogs",[26,1146,1141,1147],{},[224,1148,1149],{"href":1045},"CLI startup performance and lazy loading",[1151,1152,1153],"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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":105,"searchDepth":126,"depth":126,"links":1155},[1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1179],{"id":20,"depth":126,"text":21},{"id":68,"depth":126,"text":69},{"id":94,"depth":126,"text":95},{"id":174,"depth":126,"text":175},{"id":336,"depth":126,"text":337},{"id":350,"depth":126,"text":351},{"id":363,"depth":126,"text":364},{"id":703,"depth":126,"text":704},{"id":798,"depth":126,"text":799},{"id":845,"depth":126,"text":846},{"id":943,"depth":126,"text":944},{"id":975,"depth":126,"text":976},{"id":1009,"depth":126,"text":1010},{"id":1033,"depth":126,"text":1034,"children":1170},[1171,1172,1173,1174,1175,1176,1177,1178],{"id":1038,"depth":152,"text":1039},{"id":1050,"depth":152,"text":1051},{"id":1061,"depth":152,"text":1062},{"id":1068,"depth":152,"text":1069},{"id":1075,"depth":152,"text":1076},{"id":1090,"depth":152,"text":1091},{"id":1097,"depth":152,"text":1098},{"id":1108,"depth":152,"text":1109},{"id":1115,"depth":126,"text":1116},"2026-08-01","Ship a Python CLI to users without Python - compare PyInstaller, shiv zipapps, CI release matrices, and Homebrew or Scoop packaging, with the trade-offs.","advanced",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries",{"title":5,"description":1181},"project-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Findex",[1190,1191,197,129,1192,1193],"packaging","distribution","homebrew","ci","iZi2TM2VYpbGvbJyJcWa67uCYPtRsjI6Dwcy2EkHris",[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,1291,1294,1297,1300,1303,1306,1309,1312,1315,1318,1321,1324,1327,1330,1333,1336,1339,1342,1345,1348,1351,1354,1357,1360,1363,1366,1369,1372,1375,1378,1380,1383,1384,1387,1389,1392,1395,1398,1401,1404,1407,1410,1413,1416,1419,1422,1425,1428,1431,1434,1437],{"path":1197,"title":1198},"\u002Fabout","About Python CLI Toolcraft",{"path":1200,"title":1201},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1203,"title":1204},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1206,"title":1207},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1209,"title":1210},"\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":1212,"title":1213},"\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":1215,"title":1216},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1218,"title":1219},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1221,"title":1222},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1224,"title":1225},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1227,"title":1228},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1230,"title":1231},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1233,"title":1234},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1236,"title":1237},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1239,"title":1240},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1242,"title":1243},"\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":1245,"title":1246},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1248,"title":1249},"\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":1251,"title":1252},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1254,"title":1255},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1257,"title":1258},"\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":1260,"title":1261},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1263,"title":1264},"\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":1266,"title":1267},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1269,"title":1270},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1272,"title":1273},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1275,"title":1276},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1278,"title":1279},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1281,"title":1282},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1284,"title":1285},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1287,"title":1288},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":468,"title":1290},"Python CLI Toolcraft",{"path":1292,"title":1293},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1295,"title":1296},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1298,"title":1299},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1301,"title":1302},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1304,"title":1305},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1307,"title":1308},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1310,"title":1311},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1313,"title":1314},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1316,"title":1317},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1319,"title":1320},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1322,"title":1323},"\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":1325,"title":1326},"\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":1328,"title":1329},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1331,"title":1332},"\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":1334,"title":1335},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1337,"title":1338},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1340,"title":1341},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1343,"title":1344},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1346,"title":1347},"\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":1349,"title":1350},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1352,"title":1353},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1355,"title":1356},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1358,"title":1359},"\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":1361,"title":1362},"\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":1364,"title":1365},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1367,"title":1368},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1370,"title":1371},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1373,"title":1374},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1376,"title":1377},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1379,"title":984},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller",{"path":1381,"title":1382},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1186,"title":5},{"path":1385,"title":1386},"\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":1388,"title":1125},"\u002Fproject-setup-dependency-management",{"path":1390,"title":1391},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1393,"title":1394},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1396,"title":1397},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1399,"title":1400},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1402,"title":1403},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1405,"title":1406},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1408,"title":1409},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1411,"title":1412},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1414,"title":1415},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1417,"title":1418},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1420,"title":1421},"\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":1423,"title":1424},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1426,"title":1427},"\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":1429,"title":1430},"\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":1432,"title":1433},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1435,"title":1436},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1438,"title":1439},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690034]