[{"data":1,"prerenderedAt":2037},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002F":3,"content-directory":1193},{"id":4,"title":5,"body":6,"date":1177,"description":1178,"difficulty":1179,"draft":1180,"extension":1181,"meta":1182,"navigation":609,"path":1183,"seo":1184,"stem":1185,"tags":1186,"updated":1177,"__hash__":1192},"content\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Findex.md","Securing the Supply Chain of a Python CLI",{"type":7,"value":8,"toc":1153},"minimark",[9,13,32,36,41,87,91,94,97,133,136,140,156,185,200,204,210,268,280,283,286,290,301,323,326,340,367,371,383,386,420,423,430,434,437,474,486,490,501,505,508,834,849,860,864,871,960,963,967,1012,1016,1036,1040,1045,1051,1058,1061,1065,1074,1078,1081,1085,1088,1092,1110,1114,1149],[10,11,12],"p",{},"A command-line tool runs with everything its user has: their shell environment, their cloud credentials, their SSH keys, their source code. In CI it often runs with deployment secrets. That makes a CLI an attractive target, and the easiest way to attack it is not through its own code but through the chain of things that produce and deliver it — the dependencies it pulls in, the build that packages it, the account that publishes it and the installer that fetches it. Malicious packages uploaded under near-identical names, compromised maintainer accounts publishing a poisoned release, a CI workflow that leaks its publishing token: each of these has happened to real Python projects.",[10,14,15,16,21,22,26,27,31],{},"This topic covers the defences that are practical for a small team maintaining a CLI. It sits in the ",[17,18,20],"a",{"href":19},"\u002Fproject-setup-dependency-management\u002F","Project Setup & Dependency Management"," section because most of the work happens in the project's configuration and CI, alongside ",[17,23,25],{"href":24},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002F","CI\u002FCD pipelines for Python CLIs"," and ",[17,28,30],{"href":29},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","packaging Python CLIs for distribution",".",[33,34],"inline-diagram",{"name":35},"scs-topic-map",[37,38,40],"h2",{"id":39},"tldr","TL;DR",[42,43,44,52,63,69,75,81],"ul",{},[45,46,47,51],"li",{},[48,49,50],"strong",{},"Know what you ship."," Lock every dependency, and generate a software bill of materials (SBOM) from the lock file for each release.",[45,53,54,57,58,62],{},[48,55,56],{},"Scan for known vulnerabilities"," with ",[59,60,61],"code",{},"pip-audit"," on every pull request and on a schedule, and decide in advance how you triage findings.",[45,64,65,68],{},[48,66,67],{},"Pin with hashes where you install"," in CI and in any environment you control, so a replaced file on the index cannot slip in.",[45,70,71,74],{},[48,72,73],{},"Do not adopt brand-new releases instantly."," A short cooldown before upgrading dependencies avoids most malicious releases, which are typically caught within days.",[45,76,77,80],{},[48,78,79],{},"Publish with trusted publishing,"," never a long-lived API token, from an isolated release job — and let PyPI attach attestations users can verify.",[45,82,83,86],{},[48,84,85],{},"Give users a way to check"," what they installed: attestations for wheels, checksums and signatures for binaries.",[37,88,90],{"id":89},"where-the-risks-are","Where the risks are",[10,92,93],{},"A CLI's supply chain has four links, and each has its own failure mode.",[33,95],{"name":96},"scs-chain",[98,99,100,111,117,123],"ol",{},[45,101,102,105,106,110],{},[48,103,104],{},"Dependencies."," A package you depend on — or one of ",[107,108,109],"em",{},"its"," dependencies — has a known vulnerability, is taken over by an attacker, or is impersonated by a typo-squatted name. Most CLIs pull in 20–80 transitive packages, few of which anyone on the team has looked at.",[45,112,113,116],{},[48,114,115],{},"Build."," The wheel is built in CI. If the build job can be influenced by an untrusted pull request, or a build tool is compromised, the artefact differs from the source.",[45,118,119,122],{},[48,120,121],{},"Publish."," Whoever holds the publishing credential can release anything under your name. A long-lived PyPI token in a CI secret, a maintainer's laptop or a password manager is the single most valuable thing to steal.",[45,124,125,128,129,132],{},[48,126,127],{},"Install."," Users install from PyPI with pipx, uv or pip, which resolve your dependency ",[107,130,131],{},"ranges"," at install time — so users may get different, newer dependencies than you tested with.",[10,134,135],{},"The rest of this topic takes those links in order.",[37,137,139],{"id":138},"knowing-what-you-depend-on","Knowing what you depend on",[10,141,142,143,146,147,150,151,155],{},"You cannot secure what you cannot list. A lock file — ",[59,144,145],{},"uv.lock"," or ",[59,148,149],{},"poetry.lock"," — records the exact version and hashes of every package in the resolved environment, including transitive ones. It is the foundation for everything else: vulnerability scans read it, SBOMs are generated from it, and CI installs from it. ",[17,152,154],{"href":153},"\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"," covers how to keep it accurate.",[10,157,158,159,162,163,166,167,170,171,174,175,179,180,184],{},"Two habits keep the dependency list healthy. ",[48,160,161],{},"Review additions:"," a new dependency is a new party with commit access to your users' machines; check that the name is exactly right (typo-squats such as ",[59,164,165],{},"reqeusts"," exist), that the project is maintained, and that a standard-library or existing-dependency alternative does not already cover it. ",[48,168,169],{},"Prune regularly:"," tools such as ",[59,172,173],{},"deptry"," find declared dependencies that are no longer imported, as described in ",[17,176,178],{"href":177},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry\u002F","finding unused code and dependencies",". Every package removed is a risk removed — and, as a bonus, faster startup, as ",[17,181,183],{"href":182},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002F","reducing CLI dependency weight"," shows.",[10,186,187,188,191,192,196,197,199],{},"An ",[48,189,190],{},"SBOM"," turns that list into a standard document — CycloneDX or SPDX — that users and their security teams can feed into their own tooling. For a CLI distributed to companies, publishing one with each release answers the first question a security review asks. ",[17,193,195],{"href":194},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli\u002F","Generating an SBOM for a Python CLI"," shows how to produce one from ",[59,198,145],{}," and attach it to releases.",[37,201,203],{"id":202},"finding-known-vulnerabilities","Finding known vulnerabilities",[10,205,206,207,209],{},"Known vulnerabilities in dependencies are the most common supply-chain issue, and the cheapest to catch. ",[59,208,61],{},", maintained by the Python Packaging Authority, checks a set of requirements against the Python Packaging Advisory Database and OSV:",[211,212,217],"pre",{"className":213,"code":214,"language":215,"meta":216,"style":216},"language-bash shiki shiki-themes github-light github-dark","uv export --frozen --no-emit-project --format requirements.txt -o requirements.txt\nuvx pip-audit -r requirements.txt --disable-pip\n","bash","",[59,218,219,251],{"__ignoreMap":216},[220,221,224,228,232,236,239,242,245,248],"span",{"class":222,"line":223},"line",1,[220,225,227],{"class":226},"sScJk","uv",[220,229,231],{"class":230},"sZZnC"," export",[220,233,235],{"class":234},"sj4cs"," --frozen",[220,237,238],{"class":234}," --no-emit-project",[220,240,241],{"class":234}," --format",[220,243,244],{"class":230}," requirements.txt",[220,246,247],{"class":234}," -o",[220,249,250],{"class":230}," requirements.txt\n",[220,252,254,257,260,263,265],{"class":222,"line":253},2,[220,255,256],{"class":226},"uvx",[220,258,259],{"class":230}," pip-audit",[220,261,262],{"class":234}," -r",[220,264,244],{"class":230},[220,266,267],{"class":234}," --disable-pip\n",[10,269,270,271,274,275,279],{},"Exporting from the lock file and auditing with ",[59,272,273],{},"--disable-pip"," checks exactly what you ship, without resolving anything again. The command exits non-zero when it finds a vulnerability, which makes it a natural CI gate. ",[17,276,278],{"href":277},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit\u002F","Auditing dependencies with pip-audit"," covers running it on pull requests and on a schedule, reading its output, and the part that matters most in practice: triage.",[33,281],{"name":282},"scs-triage",[10,284,285],{},"Not every advisory affects every user of a package. A vulnerability in a web framework's request parser does not matter if your CLI only uses its template engine. Decide on a policy before the first alert: upgrade when a fix exists and the upgrade is safe, ignore with a written justification and an expiry date when the vulnerable code is unreachable, and treat anything in code that handles untrusted input — downloaded files, API responses, user-provided templates — as urgent.",[37,287,289],{"id":288},"pinning-hashes-and-cooldowns","Pinning, hashes and cooldowns",[10,291,292,293,296,297,300],{},"Version pins stop ",[107,294,295],{},"unexpected"," upgrades. Hashes go further: they stop a ",[107,298,299],{},"different file"," with the same version number from being installed. PyPI does not allow files to be replaced, but mirrors, caches and private indexes can be misconfigured or compromised, and a hash check turns any substitution into an install failure.",[10,302,303,304,307,308,310,311,57,314,317,318,322],{},"Where you control the installation — CI jobs, Docker images, a team's internal deployment — install from the lock file with hashes enforced. ",[59,305,306],{},"uv sync --locked"," does that by default from ",[59,309,145],{},"; an exported ",[59,312,313],{},"requirements.txt",[59,315,316],{},"--hash"," lines enforces it for pip. ",[17,319,321],{"href":320},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes\u002F","Pinning dependencies with hashes"," walks through both, plus building a Docker image that can only contain locked packages.",[33,324],{"name":325},"scs-pin-levels",[10,327,328,329,331,332,335,336,339],{},"End users are a harder case. A CLI published on PyPI declares dependency ",[107,330,131],{}," in ",[59,333,334],{},"pyproject.toml",", and pipx or ",[59,337,338],{},"uv tool install"," resolve them fresh at install time. That is normally good — users get security fixes without you releasing — but it also means a malicious release of a dependency reaches users the moment it is published. Two mitigations are practical:",[42,341,342,356],{},[45,343,344,347,348,351,352,355],{},[48,345,346],{},"Cooldowns."," Installers increasingly support ignoring releases newer than some age: uv's ",[59,349,350],{},"--exclude-newer"," (and its relative forms in recent versions) and pipx's ",[59,353,354],{},"--cooldown"," option. Recommend them in your install docs, and use the same idea for your own upgrades — let new dependency releases age a few days before your bot proposes them. Malicious releases are usually detected and removed quickly; a short delay avoids most of them.",[45,357,358,361,362,366],{},[48,359,360],{},"Locked distributions."," For users who need reproducibility, ship a fully pinned artefact — a ",[17,363,365],{"href":364},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002F","standalone binary",", a container image, or a published constraints file they can pass to the installer.",[37,368,370],{"id":369},"building-and-publishing-safely","Building and publishing safely",[10,372,373,374,377,378,382],{},"The publishing credential is the crown jewel. ",[48,375,376],{},"Trusted publishing"," removes it: PyPI is configured to trust a specific GitHub Actions (or GitLab, Google Cloud, ActiveState) workflow in a specific repository, and the workflow exchanges a short-lived OIDC token for a single-use upload credential at release time. There is no secret to leak. ",[17,379,381],{"href":380},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing\u002F","Publishing to PyPI with trusted publishing"," sets it up.",[10,384,385],{},"The release workflow itself needs a few guard rails:",[42,387,388,398,404,414],{},[45,389,390,393,394,397],{},[48,391,392],{},"Separate build and publish jobs."," Build the wheel and sdist in one job with no special permissions, upload them as artefacts, and publish from a second job that has ",[59,395,396],{},"id-token: write"," and nothing else. Code from the build cannot reach the publishing permission.",[45,399,400,403],{},[48,401,402],{},"Protect the release environment."," A GitHub environment with required reviewers means a tag push alone cannot publish.",[45,405,406,409,410,413],{},[48,407,408],{},"Pin your actions"," to full commit SHAs, not moving tags such as ",[59,411,412],{},"@v4",", so a compromised action release cannot change your pipeline silently.",[45,415,416,419],{},[48,417,418],{},"Never run release jobs on pull requests from forks",", and never check out untrusted code in a job that has secrets.",[33,421],{"name":422},"scs-release-jobs",[10,424,425,426,429],{},"When you publish with trusted publishing through the PyPA publish action, PyPI also stores ",[48,427,428],{},"attestations"," for each file (PEP 740): signed statements, recorded in the Sigstore transparency log, that this exact file was built by that workflow in that repository. They are what make verification possible.",[37,431,433],{"id":432},"letting-users-verify-what-they-installed","Letting users verify what they installed",[10,435,436],{},"Attestations are only useful if someone checks them. Tell users how, in your security or installation docs:",[211,438,440],{"className":213,"code":439,"language":215,"meta":216,"style":216},"uvx pypi-attestations verify pypi \\\n  --repository https:\u002F\u002Fgithub.com\u002Facme\u002Fmytool \\\n  pypi:mytool-1.6.0-py3-none-any.whl\n",[59,441,442,458,468],{"__ignoreMap":216},[220,443,444,446,449,452,455],{"class":222,"line":223},[220,445,256],{"class":226},[220,447,448],{"class":230}," pypi-attestations",[220,450,451],{"class":230}," verify",[220,453,454],{"class":230}," pypi",[220,456,457],{"class":234}," \\\n",[220,459,460,463,466],{"class":222,"line":253},[220,461,462],{"class":234},"  --repository",[220,464,465],{"class":230}," https:\u002F\u002Fgithub.com\u002Facme\u002Fmytool",[220,467,457],{"class":234},[220,469,471],{"class":222,"line":470},3,[220,472,473],{"class":230},"  pypi:mytool-1.6.0-py3-none-any.whl\n",[10,475,476,477,480,481,485],{},"The command fetches the file and its attestation from PyPI and confirms both the signature and that the publisher was the named repository. Security-conscious organisations can run it before approving a version for internal use. For binaries you publish on GitHub releases, GitHub's own artifact attestations (",[59,478,479],{},"gh attestation verify",") and published SHA-256 checksums serve the same purpose. ",[17,482,484],{"href":483},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases\u002F","Publishing attestations and verifying releases"," covers both, end to end.",[37,487,489],{"id":488},"keeping-dependencies-current","Keeping dependencies current",[10,491,492,493,495,496,500],{},"Security is not only about blocking bad releases but also adopting good ones. An automated updater — Dependabot or Renovate — that opens pull requests for lock-file updates, with your test suite and ",[59,494,61],{}," running on each, keeps the gap between \"fix released\" and \"fix shipped\" short. Configure it with a short minimum release age (both tools support one) to get the cooldown benefit, group minor updates to reduce noise, and keep security updates ungrouped so they are not delayed. ",[17,497,499],{"href":498},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags\u002F","Automating releases from git tags"," then makes shipping the result cheap.",[37,502,504],{"id":503},"a-starting-workflow","A starting workflow",[10,506,507],{},"Putting the pieces together does not take much configuration. This workflow audits the locked dependencies on every pull request that touches them, every week (new advisories appear for old versions), and on demand:",[211,509,513],{"className":510,"code":511,"language":512,"meta":216,"style":216},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fsupply-chain.yml\nname: supply-chain\non:\n  pull_request:\n    paths: [\"pyproject.toml\", \"uv.lock\"]\n  schedule:\n    - cron: \"17 6 * * 1\"          # Mondays, 06:17 UTC\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  audit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@\u003Ccommit-sha>          # v4\n      - uses: astral-sh\u002Fsetup-uv@\u003Ccommit-sha>        # v6\n      - name: Export the locked requirements\n        run: uv export --frozen --no-emit-project --format requirements.txt -o requirements.txt\n      - name: Audit\n        run: uvx pip-audit -r requirements.txt --disable-pip --desc on\n      - name: SBOM\n        if: always()\n        run: uv export --frozen --format cyclonedx1.5 -o sbom.cdx.json\n      - uses: actions\u002Fupload-artifact@\u003Ccommit-sha>   # v4\n        if: always()\n        with:\n          name: sbom\n          path: sbom.cdx.json\n","yaml",[59,514,515,521,534,542,550,571,579,596,604,611,619,630,635,643,651,662,670,687,702,714,725,737,747,759,770,780,795,804,812,823],{"__ignoreMap":216},[220,516,517],{"class":222,"line":223},[220,518,520],{"class":519},"sJ8bj","# .github\u002Fworkflows\u002Fsupply-chain.yml\n",[220,522,523,527,531],{"class":222,"line":253},[220,524,526],{"class":525},"s9eBZ","name",[220,528,530],{"class":529},"sVt8B",": ",[220,532,533],{"class":230},"supply-chain\n",[220,535,536,539],{"class":222,"line":470},[220,537,538],{"class":234},"on",[220,540,541],{"class":529},":\n",[220,543,545,548],{"class":222,"line":544},4,[220,546,547],{"class":525},"  pull_request",[220,549,541],{"class":529},[220,551,553,556,559,562,565,568],{"class":222,"line":552},5,[220,554,555],{"class":525},"    paths",[220,557,558],{"class":529},": [",[220,560,561],{"class":230},"\"pyproject.toml\"",[220,563,564],{"class":529},", ",[220,566,567],{"class":230},"\"uv.lock\"",[220,569,570],{"class":529},"]\n",[220,572,574,577],{"class":222,"line":573},6,[220,575,576],{"class":525},"  schedule",[220,578,541],{"class":529},[220,580,582,585,588,590,593],{"class":222,"line":581},7,[220,583,584],{"class":529},"    - ",[220,586,587],{"class":525},"cron",[220,589,530],{"class":529},[220,591,592],{"class":230},"\"17 6 * * 1\"",[220,594,595],{"class":519},"          # Mondays, 06:17 UTC\n",[220,597,599,602],{"class":222,"line":598},8,[220,600,601],{"class":525},"  workflow_dispatch",[220,603,541],{"class":529},[220,605,607],{"class":222,"line":606},9,[220,608,610],{"emptyLinePlaceholder":609},true,"\n",[220,612,614,617],{"class":222,"line":613},10,[220,615,616],{"class":525},"permissions",[220,618,541],{"class":529},[220,620,622,625,627],{"class":222,"line":621},11,[220,623,624],{"class":525},"  contents",[220,626,530],{"class":529},[220,628,629],{"class":230},"read\n",[220,631,633],{"class":222,"line":632},12,[220,634,610],{"emptyLinePlaceholder":609},[220,636,638,641],{"class":222,"line":637},13,[220,639,640],{"class":525},"jobs",[220,642,541],{"class":529},[220,644,646,649],{"class":222,"line":645},14,[220,647,648],{"class":525},"  audit",[220,650,541],{"class":529},[220,652,654,657,659],{"class":222,"line":653},15,[220,655,656],{"class":525},"    runs-on",[220,658,530],{"class":529},[220,660,661],{"class":230},"ubuntu-latest\n",[220,663,665,668],{"class":222,"line":664},16,[220,666,667],{"class":525},"    steps",[220,669,541],{"class":529},[220,671,673,676,679,681,684],{"class":222,"line":672},17,[220,674,675],{"class":529},"      - ",[220,677,678],{"class":525},"uses",[220,680,530],{"class":529},[220,682,683],{"class":230},"actions\u002Fcheckout@\u003Ccommit-sha>",[220,685,686],{"class":519},"          # v4\n",[220,688,690,692,694,696,699],{"class":222,"line":689},18,[220,691,675],{"class":529},[220,693,678],{"class":525},[220,695,530],{"class":529},[220,697,698],{"class":230},"astral-sh\u002Fsetup-uv@\u003Ccommit-sha>",[220,700,701],{"class":519},"        # v6\n",[220,703,705,707,709,711],{"class":222,"line":704},19,[220,706,675],{"class":529},[220,708,526],{"class":525},[220,710,530],{"class":529},[220,712,713],{"class":230},"Export the locked requirements\n",[220,715,717,720,722],{"class":222,"line":716},20,[220,718,719],{"class":525},"        run",[220,721,530],{"class":529},[220,723,724],{"class":230},"uv export --frozen --no-emit-project --format requirements.txt -o requirements.txt\n",[220,726,728,730,732,734],{"class":222,"line":727},21,[220,729,675],{"class":529},[220,731,526],{"class":525},[220,733,530],{"class":529},[220,735,736],{"class":230},"Audit\n",[220,738,740,742,744],{"class":222,"line":739},22,[220,741,719],{"class":525},[220,743,530],{"class":529},[220,745,746],{"class":230},"uvx pip-audit -r requirements.txt --disable-pip --desc on\n",[220,748,750,752,754,756],{"class":222,"line":749},23,[220,751,675],{"class":529},[220,753,526],{"class":525},[220,755,530],{"class":529},[220,757,758],{"class":230},"SBOM\n",[220,760,762,765,767],{"class":222,"line":761},24,[220,763,764],{"class":525},"        if",[220,766,530],{"class":529},[220,768,769],{"class":230},"always()\n",[220,771,773,775,777],{"class":222,"line":772},25,[220,774,719],{"class":525},[220,776,530],{"class":529},[220,778,779],{"class":230},"uv export --frozen --format cyclonedx1.5 -o sbom.cdx.json\n",[220,781,783,785,787,789,792],{"class":222,"line":782},26,[220,784,675],{"class":529},[220,786,678],{"class":525},[220,788,530],{"class":529},[220,790,791],{"class":230},"actions\u002Fupload-artifact@\u003Ccommit-sha>",[220,793,794],{"class":519},"   # v4\n",[220,796,798,800,802],{"class":222,"line":797},27,[220,799,764],{"class":525},[220,801,530],{"class":529},[220,803,769],{"class":230},[220,805,807,810],{"class":222,"line":806},28,[220,808,809],{"class":525},"        with",[220,811,541],{"class":529},[220,813,815,818,820],{"class":222,"line":814},29,[220,816,817],{"class":525},"          name",[220,819,530],{"class":529},[220,821,822],{"class":230},"sbom\n",[220,824,826,829,831],{"class":222,"line":825},30,[220,827,828],{"class":525},"          path",[220,830,530],{"class":529},[220,832,833],{"class":230},"sbom.cdx.json\n",[10,835,836,837,840,841,844,845,848],{},"Replace each ",[59,838,839],{},"\u003Ccommit-sha>"," with the full 40-character commit of the release you adopt — copy it from the action's release page — so every action is pinned to an immutable commit, with the human-readable version kept in a comment. Dependabot understands this format and will propose SHA updates with the new version in the comment. ",[59,842,843],{},"permissions: contents: read"," gives the job the least access that works; it needs no secrets at all. Pair this with the release workflow from ",[17,846,847],{"href":380},"publishing to PyPI with trusted publishing"," and a Dependabot or Renovate configuration with a minimum release age, and the four links of the chain each have a guard.",[10,850,851,852,856,857,859],{},"For local development, the same audit runs as a ",[17,853,855],{"href":854},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002F","pre-commit hook"," on changes to ",[59,858,145],{},", so a vulnerable upgrade is flagged before it is even pushed. Keep it out of the per-commit path otherwise — it needs the network and takes a few seconds.",[37,861,863],{"id":862},"a-one-page-threat-model","A one-page threat model",[10,865,866,867,870],{},"Writing down what you are defending against keeps the effort proportionate. For a typical open-source CLI, a short table in ",[59,868,869],{},"SECURITY.md"," is enough:",[872,873,874,890],"table",{},[875,876,877],"thead",{},[878,879,880,884,887],"tr",{},[881,882,883],"th",{},"Threat",[881,885,886],{},"Likelihood",[881,888,889],{},"Defence in this topic",[891,892,893,907,918,929,939,949],"tbody",{},[878,894,895,899,902],{},[896,897,898],"td",{},"Known CVE in a dependency",[896,900,901],{},"High",[896,903,904,906],{},[59,905,61],{}," in CI and on a schedule",[878,908,909,912,915],{},[896,910,911],{},"Malicious new release of a dependency",[896,913,914],{},"Medium",[896,916,917],{},"Cooldown before adopting updates",[878,919,920,923,926],{},[896,921,922],{},"Typo-squatted dependency added by mistake",[896,924,925],{},"Low–medium",[896,927,928],{},"Review of every new dependency",[878,930,931,934,936],{},[896,932,933],{},"Stolen publishing credential",[896,935,914],{},[896,937,938],{},"Trusted publishing, no tokens",[878,940,941,944,946],{},[896,942,943],{},"Compromised CI action",[896,945,925],{},[896,947,948],{},"Actions pinned to SHAs, least permissions",[878,950,951,954,957],{},[896,952,953],{},"Tampered download",[896,955,956],{},"Low",[896,958,959],{},"Hashes, attestations, checksums",[10,961,962],{},"Revisit it when the tool's audience changes — a CLI that starts being used inside banks deserves a stricter column than a hobby project.",[37,964,966],{"id":965},"common-pitfalls","Common pitfalls",[42,968,969,975,984,990,1000,1006],{},[45,970,971,974],{},[48,972,973],{},"A long-lived PyPI token in CI secrets."," Replace it with trusted publishing and delete the token.",[45,976,977,980,981,983],{},[48,978,979],{},"Auditing the wrong thing."," Running ",[59,982,61],{}," against the current developer environment instead of the lock file checks what happens to be installed, not what you ship.",[45,985,986,989],{},[48,987,988],{},"Ignoring advisories forever."," An ignore without an expiry date becomes permanent by accident.",[45,991,992,995,996,999],{},[48,993,994],{},"Unpinned actions in the release workflow."," ",[59,997,998],{},"uses: some\u002Faction@main"," hands that repository control of your release.",[45,1001,1002,1005],{},[48,1003,1004],{},"Upgrading the moment a release appears."," Immediate auto-merge of dependency updates is exactly the path a malicious release takes.",[45,1007,1008,1011],{},[48,1009,1010],{},"Assuming users get your lock file."," They do not; package installs resolve ranges. Plan for that explicitly.",[37,1013,1015],{"id":1014},"key-takeaways","Key takeaways",[42,1017,1018,1021,1027,1030,1033],{},[45,1019,1020],{},"A CLI inherits its users' privileges, so its supply chain is part of their security.",[45,1022,1023,1024,1026],{},"Lock dependencies, generate an SBOM per release, and audit the lock file with ",[59,1025,61],{}," in CI.",[45,1028,1029],{},"Enforce hashes wherever you install, and let new dependency releases age before adopting them.",[45,1031,1032],{},"Publish with trusted publishing from an isolated, protected job with pinned actions; PyPI will attach attestations.",[45,1034,1035],{},"Document how users verify releases, and keep dependencies current with an automated, cooldown-aware updater.",[37,1037,1039],{"id":1038},"frequently-asked-questions","Frequently asked questions",[1041,1042,1044],"h3",{"id":1043},"is-all-of-this-necessary-for-a-small-internal-tool","Is all of this necessary for a small internal tool?",[10,1046,1047,1048,1050],{},"The cheap parts are: a lock file, ",[59,1049,61],{}," in CI, and no long-lived publishing tokens. Attestation verification and SBOMs matter most when other organisations install your tool or when the tool runs with privileged credentials.",[1041,1052,1054,1055,1057],{"id":1053},"should-a-cli-pin-exact-dependency-versions-in-pyprojecttoml","Should a CLI pin exact dependency versions in ",[59,1056,334],{},"?",[10,1059,1060],{},"Generally no. Exact pins in published metadata stop users from receiving security fixes and cause conflicts when the CLI is installed alongside other packages. Use lower bounds (and upper bounds only for known incompatibilities) in metadata, and exact pins in the lock file you test and build with.",[1041,1062,1064],{"id":1063},"how-do-i-report-a-vulnerability-in-my-own-cli","How do I report a vulnerability in my own CLI?",[10,1066,1067,1068,1070,1071,1073],{},"Publish a ",[59,1069,869],{}," with a private contact method — GitHub's private vulnerability reporting is the easiest — and use GitHub Security Advisories to coordinate a fix and request a CVE. Advisories you publish there flow into the databases ",[59,1072,61],{}," reads.",[1041,1075,1077],{"id":1076},"do-i-need-to-sign-my-releases-myself","Do I need to sign my releases myself?",[10,1079,1080],{},"Not for packages on PyPI published with trusted publishing; the attestations are generated for you. For binaries and other artefacts you host elsewhere, publish checksums and use GitHub artifact attestations or Sigstore signing.",[1041,1082,1084],{"id":1083},"what-about-malicious-code-in-my-own-repository","What about malicious code in my own repository?",[10,1086,1087],{},"Branch protection, required reviews, and signed commits for maintainers reduce the risk. The release-job separation above limits the damage a single compromised contributor can do, because publishing requires the protected environment.",[1041,1089,1091],{"id":1090},"are-build-time-dependencies-part-of-the-supply-chain-too","Are build-time dependencies part of the supply chain too?",[10,1093,1094,1095,1098,1099,26,1102,1105,1106,31],{},"Yes. The build backend (hatchling, setuptools, uv_build) and any build plugins run code while your wheel is produced. Pin them with lower and upper bounds in ",[59,1096,1097],{},"[build-system] requires",", build in an isolated environment (the default for ",[59,1100,1101],{},"uv build",[59,1103,1104],{},"python -m build","), and treat a change of build backend with the same review as a new runtime dependency — see ",[17,1107,1109],{"href":1108},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli\u002F","choosing a build backend",[37,1111,1113],{"id":1112},"related","Related",[42,1115,1116,1121,1126,1130,1134,1138,1143],{},[45,1117,1118,1119],{},"Up: ",[17,1120,20],{"href":19},[45,1122,1123,1124],{},"Down: ",[17,1125,278],{"href":277},[45,1127,1123,1128],{},[17,1129,321],{"href":320},[45,1131,1123,1132],{},[17,1133,484],{"href":483},[45,1135,1123,1136],{},[17,1137,195],{"href":194},[45,1139,1140,1141],{},"Sideways: ",[17,1142,25],{"href":24},[45,1144,1140,1145],{},[17,1146,1148],{"href":1147},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F","Secrets and credentials in Python CLIs",[1150,1151,1152],"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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":216,"searchDepth":253,"depth":253,"links":1154},[1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1176],{"id":39,"depth":253,"text":40},{"id":89,"depth":253,"text":90},{"id":138,"depth":253,"text":139},{"id":202,"depth":253,"text":203},{"id":288,"depth":253,"text":289},{"id":369,"depth":253,"text":370},{"id":432,"depth":253,"text":433},{"id":488,"depth":253,"text":489},{"id":503,"depth":253,"text":504},{"id":862,"depth":253,"text":863},{"id":965,"depth":253,"text":966},{"id":1014,"depth":253,"text":1015},{"id":1038,"depth":253,"text":1039,"children":1168},[1169,1170,1172,1173,1174,1175],{"id":1043,"depth":470,"text":1044},{"id":1053,"depth":470,"text":1171},"Should a CLI pin exact dependency versions in pyproject.toml?",{"id":1063,"depth":470,"text":1064},{"id":1076,"depth":470,"text":1077},{"id":1083,"depth":470,"text":1084},{"id":1090,"depth":470,"text":1091},{"id":1112,"depth":253,"text":1113},"2026-10-02","Protect a Python CLI and its users: audit dependencies, pin with hashes, delay brand-new releases, publish with attestations and ship an SBOM.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain",{"title":5,"description":1178},"project-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Findex",[1187,1188,1189,1190,1191],"security","supply-chain","dependencies","pypi","ci","2iEd7KcEZqur_h_CTapDF9SVrF-17aXIPqeXXCpZmvs",[1194,1197,1200,1203,1206,1209,1212,1215,1218,1221,1224,1227,1230,1233,1236,1239,1242,1245,1248,1251,1254,1257,1260,1263,1266,1269,1272,1275,1278,1281,1284,1287,1290,1293,1296,1299,1302,1305,1308,1311,1314,1317,1320,1323,1326,1329,1332,1335,1338,1341,1344,1347,1350,1353,1356,1359,1362,1365,1368,1371,1374,1377,1380,1383,1386,1389,1392,1395,1398,1401,1404,1407,1410,1413,1416,1419,1422,1425,1428,1431,1434,1437,1440,1443,1446,1449,1452,1455,1458,1461,1464,1467,1470,1473,1476,1479,1482,1485,1488,1491,1494,1497,1500,1503,1506,1509,1512,1515,1518,1521,1524,1527,1530,1533,1536,1539,1542,1545,1548,1551,1554,1557,1560,1563,1566,1569,1572,1575,1578,1581,1584,1587,1590,1593,1596,1599,1602,1605,1608,1611,1614,1617,1620,1623,1626,1629,1632,1635,1638,1641,1644,1647,1650,1653,1656,1659,1662,1665,1668,1671,1674,1677,1680,1683,1686,1689,1692,1695,1698,1701,1704,1707,1710,1713,1716,1719,1722,1725,1728,1731,1734,1737,1740,1743,1746,1749,1752,1755,1758,1761,1764,1767,1770,1773,1776,1779,1782,1785,1788,1791,1794,1797,1800,1803,1806,1809,1812,1815,1818,1821,1824,1827,1830,1833,1836,1839,1842,1845,1848,1851,1854,1857,1860,1863,1866,1869,1871,1874,1877,1880,1883,1886,1889,1892,1895,1898,1901,1904,1907,1910,1913,1916,1919,1922,1925,1928,1931,1934,1937,1940,1943,1946,1949,1952,1955,1958,1961,1964,1967,1970,1973,1976,1979,1982,1985,1986,1989,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034],{"path":1195,"title":1196},"\u002Fabout","About Python CLI Toolcraft",{"path":1198,"title":1199},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1201,"title":1202},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1204,"title":1205},"\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":1207,"title":1208},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1210,"title":1211},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1213,"title":1214},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":1216,"title":1217},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1219,"title":1220},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":1222,"title":1223},"\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":1225,"title":1226},"\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":1228,"title":1229},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1231,"title":1232},"\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":1234,"title":1235},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":1237,"title":1238},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1240,"title":1241},"\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":1243,"title":1244},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":1246,"title":1247},"\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":1249,"title":1250},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1252,"title":1253},"\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":1255,"title":1256},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1258,"title":1259},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1261,"title":1262},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1264,"title":1265},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1267,"title":1268},"\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":1270,"title":1271},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":1273,"title":1274},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1276,"title":1277},"\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":1279,"title":1280},"\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":1282,"title":1283},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1285,"title":1286},"\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":1288,"title":1289},"\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":1291,"title":1292},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1294,"title":1295},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1297,"title":1298},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1300,"title":1301},"\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":1303,"title":1304},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":1306,"title":1307},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1309,"title":1310},"\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":1312,"title":1313},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1315,"title":1316},"\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":1318,"title":1319},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1321,"title":1322},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1324,"title":1325},"\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":1327,"title":1328},"\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":1330,"title":1331},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1333,"title":1334},"\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":1336,"title":1337},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1339,"title":1340},"\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":1342,"title":1343},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1345,"title":1346},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1348,"title":1349},"\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":1351,"title":1352},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1354,"title":1355},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1357,"title":1358},"\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":1360,"title":1361},"\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":1363,"title":1364},"\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":1366,"title":1367},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":1369,"title":1370},"\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":1372,"title":1373},"\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":1375,"title":1376},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":1378,"title":1379},"\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":1381,"title":1382},"\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":1384,"title":1385},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1387,"title":1388},"\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":1390,"title":1391},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":1393,"title":1394},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1396,"title":1397},"\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":1399,"title":1400},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1402,"title":1403},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1405,"title":1406},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":1408,"title":1409},"\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":1411,"title":1412},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1414,"title":1415},"\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":1417,"title":1418},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1420,"title":1421},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1423,"title":1424},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1426,"title":1427},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1429,"title":1430},"\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":1432,"title":1433},"\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":1435,"title":1436},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1438,"title":1439},"\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":1441,"title":1442},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1444,"title":1445},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1447,"title":1448},"\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":1450,"title":1451},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1453,"title":1454},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1456,"title":1457},"\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":1459,"title":1460},"\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":1462,"title":1463},"\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":1465,"title":1466},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1468,"title":1469},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1471,"title":1472},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1474,"title":1475},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1477,"title":1478},"\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":1480,"title":1481},"\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":1483,"title":1484},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":1486,"title":1487},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1489,"title":1490},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1492,"title":1493},"\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":1495,"title":1496},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1498,"title":1499},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1501,"title":1502},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1504,"title":1505},"\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":1507,"title":1508},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1510,"title":1511},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1513,"title":1514},"\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":1516,"title":1517},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":1519,"title":1520},"\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":1522,"title":1523},"\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":1525,"title":1526},"\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":1528,"title":1529},"\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":1531,"title":1532},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1534,"title":1535},"\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":1537,"title":1538},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1540,"title":1541},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":1543,"title":1544},"\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":1546,"title":1547},"\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":1549,"title":1550},"\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":1552,"title":1553},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1555,"title":1556},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1558,"title":1559},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1561,"title":1562},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1564,"title":1565},"\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":1567,"title":1568},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":1570,"title":1571},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1573,"title":1574},"\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":1576,"title":1577},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1579,"title":1580},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1582,"title":1583},"\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":1585,"title":1586},"\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":1588,"title":1589},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":1591,"title":1592},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1594,"title":1595},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1597,"title":1598},"\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":1600,"title":1601},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":1603,"title":1604},"\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":1606,"title":1607},"\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":1609,"title":1610},"\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":1612,"title":1613},"\u002F","Python CLI Toolcraft",{"path":1615,"title":1616},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":1618,"title":1619},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":1621,"title":1622},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":1624,"title":1625},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":1627,"title":1628},"\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":1630,"title":1631},"\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":1633,"title":1634},"\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":1636,"title":1637},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":1639,"title":1640},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1642,"title":1643},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1645,"title":1646},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1648,"title":1649},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1651,"title":1652},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1654,"title":1655},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1657,"title":1658},"\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":1660,"title":1661},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1663,"title":1664},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1666,"title":1667},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1669,"title":1670},"\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":1672,"title":1673},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1675,"title":1676},"\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":1678,"title":1679},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":1681,"title":1682},"\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":1684,"title":1685},"\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":1687,"title":1688},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1690,"title":1691},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1693,"title":1694},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":1696,"title":1697},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1699,"title":1700},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1702,"title":1703},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1705,"title":1706},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1708,"title":1709},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":1711,"title":1712},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":1714,"title":1715},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1717,"title":1718},"\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":1720,"title":1721},"\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":1723,"title":1724},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1726,"title":1727},"\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":1729,"title":1730},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1732,"title":1733},"\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":1735,"title":1736},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":1738,"title":1739},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1741,"title":1742},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1744,"title":1745},"\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":1747,"title":1748},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1750,"title":1751},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1753,"title":1754},"\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":1756,"title":1757},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1759,"title":1760},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1762,"title":1763},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1765,"title":1766},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":1768,"title":1769},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1771,"title":1772},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":1774,"title":1775},"\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":1777,"title":1778},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1780,"title":1781},"\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":1783,"title":1784},"\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":1786,"title":1787},"\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":1789,"title":1790},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1792,"title":1793},"\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":1795,"title":1796},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1798,"title":1799},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":1801,"title":1802},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":1804,"title":1805},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":1807,"title":1808},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":1810,"title":1811},"\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":1813,"title":1814},"\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":1816,"title":1817},"\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":1819,"title":1820},"\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":1822,"title":1823},"\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":1825,"title":1826},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":1828,"title":1829},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1831,"title":1832},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1834,"title":1835},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":1837,"title":1838},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":1840,"title":1841},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":1843,"title":1844},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":1846,"title":1847},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1849,"title":1850},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1852,"title":1853},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":1855,"title":1856},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1858,"title":1859},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1861,"title":1862},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":1864,"title":1865},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":1867,"title":1868},"\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":1870,"title":20},"\u002Fproject-setup-dependency-management",{"path":1872,"title":1873},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":1875,"title":1876},"\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":1878,"title":1879},"\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":1881,"title":1882},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":1884,"title":1885},"\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":1887,"title":1888},"\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":1890,"title":1891},"\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":1893,"title":1894},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1896,"title":1897},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":1899,"title":1900},"\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":1902,"title":1903},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1905,"title":1906},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1908,"title":1909},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":1911,"title":1912},"\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":1914,"title":1915},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1917,"title":1918},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":1920,"title":1921},"\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":1923,"title":1924},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1926,"title":1927},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1929,"title":1930},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":1932,"title":1933},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1935,"title":1936},"\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":1938,"title":1939},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":1941,"title":1942},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1944,"title":1945},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":1947,"title":1948},"\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":1950,"title":1951},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":1953,"title":1954},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1956,"title":1957},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":1959,"title":1960},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1962,"title":1963},"\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":1965,"title":1966},"\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":1968,"title":1969},"\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":1971,"title":1972},"\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":1974,"title":1975},"\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":1977,"title":1978},"\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":1980,"title":1981},"\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":1983,"title":1984},"\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":1183,"title":5},{"path":1987,"title":1988},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":1990,"title":1991},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":1993,"title":1994},"\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":1996,"title":1997},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1999,"title":2000},"\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":2002,"title":2003},"\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":2005,"title":2006},"\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":2008,"title":2009},"\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":2011,"title":2012},"\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":2014,"title":2015},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2017,"title":2018},"\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":2020,"title":2021},"\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":2023,"title":2024},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2026,"title":2027},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":2029,"title":2030},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2032,"title":2033},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2035,"title":2036},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967543035]