[{"data":1,"prerenderedAt":1012},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv\u002F":3,"content-directory":766},{"id":4,"title":5,"body":6,"date":752,"description":753,"difficulty":754,"draft":755,"extension":756,"meta":757,"navigation":515,"path":758,"seo":759,"stem":760,"tags":761,"updated":752,"__hash__":765},"content\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv\u002Findex.md","Shipping a CLI as a Zipapp with shiv",{"type":7,"value":8,"toc":729},"minimark",[9,13,18,54,58,65,97,101,105,127,174,183,230,234,237,247,250,298,304,307,345,349,356,381,388,391,395,398,415,425,434,441,445,459,476,485,489,492,559,562,568,572,579,583,588,591,595,605,609,614,618,629,633,643,647,654,658,671,675,678,682,685,689,725],[10,11,12],"p",{},"A zipapp is a zip archive of your application and its dependencies with a shebang line on the\nfront. Copy the file, mark it executable, run it. There is no install step and no virtual\nenvironment — but there is still a Python interpreter, which is exactly the trade that makes\nzipapps interesting for locked-down hosts and scratch containers.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,29,37,44,51],"ul",{},[22,23,24,28],"li",{},[25,26,27],"code",{},"shiv -c mytool -o dist\u002Fmytool.pyz ."," produces one runnable file.",[22,30,31,32,36],{},"The machine still needs a ",[33,34,35],"strong",{},"compatible Python","; the zipapp bundles your dependencies, not the\ninterpreter.",[22,38,39,40,43],{},"The ",[33,41,42],{},"first run unpacks"," into a cache directory and is slow; later runs are fast.",[22,45,46,47,50],{},"Pin ",[25,48,49],{},"--python"," to the shebang you want, and build on the oldest interpreter you support.",[22,52,53],{},"Prefer a wheel and pipx whenever the user can run an installer.",[14,55,57],{"id":56},"prerequisites","Prerequisites",[10,59,60,61,64],{},"A project that builds a wheel and declares a console script in ",[25,62,63],{},"[project.scripts]",", plus shiv:",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-bash shiki shiki-themes github-light github-dark","uv tool install shiv        # or: pipx install shiv\n","bash","",[25,73,74],{"__ignoreMap":71},[75,76,79,83,87,90,93],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"sScJk","uv",[75,84,86],{"class":85},"sZZnC"," tool",[75,88,89],{"class":85}," install",[75,91,92],{"class":85}," shiv",[75,94,96],{"class":95},"sJ8bj","        # or: pipx install shiv\n",[14,98,100],{"id":99},"how-a-zipapp-actually-works","How a zipapp actually works",[102,103],"inline-diagram",{"name":104},"zipapp-anatomy",[10,106,107,108,111,112,115,116,118,119,122,123,126],{},"Python has been able to execute a zip archive containing ",[25,109,110],{},"__main__.py"," since 3.5, and ",[25,113,114],{},"zipapp","\nprepends a shebang so the file can be executed directly. shiv builds on that: it installs your\nproject and its dependencies into a directory, zips the result, and adds a bootstrap ",[25,117,110],{},"\nthat puts the bundled ",[25,120,121],{},"site-packages"," on ",[25,124,125],{},"sys.path"," before importing your entry point.",[66,128,130],{"className":68,"code":129,"language":70,"meta":71,"style":71},"shiv -c mytool -o dist\u002Fmytool.pyz .\nchmod +x dist\u002Fmytool.pyz\n.\u002Fdist\u002Fmytool.pyz --help\n",[25,131,132,153,165],{"__ignoreMap":71},[75,133,134,137,141,144,147,150],{"class":77,"line":78},[75,135,136],{"class":81},"shiv",[75,138,140],{"class":139},"sj4cs"," -c",[75,142,143],{"class":85}," mytool",[75,145,146],{"class":139}," -o",[75,148,149],{"class":85}," dist\u002Fmytool.pyz",[75,151,152],{"class":85}," .\n",[75,154,156,159,162],{"class":77,"line":155},2,[75,157,158],{"class":81},"chmod",[75,160,161],{"class":85}," +x",[75,163,164],{"class":85}," dist\u002Fmytool.pyz\n",[75,166,168,171],{"class":77,"line":167},3,[75,169,170],{"class":81},".\u002Fdist\u002Fmytool.pyz",[75,172,173],{"class":139}," --help\n",[10,175,176,179,180,182],{},[25,177,178],{},"-c mytool"," names the console script from your ",[25,181,63],{}," table — shiv looks it up in the\ninstalled metadata, so the entry point you already ship is what runs. Building from a requirements\nfile or a published package works too:",[66,184,186],{"className":68,"code":185,"language":70,"meta":71,"style":71},"shiv -c mytool -o dist\u002Fmytool.pyz mytool==1.4.0        # from an index\nshiv -c mytool -o dist\u002Fmytool.pyz -r requirements.txt  # a pinned set\n",[25,187,188,209],{"__ignoreMap":71},[75,189,190,192,194,196,198,200,203,206],{"class":77,"line":78},[75,191,136],{"class":81},[75,193,140],{"class":139},[75,195,143],{"class":85},[75,197,146],{"class":139},[75,199,149],{"class":85},[75,201,202],{"class":85}," mytool==",[75,204,205],{"class":139},"1.4.0",[75,207,208],{"class":95},"        # from an index\n",[75,210,211,213,215,217,219,221,224,227],{"class":77,"line":155},[75,212,136],{"class":81},[75,214,140],{"class":139},[75,216,143],{"class":85},[75,218,146],{"class":139},[75,220,149],{"class":85},[75,222,223],{"class":139}," -r",[75,225,226],{"class":85}," requirements.txt",[75,228,229],{"class":95},"  # a pinned set\n",[14,231,233],{"id":232},"the-first-run-cache","The first-run cache",[102,235],{"name":236},"shiv-first-run",[10,238,239,240,242,243,246],{},"Python cannot import compiled extensions from inside a zip, so shiv extracts the bundled\n",[25,241,121],{}," into a cache directory the first time the zipapp runs — by default under\n",[25,244,245],{},"~\u002F.shiv\u002F",". That first invocation can take several seconds for a large dependency tree; every\nsubsequent one reuses the cache and starts quickly.",[10,248,249],{},"Two knobs matter in practice:",[66,251,253],{"className":68,"code":252,"language":70,"meta":71,"style":71},"SHIV_ROOT=\u002Fopt\u002Fmytool\u002Fcache .\u002Fmytool.pyz --version     # choose where the cache lives\nshiv --compressed -c mytool -o dist\u002Fmytool.pyz .       # smaller file, slightly slower unpack\n",[25,254,255,277],{"__ignoreMap":71},[75,256,257,261,265,268,271,274],{"class":77,"line":78},[75,258,260],{"class":259},"sVt8B","SHIV_ROOT",[75,262,264],{"class":263},"szBVR","=",[75,266,267],{"class":85},"\u002Fopt\u002Fmytool\u002Fcache",[75,269,270],{"class":81}," .\u002Fmytool.pyz",[75,272,273],{"class":139}," --version",[75,275,276],{"class":95},"     # choose where the cache lives\n",[75,278,279,281,284,286,288,290,292,295],{"class":77,"line":155},[75,280,136],{"class":81},[75,282,283],{"class":139}," --compressed",[75,285,140],{"class":139},[75,287,143],{"class":85},[75,289,146],{"class":139},[75,291,149],{"class":85},[75,293,294],{"class":85}," .",[75,296,297],{"class":95},"       # smaller file, slightly slower unpack\n",[10,299,300,301,303],{},"Setting ",[25,302,260],{}," explicitly is worth doing in images and on shared hosts, where a per-user home\ndirectory may be read-only or wiped between runs. A zipapp that re-extracts on every invocation\nbecause its cache directory vanished is the slowest possible version of this idea.",[10,305,306],{},"If the first-run delay is unacceptable — a health check with a two-second timeout, say — warm it\nduring installation:",[66,308,310],{"className":68,"code":309,"language":70,"meta":71,"style":71},"install -m 0755 mytool.pyz \u002Fusr\u002Flocal\u002Fbin\u002Fmytool\n\u002Fusr\u002Flocal\u002Fbin\u002Fmytool --version >\u002Fdev\u002Fnull    # unpack now, not on first real use\n",[25,311,312,329],{"__ignoreMap":71},[75,313,314,317,320,323,326],{"class":77,"line":78},[75,315,316],{"class":81},"install",[75,318,319],{"class":139}," -m",[75,321,322],{"class":139}," 0755",[75,324,325],{"class":85}," mytool.pyz",[75,327,328],{"class":85}," \u002Fusr\u002Flocal\u002Fbin\u002Fmytool\n",[75,330,331,334,336,339,342],{"class":77,"line":155},[75,332,333],{"class":81},"\u002Fusr\u002Flocal\u002Fbin\u002Fmytool",[75,335,273],{"class":139},[75,337,338],{"class":263}," >",[75,340,341],{"class":85},"\u002Fdev\u002Fnull",[75,343,344],{"class":95},"    # unpack now, not on first real use\n",[14,346,348],{"id":347},"pinning-the-interpreter","Pinning the interpreter",[10,350,351,352,355],{},"The shebang decides which Python runs your code, and the default is whatever ",[25,353,354],{},"python3"," resolves to\non the target. Be explicit:",[66,357,359],{"className":68,"code":358,"language":70,"meta":71,"style":71},"shiv -c mytool -o dist\u002Fmytool.pyz --python \"\u002Fusr\u002Fbin\u002Fenv python3.11\" .\n",[25,360,361],{"__ignoreMap":71},[75,362,363,365,367,369,371,373,376,379],{"class":77,"line":78},[75,364,136],{"class":81},[75,366,140],{"class":139},[75,368,143],{"class":85},[75,370,146],{"class":139},[75,372,149],{"class":85},[75,374,375],{"class":139}," --python",[75,377,378],{"class":85}," \"\u002Fusr\u002Fbin\u002Fenv python3.11\"",[75,380,152],{"class":85},[10,382,383,384,387],{},"Two rules keep this predictable. ",[33,385,386],{},"Build on the oldest interpreter you support",", because bytecode\nand some wheels are forward-compatible but not backward. And if your dependencies include compiled\nextensions, the wheels bundled into the zipapp are platform-specific — a zipapp built on macOS\narm64 will not run on Linux x86-64, even though the format itself is portable.",[102,389],{"name":390},"zipapp-vs-wheel-decision",[14,392,394],{"id":393},"where-zipapps-genuinely-win","Where zipapps genuinely win",[10,396,397],{},"Three situations, all with the same shape: the machine has Python but you cannot run an installer\non it.",[10,399,400,403,404,407,408,410,411,414],{},[33,401,402],{},"Locked-down hosts."," No network access to an index, no permission to create virtual environments,\nbut ",[25,405,406],{},"scp"," works and ",[25,409,354],{}," exists. A ",[25,412,413],{},".pyz"," arrives as one file.",[10,416,417,420,421,424],{},[33,418,419],{},"Ephemeral containers and CI steps."," Copying a single artifact into an image layer is cheaper and\nmore cacheable than a ",[25,422,423],{},"pip install"," that resolves dependencies on every build.",[10,426,427,430,431,433],{},[33,428,429],{},"Internal tools with no index."," Publishing to a private PyPI is real infrastructure; putting a\n",[25,432,413],{}," in object storage with a version in the filename is not.",[10,435,436,437,440],{},"Where they lose is upgrades. There is no ",[25,438,439],{},"pipx upgrade"," equivalent — the user replaces the file —\nso a zipapp is best for tools distributed by a system that already manages files, such as\nconfiguration management or an image build.",[14,442,444],{"id":443},"ux-considerations","UX considerations",[10,446,447,450,451,454,455,458],{},[33,448,449],{},"Name the file with its version."," ",[25,452,453],{},"mytool-1.4.0.pyz"," in a shared location makes it obvious what is\nrunning and lets both versions coexist during a rollout. A bare ",[25,456,457],{},"mytool.pyz"," that changes under\npeople is difficult to reason about.",[10,460,461,464,465,468,469,472,473,475],{},[33,462,463],{},"Report that it is a zipapp."," Frozen and zipped builds get copied around and lose provenance, so\ninclude the form in ",[25,466,467],{},"--version"," output. ",[25,470,471],{},"sys.argv[0]"," ends in ",[25,474,413],{}," when running this way, which\nis enough to detect.",[10,477,478,481,482,484],{},[33,479,480],{},"Document the cache."," The first-run delay and the ",[25,483,260],{}," variable belong in your install\ninstructions; without them, a user whose first invocation takes eight seconds concludes the tool\nis broken.",[14,486,488],{"id":487},"testing-the-behaviour","Testing the behaviour",[10,490,491],{},"Verify the artifact by running it somewhere your project is not installed:",[66,493,495],{"className":68,"code":494,"language":70,"meta":71,"style":71},"shiv -c mytool -o dist\u002Fmytool.pyz .\n\nenv -i PATH=\u002Fusr\u002Fbin:\u002Fbin SHIV_ROOT=\u002Ftmp\u002Fshiv .\u002Fdist\u002Fmytool.pyz --version\nenv -i PATH=\u002Fusr\u002Fbin:\u002Fbin SHIV_ROOT=\u002Ftmp\u002Fshiv .\u002Fdist\u002Fmytool.pyz report --period 7d\n",[25,496,497,511,517,537],{"__ignoreMap":71},[75,498,499,501,503,505,507,509],{"class":77,"line":78},[75,500,136],{"class":81},[75,502,140],{"class":139},[75,504,143],{"class":85},[75,506,146],{"class":139},[75,508,149],{"class":85},[75,510,152],{"class":85},[75,512,513],{"class":77,"line":155},[75,514,516],{"emptyLinePlaceholder":515},true,"\n",[75,518,519,522,525,528,531,534],{"class":77,"line":167},[75,520,521],{"class":81},"env",[75,523,524],{"class":139}," -i",[75,526,527],{"class":85}," PATH=\u002Fusr\u002Fbin:\u002Fbin",[75,529,530],{"class":85}," SHIV_ROOT=\u002Ftmp\u002Fshiv",[75,532,533],{"class":85}," .\u002Fdist\u002Fmytool.pyz",[75,535,536],{"class":139}," --version\n",[75,538,540,542,544,546,548,550,553,556],{"class":77,"line":539},4,[75,541,521],{"class":81},[75,543,524],{"class":139},[75,545,527],{"class":85},[75,547,530],{"class":85},[75,549,533],{"class":85},[75,551,552],{"class":85}," report",[75,554,555],{"class":139}," --period",[75,557,558],{"class":85}," 7d\n",[10,560,561],{},"The empty environment is the important part: it proves the zipapp is self-contained rather than\nimporting your project from the working directory. Running a lazily loaded command exercises the\ndynamic-import path, which is the one thing likely to be missing.",[10,563,564,565,567],{},"In CI, add the same steps to the release job and upload the ",[25,566,413],{}," as a release asset alongside the\nwheel.",[14,569,571],{"id":570},"conclusion","Conclusion",[10,573,574,575,578],{},"A zipapp is the smallest possible distribution that still carries your dependencies: one file, no\ninstaller, no environment. It does not remove the Python requirement — only freezing does that —\nbut for hosts that have Python and will not run ",[25,576,577],{},"pip",", it is the least machinery that works.",[14,580,582],{"id":581},"frequently-asked-questions","Frequently asked questions",[584,585,587],"h3",{"id":586},"does-a-zipapp-include-the-python-interpreter","Does a zipapp include the Python interpreter?",[10,589,590],{},"No. It bundles your code and its dependencies; the shebang points at an interpreter that must\nalready exist on the machine. If your users have no Python at all, you need a frozen binary\ninstead.",[584,592,594],{"id":593},"can-i-run-a-zipapp-on-windows","Can I run a zipapp on Windows?",[10,596,597,598,601,602,604],{},"Yes, with ",[25,599,600],{},"python mytool.pyz",", since Windows does not use shebangs. The Python launcher associates\n",[25,603,413],{}," files by default on most installations, so double-clicking often works too — but for a CLI\nthe documented form should be the explicit one.",[584,606,608],{"id":607},"what-is-the-difference-between-shiv-and-the-stdlib-zipapp-module","What is the difference between shiv and the stdlib zipapp module?",[10,610,611,613],{},[25,612,114],{}," produces the archive; it does not install dependencies for you or handle extension modules,\nso it suits a tool with no third-party requirements. shiv adds dependency installation, the\nfirst-run cache and interpreter pinning, which is what makes it practical for real projects.",[584,615,617],{"id":616},"why-is-my-zipapp-huge","Why is my zipapp huge?",[10,619,620,621,624,625,628],{},"Because it contains every dependency, including any compiled wheels. Check what went in with\n",[25,622,623],{},"unzip -l mytool.pyz","; the usual surprise is a transitive dependency pulling in a scientific stack.\n",[25,626,627],{},"--compressed"," helps the download size at a small cost in unpack time.",[584,630,632],{"id":631},"can-i-ship-several-commands-in-one-zipapp","Can I ship several commands in one zipapp?",[10,634,635,636,638,639,642],{},"Yes — the ",[25,637,413],{}," runs one console script, but that script can be your root command group with all\nits subcommands. If you genuinely need two separate executables, build two zipapps from the same\nproject, each with a different ",[25,640,641],{},"-c",".",[584,644,646],{"id":645},"how-do-i-verify-a-zipapp-is-self-contained","How do I verify a zipapp is self-contained?",[10,648,649,650,653],{},"Run it with an empty environment and a scratch cache: ",[25,651,652],{},"env -i PATH=\u002Fusr\u002Fbin:\u002Fbin SHIV_ROOT=\u002Ftmp\u002Fx .\u002Fmytool.pyz --version",". If that works from a directory that contains none of\nyour source, nothing is leaking in from the build machine. It is the same check worth running in\nCI on the artifact you are about to publish.",[584,655,657],{"id":656},"can-a-zipapp-read-files-next-to-itself","Can a zipapp read files next to itself?",[10,659,660,661,663,664,667,668,642],{},"Yes, but compute the location from ",[25,662,471],{}," rather than ",[25,665,666],{},"__file__",", which points inside the\narchive. In general prefer explicit paths supplied by the user or a documented configuration\ndirectory — a tool that expects files beside its own binary is awkward to place on a ",[25,669,670],{},"PATH",[584,672,674],{"id":673},"how-do-zipapps-interact-with-lazy-command-loading","How do zipapps interact with lazy command loading?",[10,676,677],{},"Well, and for the same reason they interact badly with freezing: nothing is analysed statically.\nshiv bundles whatever the install produced, so a module resolved by name at run time is present as\nlong as it was part of the package. That makes a zipapp a gentler distribution format for a\nplugin-friendly CLI than a frozen binary, which needs every dynamic import declared.",[584,679,681],{"id":680},"should-i-publish-the-zipapp-alongside-the-wheel-on-every-release","Should I publish the zipapp alongside the wheel on every release?",[10,683,684],{},"If the audience that needs it is real, yes — build it in the same release job, name it with the\nversion, and attach it to the release. Producing it only on request is how it drifts: the first\nperson who asks discovers that the format broke three versions ago.",[14,686,688],{"id":687},"related","Related",[19,690,691,699,706,712,719],{},[22,692,693,694],{},"Up: ",[695,696,698],"a",{"href":697},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002F","Distributing CLIs as standalone binaries",[22,700,701,702],{},"Sideways: ",[695,703,705],{"href":704},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002F","Bundling a Python CLI with PyInstaller",[22,707,701,708],{},[695,709,711],{"href":710},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis\u002F","Homebrew and Scoop packaging for Python CLIs",[22,713,714,715],{},"Related: ",[695,716,718],{"href":717},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","Best practices for Python CLI entry points",[22,720,714,721],{},[695,722,724],{"href":723},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis\u002F","Managing virtual environments for cross-platform CLIs",[726,727,728],"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 .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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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}",{"title":71,"searchDepth":155,"depth":155,"links":730},[731,732,733,734,735,736,737,738,739,740,751],{"id":16,"depth":155,"text":17},{"id":56,"depth":155,"text":57},{"id":99,"depth":155,"text":100},{"id":232,"depth":155,"text":233},{"id":347,"depth":155,"text":348},{"id":393,"depth":155,"text":394},{"id":443,"depth":155,"text":444},{"id":487,"depth":155,"text":488},{"id":570,"depth":155,"text":571},{"id":581,"depth":155,"text":582,"children":741},[742,743,744,745,746,747,748,749,750],{"id":586,"depth":167,"text":587},{"id":593,"depth":167,"text":594},{"id":607,"depth":167,"text":608},{"id":616,"depth":167,"text":617},{"id":631,"depth":167,"text":632},{"id":645,"depth":167,"text":646},{"id":656,"depth":167,"text":657},{"id":673,"depth":167,"text":674},{"id":680,"depth":167,"text":681},{"id":687,"depth":155,"text":688},"2026-08-01","Build a single-file Python CLI with shiv - how zipapps work, first-run caching, pinning the interpreter, and when a .pyz beats a pipx install.","advanced",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv",{"title":5,"description":753},"project-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv\u002Findex",[114,136,762,763,764],"packaging","distribution","deployment","Zf0f1k0ZypUiTmmxn6Xb10zBJbqValMomIRTsslqpVQ",[767,770,773,776,779,782,785,788,791,794,797,800,803,806,809,812,815,818,821,824,827,830,833,836,839,842,845,848,851,854,857,860,863,866,869,872,875,878,881,884,887,890,893,896,898,901,904,907,910,913,916,919,922,925,928,931,934,937,940,943,946,949,951,954,957,958,961,964,967,970,973,976,979,982,985,988,991,994,997,1000,1003,1006,1009],{"path":768,"title":769},"\u002Fabout","About Python CLI Toolcraft",{"path":771,"title":772},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":774,"title":775},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":777,"title":778},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":780,"title":781},"\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":783,"title":784},"\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":786,"title":787},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":789,"title":790},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":792,"title":793},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":795,"title":796},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":798,"title":799},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":801,"title":802},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":804,"title":805},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":807,"title":808},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":810,"title":811},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":813,"title":814},"\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":816,"title":817},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":819,"title":820},"\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":822,"title":823},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":825,"title":826},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":828,"title":829},"\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":831,"title":832},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":834,"title":835},"\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":837,"title":838},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":840,"title":841},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":843,"title":844},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":846,"title":847},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":849,"title":850},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":852,"title":853},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":855,"title":856},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":858,"title":859},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":861,"title":862},"\u002F","Python CLI Toolcraft",{"path":864,"title":865},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":867,"title":868},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":870,"title":871},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":873,"title":874},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":876,"title":877},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":879,"title":880},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":882,"title":883},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":885,"title":886},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":888,"title":889},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":891,"title":892},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":894,"title":895},"\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":897,"title":718},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points",{"path":899,"title":900},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":902,"title":903},"\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":905,"title":906},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":908,"title":909},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":911,"title":912},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":914,"title":915},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":917,"title":918},"\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":920,"title":921},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":923,"title":924},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":926,"title":927},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":929,"title":930},"\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":932,"title":933},"\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":935,"title":936},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":938,"title":939},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":941,"title":942},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":944,"title":945},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":947,"title":948},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":950,"title":705},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller",{"path":952,"title":953},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":955,"title":956},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":758,"title":5},{"path":959,"title":960},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":962,"title":963},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":965,"title":966},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":968,"title":969},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":971,"title":972},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":974,"title":975},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":977,"title":978},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":980,"title":981},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":983,"title":984},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":986,"title":987},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":989,"title":990},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":992,"title":993},"\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":995,"title":996},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":998,"title":999},"\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":1001,"title":1002},"\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":1004,"title":1005},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1007,"title":1008},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1010,"title":1011},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690034]