[{"data":1,"prerenderedAt":1853},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes\u002F":3,"content-directory":1009},{"id":4,"title":5,"body":6,"date":994,"description":995,"difficulty":996,"draft":997,"extension":998,"meta":999,"navigation":481,"path":1000,"seo":1001,"stem":1002,"tags":1003,"updated":994,"__hash__":1008},"content\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes\u002Findex.md","Pinning a Python CLI’s Dependencies with Hashes",{"type":7,"value":8,"toc":971},"minimark",[9,33,38,56,60,64,78,130,133,137,142,188,200,204,207,265,268,276,283,286,290,293,410,418,422,425,525,535,538,542,552,558,595,613,624,627,631,667,671,678,861,875,879,885,889,893,896,900,903,911,917,925,931,935,967],[10,11,12,13,17,18,21,22,26,27,32],"p",{},"A version pin says ",[14,15,16],"em",{},"which release"," to install. It does not say ",[14,19,20],{},"which file",". If a mirror, a caching proxy or a private index ever serves a different ",[23,24,25],"code",{},"click-8.5.0-py3-none-any.whl"," than the one you tested — through misconfiguration or compromise — a version pin installs it without complaint. A hash pin closes that gap: the installer computes the SHA-256 of every downloaded file and refuses anything that does not match the value recorded when you locked. This guide applies hash pinning to every place a Python CLI gets installed under your control — development, CI, release builds and container images — and covers the harder case of end users, who install from version ranges. It belongs to the ",[28,29,31],"a",{"href":30},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002F","supply-chain security topic",".",[34,35,37],"h2",{"id":36},"prerequisites","Prerequisites",[39,40,41,53],"ul",{},[42,43,44,45,48,49,32],"li",{},"A CLI project with a ",[23,46,47],{},"uv.lock"," (or Poetry lock file); see ",[28,50,52],{"href":51},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv\u002F","locking and syncing CLI dependencies with uv",[42,54,55],{},"uv 0.5+; Docker for the image section.",[34,57,59],{"id":58},"what-a-hash-pin-protects-against","What a hash pin protects against",[61,62],"inline-diagram",{"name":63},"hash-what",[10,65,66,67,69,70,73,74,77],{},"A lock file already records hashes. ",[23,68,47],{}," stores the SHA-256 of every wheel and sdist for every locked version, so the question is not whether you ",[14,71,72],{},"have"," hashes but whether every installer ",[14,75,76],{},"checks"," them. There are three situations:",[39,79,80,100,117],{},[42,81,82,88,89,91,92,95,96,99],{},[83,84,85],"strong",{},[23,86,87],{},"uv sync"," from ",[23,90,47],{}," verifies hashes automatically. Running it with ",[23,93,94],{},"--locked"," additionally fails if the lock file is out of date with ",[23,97,98],{},"pyproject.toml",", so CI never silently re-resolves.",[42,101,102,108,109,112,113,116],{},[83,103,104,105],{},"pip or ",[23,106,107],{},"uv pip"," with a requirements file verifies hashes only when the file contains ",[23,110,111],{},"--hash"," lines. With ",[23,114,115],{},"--require-hashes",", it also refuses any requirement that lacks one — including transitive dependencies you forgot to list.",[42,118,119,129],{},[83,120,121,122,125,126],{},"pipx, ",[23,123,124],{},"uv tool install",", ",[23,127,128],{},"pip install mytool"," — the way end users install — resolve dependency ranges fresh and check only what PyPI's metadata says. Your lock file plays no part.",[10,131,132],{},"Hash checking costs nothing measurable. The real cost is discipline: every dependency must be locked, including the ones pulled in transitively, and the lock must be updated deliberately.",[34,134,136],{"id":135},"the-recipe","The recipe",[138,139,141],"h3",{"id":140},"development-and-ci-sync-from-the-lock","Development and CI: sync from the lock",[143,144,149],"pre",{"className":145,"code":146,"language":147,"meta":148,"style":148},"language-bash shiki shiki-themes github-light github-dark","uv sync --locked                 # install exactly what uv.lock says; fail if it is stale\nuv run --locked pytest           # same guarantee for one-off commands\n","bash","",[23,150,151,172],{"__ignoreMap":148},[152,153,156,160,164,168],"span",{"class":154,"line":155},"line",1,[152,157,159],{"class":158},"sScJk","uv",[152,161,163],{"class":162},"sZZnC"," sync",[152,165,167],{"class":166},"sj4cs"," --locked",[152,169,171],{"class":170},"sJ8bj","                 # install exactly what uv.lock says; fail if it is stale\n",[152,173,175,177,180,182,185],{"class":154,"line":174},2,[152,176,159],{"class":158},[152,178,179],{"class":162}," run",[152,181,167],{"class":166},[152,183,184],{"class":162}," pytest",[152,186,187],{"class":170},"           # same guarantee for one-off commands\n",[10,189,190,192,193,195,196,199],{},[23,191,94],{}," is the important flag in CI. Without it, a ",[23,194,98],{}," change that was not followed by ",[23,197,198],{},"uv lock"," triggers a fresh resolution on the CI runner, and you test something no one reviewed. With it, the job fails with a clear message instead.",[138,201,203],{"id":202},"anywhere-pip-is-used-export-with-hashes","Anywhere pip is used: export with hashes",[10,205,206],{},"Some environments do not have uv — a minimal base image, a deployment system that only speaks pip. Export a fully hashed requirements file from the lock:",[143,208,210],{"className":145,"code":209,"language":147,"meta":148,"style":148},"uv export --frozen --no-dev --no-emit-project --format requirements.txt -o requirements.txt\npython -m pip install --require-hashes --no-deps -r requirements.txt\n",[23,211,212,240],{"__ignoreMap":148},[152,213,214,216,219,222,225,228,231,234,237],{"class":154,"line":155},[152,215,159],{"class":158},[152,217,218],{"class":162}," export",[152,220,221],{"class":166}," --frozen",[152,223,224],{"class":166}," --no-dev",[152,226,227],{"class":166}," --no-emit-project",[152,229,230],{"class":166}," --format",[152,232,233],{"class":162}," requirements.txt",[152,235,236],{"class":166}," -o",[152,238,239],{"class":162}," requirements.txt\n",[152,241,242,245,248,251,254,257,260,263],{"class":154,"line":174},[152,243,244],{"class":158},"python",[152,246,247],{"class":166}," -m",[152,249,250],{"class":162}," pip",[152,252,253],{"class":162}," install",[152,255,256],{"class":166}," --require-hashes",[152,258,259],{"class":166}," --no-deps",[152,261,262],{"class":166}," -r",[152,264,239],{"class":162},[10,266,267],{},"The exported file lists every package with all of its acceptable file hashes:",[143,269,274],{"className":270,"code":272,"language":273,"meta":148},[271],"language-text","click==8.5.0 \\\n    --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \\\n    --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34\n    # via mytool\n","text",[23,275,272],{"__ignoreMap":148},[10,277,278,279,282],{},"Each package has several hashes — one per wheel and one for the sdist — and the installer accepts any of them, so the same file works across platforms. ",[23,280,281],{},"--no-deps"," is belt and braces: the file is already complete, so pip should not resolve anything further. If the file is tampered with or a mirror serves a different artefact, the install stops:",[61,284],{"name":285},"hash-mismatch-terminal",[138,287,289],{"id":288},"installing-your-own-wheel-by-hash","Installing your own wheel by hash",[10,291,292],{},"The CLI itself should be installed with the same guarantee. Build the wheel once, record its hash, and install that exact file:",[143,294,296],{"className":145,"code":295,"language":147,"meta":148,"style":148},"uv build --wheel\nWHEEL=$(ls dist\u002Fmytool-*.whl)\necho \"mytool @ file:\u002F\u002F$PWD\u002F$WHEEL --hash=sha256:$(sha256sum \"$WHEEL\" | cut -d' ' -f1)\" >> requirements.txt\npython -m pip install --require-hashes --no-deps -r requirements.txt\n",[23,297,298,308,336,391],{"__ignoreMap":148},[152,299,300,302,305],{"class":154,"line":155},[152,301,159],{"class":158},[152,303,304],{"class":162}," build",[152,306,307],{"class":166}," --wheel\n",[152,309,310,314,318,321,324,327,330,333],{"class":154,"line":174},[152,311,313],{"class":312},"sVt8B","WHEEL",[152,315,317],{"class":316},"szBVR","=",[152,319,320],{"class":312},"$(",[152,322,323],{"class":158},"ls",[152,325,326],{"class":162}," dist\u002Fmytool-",[152,328,329],{"class":166},"*",[152,331,332],{"class":162},".whl",[152,334,335],{"class":312},")\n",[152,337,339,342,345,348,351,354,357,360,363,365,368,371,374,377,380,383,386,389],{"class":154,"line":338},3,[152,340,341],{"class":166},"echo",[152,343,344],{"class":162}," \"mytool @ file:\u002F\u002F",[152,346,347],{"class":312},"$PWD",[152,349,350],{"class":162},"\u002F",[152,352,353],{"class":312},"$WHEEL",[152,355,356],{"class":162}," --hash=sha256:$(",[152,358,359],{"class":158},"sha256sum",[152,361,362],{"class":162}," \"",[152,364,353],{"class":312},[152,366,367],{"class":162},"\" ",[152,369,370],{"class":316},"|",[152,372,373],{"class":158}," cut",[152,375,376],{"class":166}," -d",[152,378,379],{"class":162},"' ' ",[152,381,382],{"class":166},"-f1",[152,384,385],{"class":162},")\"",[152,387,388],{"class":316}," >>",[152,390,239],{"class":162},[152,392,394,396,398,400,402,404,406,408],{"class":154,"line":393},4,[152,395,244],{"class":158},[152,397,247],{"class":166},[152,399,250],{"class":162},[152,401,253],{"class":162},[152,403,256],{"class":166},[152,405,259],{"class":166},[152,407,262],{"class":166},[152,409,239],{"class":162},[10,411,412,413,417],{},"This is exactly what ",[28,414,416],{"href":415},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F","smoke-testing the built wheel in CI"," needs: the artefact you test is provably the one you publish.",[138,419,421],{"id":420},"container-images","Container images",[10,423,424],{},"A Docker image of a CLI is a frozen environment, and the ideal place for full hash pinning. A two-stage build keeps uv out of the final image:",[143,426,430],{"className":427,"code":428,"language":429,"meta":148,"style":148},"language-dockerfile shiki shiki-themes github-light github-dark","# Dockerfile\nFROM python:3.13-slim AS build\nCOPY --from=ghcr.io\u002Fastral-sh\u002Fuv:0.8 \u002Fuv \u002Fbin\u002Fuv\nWORKDIR \u002Fsrc\nCOPY pyproject.toml uv.lock .\u002F\nRUN uv export --frozen --no-dev --no-emit-project --format requirements.txt -o \u002Frequirements.txt\nCOPY . .\nRUN uv build --wheel --out-dir \u002Fdist\n\nFROM python:3.13-slim\nCOPY --from=build \u002Frequirements.txt \u002Fdist\u002F \u002Ftmp\u002Finstall\u002F\nRUN python -m pip install --no-cache-dir --require-hashes --no-deps -r \u002Ftmp\u002Finstall\u002Frequirements.txt \\\n && python -m pip install --no-cache-dir --no-deps \u002Ftmp\u002Finstall\u002F*.whl \\\n && rm -rf \u002Ftmp\u002Finstall\nUSER 65534\nENTRYPOINT [\"mytool\"]\n","dockerfile",[23,431,432,437,442,447,452,458,464,470,476,483,489,495,501,507,513,519],{"__ignoreMap":148},[152,433,434],{"class":154,"line":155},[152,435,436],{},"# Dockerfile\n",[152,438,439],{"class":154,"line":174},[152,440,441],{},"FROM python:3.13-slim AS build\n",[152,443,444],{"class":154,"line":338},[152,445,446],{},"COPY --from=ghcr.io\u002Fastral-sh\u002Fuv:0.8 \u002Fuv \u002Fbin\u002Fuv\n",[152,448,449],{"class":154,"line":393},[152,450,451],{},"WORKDIR \u002Fsrc\n",[152,453,455],{"class":154,"line":454},5,[152,456,457],{},"COPY pyproject.toml uv.lock .\u002F\n",[152,459,461],{"class":154,"line":460},6,[152,462,463],{},"RUN uv export --frozen --no-dev --no-emit-project --format requirements.txt -o \u002Frequirements.txt\n",[152,465,467],{"class":154,"line":466},7,[152,468,469],{},"COPY . .\n",[152,471,473],{"class":154,"line":472},8,[152,474,475],{},"RUN uv build --wheel --out-dir \u002Fdist\n",[152,477,479],{"class":154,"line":478},9,[152,480,482],{"emptyLinePlaceholder":481},true,"\n",[152,484,486],{"class":154,"line":485},10,[152,487,488],{},"FROM python:3.13-slim\n",[152,490,492],{"class":154,"line":491},11,[152,493,494],{},"COPY --from=build \u002Frequirements.txt \u002Fdist\u002F \u002Ftmp\u002Finstall\u002F\n",[152,496,498],{"class":154,"line":497},12,[152,499,500],{},"RUN python -m pip install --no-cache-dir --require-hashes --no-deps -r \u002Ftmp\u002Finstall\u002Frequirements.txt \\\n",[152,502,504],{"class":154,"line":503},13,[152,505,506],{}," && python -m pip install --no-cache-dir --no-deps \u002Ftmp\u002Finstall\u002F*.whl \\\n",[152,508,510],{"class":154,"line":509},14,[152,511,512],{}," && rm -rf \u002Ftmp\u002Finstall\n",[152,514,516],{"class":154,"line":515},15,[152,517,518],{},"USER 65534\n",[152,520,522],{"class":154,"line":521},16,[152,523,524],{},"ENTRYPOINT [\"mytool\"]\n",[10,526,527,528,530,531,534],{},"Every third-party package in the final image matches a hash from ",[23,529,47],{},"; the only unhashed install is your own wheel, built moments earlier in the same build. Pin the base images by digest (",[23,532,533],{},"python:3.13-slim@sha256:…",") for the same reason you pin packages. Running as an unprivileged user limits what a compromised dependency could do at runtime.",[61,536],{"name":537},"hash-docker-stages",[34,539,541],{"id":540},"end-users-constraints-and-cooldowns","End users: constraints and cooldowns",[10,543,544,545,548,549,551],{},"When someone runs ",[23,546,547],{},"pipx install mytool",", the resolver chooses dependency versions from your declared ranges at that moment. You cannot force hashes on them through package metadata — and you should not pin exact versions in ",[23,550,98],{},", which would block security fixes and cause conflicts. Instead, offer reproducibility as an option and reduce the exposure window for everyone.",[10,553,554,557],{},[83,555,556],{},"Publish a constraints file"," with each release — the hashed export from the lock, attached to the GitHub release. Users who need the exact tested set can install with it:",[143,559,561],{"className":145,"code":560,"language":147,"meta":148,"style":148},"curl -LO https:\u002F\u002Fgithub.com\u002Facme\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.6.0\u002Fconstraints.txt\nuv tool install mytool==1.6.0 --constraints constraints.txt\n",[23,562,563,574],{"__ignoreMap":148},[152,564,565,568,571],{"class":154,"line":155},[152,566,567],{"class":158},"curl",[152,569,570],{"class":166}," -LO",[152,572,573],{"class":162}," https:\u002F\u002Fgithub.com\u002Facme\u002Fmytool\u002Freleases\u002Fdownload\u002Fv1.6.0\u002Fconstraints.txt\n",[152,575,576,578,581,583,586,589,592],{"class":154,"line":174},[152,577,159],{"class":158},[152,579,580],{"class":162}," tool",[152,582,253],{"class":162},[152,584,585],{"class":162}," mytool==",[152,587,588],{"class":166},"1.6.0",[152,590,591],{"class":166}," --constraints",[152,593,594],{"class":162}," constraints.txt\n",[10,596,597,600,601,604,605,608,609,612],{},[83,598,599],{},"Recommend a cooldown."," uv's ",[23,602,603],{},"--exclude-newer"," accepts a duration such as ",[23,606,607],{},"\"7 days\""," and ignores any file uploaded more recently; pipx offers ",[23,610,611],{},"--cooldown DAYS"," on install and upgrade. Malicious releases of popular packages are usually detected and removed within hours or days, so a week's delay protects users from most of them at the cost of receiving genuine fixes a few days later. Mention it in your installation docs next to the plain command.",[10,614,615,618,619,623],{},[83,616,617],{},"Ship a locked artefact"," for users who need full control: a container image as above, or a ",[28,620,622],{"href":621},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002F","standalone binary"," that bundles the exact locked dependencies.",[61,625],{"name":626},"hash-user-options",[34,628,630],{"id":629},"ux-considerations","UX considerations",[39,632,633,645,655,661],{},[42,634,635,638,639,641,642,644],{},[83,636,637],{},"Make the CI failure obvious."," A ",[23,640,94],{}," failure should say \"run ",[23,643,198],{}," and commit the result\"; add that hint to the workflow step name or a wrapper script.",[42,646,647,650,651,654],{},[83,648,649],{},"Keep lock updates reviewable."," Upgrade with ",[23,652,653],{},"uv lock --upgrade-package NAME"," for targeted fixes, and let a bot propose broader upgrades on a schedule, so each lock diff is small enough to read.",[42,656,657,660],{},[83,658,659],{},"Document the paranoid install."," A short \"verified install\" section with the constraints and cooldown commands helps security teams adopt the tool.",[42,662,663,666],{},[83,664,665],{},"Do not hand-edit exported files."," Regenerate from the lock; a hand-edited hash is either wrong or a mistake waiting to happen.",[34,668,670],{"id":669},"testing-the-behaviour","Testing the behaviour",[10,672,673,674,677],{},"The property worth testing is that installs ",[14,675,676],{},"fail"," when they should. A small CI job tampers with one hash and asserts the install is refused:",[143,679,681],{"className":145,"code":680,"language":147,"meta":148,"style":148},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# scripts\u002Fcheck-hash-enforcement.sh\nset -euo pipefail\nuv export --frozen --no-dev --no-emit-project --format requirements.txt -o \u002Ftmp\u002Freq.txt\n# Corrupt every hash of the first package in the file.\npython - \u003C\u003C'PY'\nimport re\ntext = open(\"\u002Ftmp\u002Freq.txt\").read()\nstart = re.search(r\"^[A-Za-z0-9_.-]+==\", text, re.M).start()\nend = text.index(\"# via\", start)\nblock = re.sub(r\"sha256:[0-9a-f]{8}\", \"sha256:00000000\", text[start:end])\nopen(\"\u002Ftmp\u002Freq.txt\", \"w\").write(text[:start] + block + text[end:])\nPY\nuv venv -q \u002Ftmp\u002Fhash-check\nif VIRTUAL_ENV=\u002Ftmp\u002Fhash-check uv pip install --no-cache --require-hashes -r \u002Ftmp\u002Freq.txt; then\n  echo \"ERROR: install succeeded with corrupted hashes\" >&2\n  exit 1\nfi\necho \"hash enforcement OK\"\n",[23,682,683,688,693,704,725,730,743,748,753,758,763,768,773,778,791,827,838,847,853],{"__ignoreMap":148},[152,684,685],{"class":154,"line":155},[152,686,687],{"class":170},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[152,689,690],{"class":154,"line":174},[152,691,692],{"class":170},"# scripts\u002Fcheck-hash-enforcement.sh\n",[152,694,695,698,701],{"class":154,"line":338},[152,696,697],{"class":166},"set",[152,699,700],{"class":166}," -euo",[152,702,703],{"class":162}," pipefail\n",[152,705,706,708,710,712,714,716,718,720,722],{"class":154,"line":393},[152,707,159],{"class":158},[152,709,218],{"class":162},[152,711,221],{"class":166},[152,713,224],{"class":166},[152,715,227],{"class":166},[152,717,230],{"class":166},[152,719,233],{"class":162},[152,721,236],{"class":166},[152,723,724],{"class":162}," \u002Ftmp\u002Freq.txt\n",[152,726,727],{"class":154,"line":454},[152,728,729],{"class":170},"# Corrupt every hash of the first package in the file.\n",[152,731,732,734,737,740],{"class":154,"line":460},[152,733,244],{"class":158},[152,735,736],{"class":162}," -",[152,738,739],{"class":316}," \u003C\u003C",[152,741,742],{"class":162},"'PY'\n",[152,744,745],{"class":154,"line":466},[152,746,747],{"class":162},"import re\n",[152,749,750],{"class":154,"line":472},[152,751,752],{"class":162},"text = open(\"\u002Ftmp\u002Freq.txt\").read()\n",[152,754,755],{"class":154,"line":478},[152,756,757],{"class":162},"start = re.search(r\"^[A-Za-z0-9_.-]+==\", text, re.M).start()\n",[152,759,760],{"class":154,"line":485},[152,761,762],{"class":162},"end = text.index(\"# via\", start)\n",[152,764,765],{"class":154,"line":491},[152,766,767],{"class":162},"block = re.sub(r\"sha256:[0-9a-f]{8}\", \"sha256:00000000\", text[start:end])\n",[152,769,770],{"class":154,"line":497},[152,771,772],{"class":162},"open(\"\u002Ftmp\u002Freq.txt\", \"w\").write(text[:start] + block + text[end:])\n",[152,774,775],{"class":154,"line":503},[152,776,777],{"class":162},"PY\n",[152,779,780,782,785,788],{"class":154,"line":509},[152,781,159],{"class":158},[152,783,784],{"class":162}," venv",[152,786,787],{"class":166}," -q",[152,789,790],{"class":162}," \u002Ftmp\u002Fhash-check\n",[152,792,793,796,799,801,804,807,809,811,814,816,818,821,824],{"class":154,"line":515},[152,794,795],{"class":316},"if",[152,797,798],{"class":312}," VIRTUAL_ENV",[152,800,317],{"class":316},[152,802,803],{"class":162},"\u002Ftmp\u002Fhash-check",[152,805,806],{"class":158}," uv",[152,808,250],{"class":162},[152,810,253],{"class":162},[152,812,813],{"class":166}," --no-cache",[152,815,256],{"class":166},[152,817,262],{"class":166},[152,819,820],{"class":162}," \u002Ftmp\u002Freq.txt",[152,822,823],{"class":312},"; ",[152,825,826],{"class":316},"then\n",[152,828,829,832,835],{"class":154,"line":521},[152,830,831],{"class":166},"  echo",[152,833,834],{"class":162}," \"ERROR: install succeeded with corrupted hashes\"",[152,836,837],{"class":316}," >&2\n",[152,839,841,844],{"class":154,"line":840},17,[152,842,843],{"class":166},"  exit",[152,845,846],{"class":166}," 1\n",[152,848,850],{"class":154,"line":849},18,[152,851,852],{"class":316},"fi\n",[152,854,856,858],{"class":154,"line":855},19,[152,857,341],{"class":166},[152,859,860],{"class":162}," \"hash enforcement OK\"\n",[10,862,863,864,867,868,871,872,874],{},"Corrupting ",[14,865,866],{},"every"," hash of a package matters: each package lists several acceptable hashes (one per file), and changing only one leaves the others valid. A second check — ",[23,869,870],{},"uv sync --locked"," after editing ",[23,873,98],{}," without re-locking — should also fail; run it once by hand when setting up CI to see the message your contributors will get.",[34,876,878],{"id":877},"conclusion","Conclusion",[10,880,881,882,884],{},"Hashes turn \"the right version\" into \"the right file\". Use ",[23,883,870],{}," in development and CI, export a hashed requirements file for pip-based installs and container images, install your own wheel by hash, and test that tampering is refused. For end users, publish a constraints file, recommend a cooldown and offer locked artefacts — reproducibility as an option, without taking security fixes away from everyone else.",[34,886,888],{"id":887},"frequently-asked-questions","Frequently asked questions",[138,890,892],{"id":891},"do-hashes-slow-installs-down","Do hashes slow installs down?",[10,894,895],{},"Not noticeably. Installers hash files as they download them anyway for caching; comparing against a recorded value is free. The cost is only in maintaining the lock file.",[138,897,899],{"id":898},"why-does-each-package-have-several-hashes","Why does each package have several hashes?",[10,901,902],{},"A release can contain many files — wheels for different platforms and Python versions, plus an sdist. The lock records all of them so the same file works on Linux, macOS and Windows; the installer accepts whichever file it downloads, as long as its hash is on the list.",[138,904,906,907,910],{"id":905},"should-i-commit-the-exported-requirementstxt","Should I commit the exported ",[23,908,909],{},"requirements.txt","?",[10,912,913,914,916],{},"Usually not — generate it from ",[23,915,47],{}," when needed, so there is one source of truth. Commit it only if a downstream system requires a static file, and add a CI check that it matches a fresh export.",[138,918,920,921,924],{"id":919},"does-pip-install-require-hashes-work-with-editable-installs","Does ",[23,922,923],{},"pip install --require-hashes"," work with editable installs?",[10,926,927,928,930],{},"No; editable installs and local directories cannot be hashed. Install your own project as a built wheel by hash, as shown above, or install it separately with ",[23,929,281],{}," after the hashed dependencies.",[34,932,934],{"id":933},"related","Related",[39,936,937,943,949,955,961],{},[42,938,939,940],{},"Up: ",[28,941,942],{"href":30},"Securing the supply chain of a Python CLI",[42,944,945],{},[28,946,948],{"href":947},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit\u002F","Auditing dependencies with pip-audit",[42,950,951],{},[28,952,954],{"href":953},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases\u002F","Publishing attestations and verifying releases",[42,956,957],{},[28,958,960],{"href":959},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci\u002F","Caching uv dependencies in CI",[42,962,963],{},[28,964,966],{"href":965},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli\u002F","Pinning the Python version for a CLI",[968,969,970],"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}",{"title":148,"searchDepth":174,"depth":174,"links":972},[973,974,975,981,982,983,984,985,993],{"id":36,"depth":174,"text":37},{"id":58,"depth":174,"text":59},{"id":135,"depth":174,"text":136,"children":976},[977,978,979,980],{"id":140,"depth":338,"text":141},{"id":202,"depth":338,"text":203},{"id":288,"depth":338,"text":289},{"id":420,"depth":338,"text":421},{"id":540,"depth":174,"text":541},{"id":629,"depth":174,"text":630},{"id":669,"depth":174,"text":670},{"id":877,"depth":174,"text":878},{"id":887,"depth":174,"text":888,"children":986},[987,988,989,991],{"id":891,"depth":338,"text":892},{"id":898,"depth":338,"text":899},{"id":905,"depth":338,"text":990},"Should I commit the exported requirements.txt?",{"id":919,"depth":338,"text":992},"Does pip install --require-hashes work with editable installs?",{"id":933,"depth":174,"text":934},"2026-10-02","Make every install of a Python CLI tamper-evident: locked hashes with uv, require-hashes installs, Docker images, constraints for users, and cooldowns.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes",{"title":5,"description":995},"project-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes\u002Findex",[1004,1005,159,1006,1007],"security","hashes","docker","reproducibility","iVuXBT9mBzXBJ4aiT-jFwwkEWfW19F_LDhnd9b8Gotc",[1010,1013,1016,1019,1022,1025,1028,1031,1034,1037,1040,1043,1046,1049,1052,1055,1058,1061,1064,1067,1070,1073,1076,1079,1082,1085,1088,1091,1094,1097,1100,1103,1106,1109,1112,1115,1118,1121,1124,1127,1130,1133,1136,1139,1142,1145,1148,1151,1154,1157,1160,1163,1166,1169,1172,1175,1178,1181,1184,1187,1190,1193,1196,1199,1202,1205,1208,1211,1214,1217,1220,1223,1226,1229,1232,1235,1238,1241,1244,1247,1250,1253,1256,1259,1262,1265,1268,1271,1274,1277,1280,1283,1286,1289,1292,1295,1298,1301,1304,1307,1310,1313,1316,1319,1322,1325,1328,1331,1334,1337,1340,1343,1346,1349,1352,1355,1358,1361,1364,1367,1370,1373,1376,1379,1382,1385,1388,1391,1394,1397,1400,1403,1406,1409,1412,1415,1418,1421,1424,1427,1429,1432,1435,1438,1441,1444,1447,1450,1453,1456,1459,1462,1465,1468,1471,1474,1477,1480,1483,1486,1489,1492,1495,1498,1501,1504,1507,1510,1513,1516,1519,1522,1525,1528,1531,1534,1537,1540,1543,1546,1549,1552,1555,1558,1561,1564,1567,1570,1573,1576,1579,1582,1585,1588,1591,1594,1597,1600,1603,1606,1609,1612,1615,1618,1621,1624,1627,1630,1633,1636,1639,1642,1645,1648,1651,1654,1657,1660,1663,1666,1669,1672,1675,1678,1681,1684,1687,1690,1693,1696,1699,1702,1705,1708,1711,1714,1717,1720,1723,1726,1729,1732,1735,1738,1741,1744,1747,1750,1753,1756,1759,1762,1765,1768,1771,1774,1777,1780,1783,1786,1789,1792,1795,1798,1801,1804,1805,1808,1811,1814,1817,1820,1823,1826,1829,1832,1835,1838,1841,1844,1847,1850],{"path":1011,"title":1012},"\u002Fabout","About Python CLI Toolcraft",{"path":1014,"title":1015},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1017,"title":1018},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1020,"title":1021},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":1023,"title":1024},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1026,"title":1027},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1029,"title":1030},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":1032,"title":1033},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1035,"title":1036},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":1038,"title":1039},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":1041,"title":1042},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":1044,"title":1045},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1047,"title":1048},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":1050,"title":1051},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":1053,"title":1054},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1056,"title":1057},"\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":1059,"title":1060},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":1062,"title":1063},"\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":1065,"title":1066},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1068,"title":1069},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":1071,"title":1072},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1074,"title":1075},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1077,"title":1078},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1080,"title":1081},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1083,"title":1084},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":1086,"title":1087},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":1089,"title":1090},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1092,"title":1093},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1095,"title":1096},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":1098,"title":1099},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1101,"title":1102},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":1104,"title":1105},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":1107,"title":1108},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1110,"title":1111},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1113,"title":1114},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1116,"title":1117},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":1119,"title":1120},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":1122,"title":1123},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1125,"title":1126},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":1128,"title":1129},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1131,"title":1132},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":1134,"title":1135},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1137,"title":1138},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1140,"title":1141},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":1143,"title":1144},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":1146,"title":1147},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1149,"title":1150},"\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":1152,"title":1153},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1155,"title":1156},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":1158,"title":1159},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1161,"title":1162},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1164,"title":1165},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":1167,"title":1168},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1170,"title":1171},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1173,"title":1174},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":1176,"title":1177},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":1179,"title":1180},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":1182,"title":1183},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":1185,"title":1186},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":1188,"title":1189},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":1191,"title":1192},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":1194,"title":1195},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":1197,"title":1198},"\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":1200,"title":1201},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1203,"title":1204},"\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":1206,"title":1207},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":1209,"title":1210},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1212,"title":1213},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":1215,"title":1216},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1218,"title":1219},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1221,"title":1222},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":1224,"title":1225},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":1227,"title":1228},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1230,"title":1231},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":1233,"title":1234},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1236,"title":1237},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1239,"title":1240},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1242,"title":1243},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1245,"title":1246},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":1248,"title":1249},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":1251,"title":1252},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1254,"title":1255},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":1257,"title":1258},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1260,"title":1261},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1263,"title":1264},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":1266,"title":1267},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1269,"title":1270},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1272,"title":1273},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":1275,"title":1276},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":1278,"title":1279},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":1281,"title":1282},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1284,"title":1285},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1287,"title":1288},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1290,"title":1291},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1293,"title":1294},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":1296,"title":1297},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":1299,"title":1300},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":1302,"title":1303},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1305,"title":1306},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1308,"title":1309},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":1311,"title":1312},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1314,"title":1315},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1317,"title":1318},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1320,"title":1321},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":1323,"title":1324},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1326,"title":1327},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1329,"title":1330},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":1332,"title":1333},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":1335,"title":1336},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":1338,"title":1339},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":1341,"title":1342},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":1344,"title":1345},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":1347,"title":1348},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1350,"title":1351},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":1353,"title":1354},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1356,"title":1357},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":1359,"title":1360},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":1362,"title":1363},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":1365,"title":1366},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":1368,"title":1369},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1371,"title":1372},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1374,"title":1375},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1377,"title":1378},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1380,"title":1381},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":1383,"title":1384},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":1386,"title":1387},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1389,"title":1390},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":1392,"title":1393},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1395,"title":1396},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1398,"title":1399},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":1401,"title":1402},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":1404,"title":1405},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":1407,"title":1408},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1410,"title":1411},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1413,"title":1414},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":1416,"title":1417},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":1419,"title":1420},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":1422,"title":1423},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":1425,"title":1426},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":350,"title":1428},"Python CLI Toolcraft",{"path":1430,"title":1431},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":1433,"title":1434},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":1436,"title":1437},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":1439,"title":1440},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":1442,"title":1443},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":1445,"title":1446},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":1448,"title":1449},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":1451,"title":1452},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":1454,"title":1455},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1457,"title":1458},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1460,"title":1461},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1463,"title":1464},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1466,"title":1467},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1469,"title":1470},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1472,"title":1473},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":1475,"title":1476},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1478,"title":1479},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1481,"title":1482},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1484,"title":1485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":1487,"title":1488},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1490,"title":1491},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":1493,"title":1494},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":1496,"title":1497},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":1499,"title":1500},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":1502,"title":1503},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1505,"title":1506},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1508,"title":1509},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":1511,"title":1512},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1514,"title":1515},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1517,"title":1518},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1520,"title":1521},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1523,"title":1524},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":1526,"title":1527},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":1529,"title":1530},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1532,"title":1533},"\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":1535,"title":1536},"\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":1538,"title":1539},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1541,"title":1542},"\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":1544,"title":1545},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1547,"title":1548},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":1550,"title":1551},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":1553,"title":1554},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1556,"title":1557},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1559,"title":1560},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":1562,"title":1563},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1565,"title":1566},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1568,"title":1569},"\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":1571,"title":1572},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1574,"title":1575},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1577,"title":1578},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1580,"title":1581},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":1583,"title":1584},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1586,"title":1587},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":1589,"title":1590},"\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":1592,"title":1593},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1595,"title":1596},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":1598,"title":1599},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":1601,"title":1602},"\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":1604,"title":1605},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1607,"title":1608},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":1610,"title":1611},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1613,"title":1614},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":1616,"title":1617},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":1619,"title":1620},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":1622,"title":1623},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":1625,"title":1626},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":1628,"title":1629},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":1631,"title":1632},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":1634,"title":1635},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":1637,"title":1638},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":1640,"title":1641},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":1643,"title":1644},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1646,"title":1647},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1649,"title":1650},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":1652,"title":1653},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":1655,"title":1656},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":1658,"title":1659},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":1661,"title":1662},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1664,"title":1665},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1667,"title":1668},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":1670,"title":1671},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1673,"title":1674},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1676,"title":1677},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":1679,"title":1680},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":1682,"title":1683},"\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":1685,"title":1686},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1688,"title":1689},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":1691,"title":1692},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":1694,"title":1695},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":1697,"title":1698},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":1700,"title":1701},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":1703,"title":1704},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":1706,"title":1707},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":1709,"title":1710},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1712,"title":1713},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":1715,"title":1716},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":1718,"title":1719},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1721,"title":1722},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1724,"title":1725},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":1727,"title":1728},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":1730,"title":1731},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1733,"title":1734},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":1736,"title":1737},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":1739,"title":1740},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1742,"title":1743},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1745,"title":1746},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":1748,"title":1749},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1751,"title":1752},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":1754,"title":1755},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":1757,"title":1758},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1760,"title":1761},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":1763,"title":1764},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":1766,"title":1767},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":1769,"title":1770},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1772,"title":1773},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":1775,"title":1776},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1778,"title":1779},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":1781,"title":1782},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":1784,"title":1785},"\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":1787,"title":1788},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":1790,"title":1791},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":1793,"title":1794},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":1796,"title":1797},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":1799,"title":1800},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":1802,"title":1803},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":1000,"title":5},{"path":1806,"title":1807},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":1809,"title":1810},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":1812,"title":1813},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1815,"title":1816},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":1818,"title":1819},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":1821,"title":1822},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":1824,"title":1825},"\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":1827,"title":1828},"\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":1830,"title":1831},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":1833,"title":1834},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":1836,"title":1837},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":1839,"title":1840},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1842,"title":1843},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":1845,"title":1846},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1848,"title":1849},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":1851,"title":1852},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967543040]