[{"data":1,"prerenderedAt":3280},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F":3,"content-directory":2437},{"id":4,"title":5,"body":6,"date":2423,"description":2424,"difficulty":2425,"draft":2426,"extension":2427,"meta":2428,"navigation":218,"path":2429,"seo":2430,"stem":2431,"tags":2432,"updated":2423,"__hash__":2436},"content\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Findex.md","Secrets and Credentials in Python CLIs",{"type":7,"value":8,"toc":2401},"minimark",[9,13,27,31,36,90,94,97,100,129,139,153,167,171,174,177,943,968,972,994,997,1025,1029,1035,1107,1124,1128,1143,1156,1167,1171,1195,1199,1212,1624,1644,1648,1655,1683,1693,1708,1712,1732,1736,1746,2256,2260,2293,2297,2302,2308,2312,2315,2319,2330,2334,2337,2341,2344,2348,2397],[10,11,12],"p",{},"Command-line tools handle some of the most sensitive strings on a developer's machine: API tokens with write access to production, cloud credentials, database passwords, signing keys. They also have more ways to leak them than almost any other kind of software. Arguments show up in the process list and shell history. Config files end up in dotfile repositories and backups. Debug logs get pasted into issue trackers. Tracebacks print local variables. Environment variables are inherited by every child process the tool starts. None of these leaks requires an attacker — just a normal workday and a tool that was not designed with them in mind.",[10,14,15,16,21,22,26],{},"This topic covers handling credentials in a Python CLI so that the safe path is the default one: storing tokens in the operating system's keychain, accepting secrets from environment variables and mounted files for automation, prompting securely when a person is present, redacting secrets from everything the tool prints, and supporting several accounts without mixing them up. It belongs to the ",[17,18,20],"a",{"href":19},"\u002Fcli-runtime-systems-integration\u002F","CLI Runtime & Systems Integration"," section and underpins ",[17,23,25],{"href":24},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002F","calling HTTP APIs",", where most of those tokens are used.",[28,29],"inline-diagram",{"name":30},"sec-topic-map",[32,33,35],"h2",{"id":34},"tldr","TL;DR",[37,38,39,47,58,72,78,84],"ul",{},[40,41,42,46],"li",{},[43,44,45],"strong",{},"Never accept a secret as a plain command-line argument."," Use an environment variable, a file path, stdin or a prompt.",[40,48,49,52,53,57],{},[43,50,51],{},"Store long-lived tokens in the system keychain"," via ",[54,55,56],"code",{},"keyring",", not in a config file. Keep non-secret settings in config.",[40,59,60,63,64,67,68,71],{},[43,61,62],{},"Support automation explicitly:"," ",[54,65,66],{},"MYTOOL_TOKEN"," and ",[54,69,70],{},"MYTOOL_TOKEN_FILE"," for CI and containers.",[40,73,74,77],{},[43,75,76],{},"Prompt only when a person is present",", with no echo, and fail with instructions otherwise.",[40,79,80,83],{},[43,81,82],{},"Wrap secrets in a type that cannot print itself",", and add a redacting filter to logging as a second line of defence.",[40,85,86,89],{},[43,87,88],{},"Make the active account visible",", and add friction to destructive commands against production profiles.",[32,91,93],{"id":92},"where-clis-leak-secrets","Where CLIs leak secrets",[10,95,96],{},"Before the techniques, the threat model. For most internal tools the danger is not a targeted attacker but ordinary exposure: a secret ending up somewhere it is readable by more people, for longer, than intended.",[28,98],{"name":99},"sec-exposure-matrix",[10,101,102,105,106,109,110,113,114,109,117,120,121,124,125,128],{},[43,103,104],{},"Command-line arguments"," are the worst offender. On Linux, every user on the machine can read every process's arguments through ",[54,107,108],{},"ps"," or ",[54,111,112],{},"\u002Fproc"," for as long as the process runs. Shells record them in history files. CI systems echo commands into build logs. A ",[54,115,116],{},"--token",[54,118,119],{},"--password"," option invites every one of these leaks, which is why well-designed tools such as ",[54,122,123],{},"docker login"," warn when a password is passed on the command line and offer ",[54,126,127],{},"--password-stdin"," instead.",[10,130,131,134,135,138],{},[43,132,133],{},"Configuration files"," are designed to be copied: into backups, into dotfile repositories published on GitHub, into support tickets (\"here is my config\"). A token stored in ",[54,136,137],{},"~\u002F.config\u002Fmytool\u002Fconfig.toml"," inherits all of that.",[10,140,141,144,145,148,149,152],{},[43,142,143],{},"Logs, debug output and tracebacks"," are where secrets leak most often in practice, because they are produced precisely when something is wrong and are then shared with the people helping to fix it. HTTP debug logging prints ",[54,146,147],{},"Authorization"," headers; exceptions include URLs with embedded credentials; rich tracebacks with ",[54,150,151],{},"show_locals=True"," print every local variable.",[10,154,155,158,159,162,163,166],{},[43,156,157],{},"The environment"," is inherited by every subprocess. A tool that sets ",[54,160,161],{},"AWS_SECRET_ACCESS_KEY"," for its own use and then runs ",[54,164,165],{},"npm install"," has just given that key to every install script of every dependency.",[32,168,170],{"id":169},"where-a-credential-should-come-from","Where a credential should come from",[10,172,173],{},"A CLI needs to serve two kinds of user with one code path: a person at a terminal, who should log in once and forget about it, and automation — CI jobs, containers, cron — which cannot interact and needs to inject credentials from a secret manager. Resolving credentials from an ordered list of sources satisfies both:",[28,175],{"name":176},"sec-lookup-order",[178,179,184],"pre",{"className":180,"code":181,"language":182,"meta":183,"style":183},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fcredentials.py\nfrom __future__ import annotations\n\nimport hmac\nimport os\nimport sys\nfrom getpass import getpass\nfrom pathlib import Path\n\nimport keyring\nfrom keyring.errors import KeyringError\n\nSERVICE = \"mytool\"\n\n\nclass Secret:\n    \"\"\"A string that will not print itself.\"\"\"\n\n    __slots__ = (\"_value\",)\n\n    def __init__(self, value: str) -> None:\n        self._value = value\n\n    def reveal(self) -> str:\n        return self._value\n\n    def __str__(self) -> str:\n        return \"********\"\n\n    def __repr__(self) -> str:\n        return \"Secret('********')\"\n\n    def __eq__(self, other: object) -> bool:\n        return isinstance(other, Secret) and hmac.compare_digest(self._value, other._value)\n\n    def __hash__(self) -> int:\n        return hash(self._value)\n\n\nclass NoCredential(Exception):\n    pass\n\n\ndef resolve_token(profile: str, token_file: Path | None = None,\n                  allow_prompt: bool = True) -> tuple[Secret, str]:\n    \"\"\"Return (token, source), trying each source in precedence order.\"\"\"\n    if token_file is not None:\n        return Secret(token_file.read_text(encoding=\"utf-8\").strip()), f\"file {token_file}\"\n    if path := os.environ.get(\"MYTOOL_TOKEN_FILE\"):\n        return Secret(Path(path).read_text(encoding=\"utf-8\").strip()), \"MYTOOL_TOKEN_FILE\"\n    if value := os.environ.get(\"MYTOOL_TOKEN\"):\n        return Secret(value), \"MYTOOL_TOKEN\"\n    try:\n        if value := keyring.get_password(SERVICE, profile):\n            return Secret(value), \"keychain\"\n    except KeyringError:\n        pass                               # no usable backend: fall through\n    if allow_prompt and sys.stdin.isatty():\n        return Secret(getpass(f\"API token for profile {profile!r}: \")), \"prompt\"\n    raise NoCredential(\n        f\"no token for profile {profile!r}: run 'mytool auth login', \"\n        \"or set MYTOOL_TOKEN \u002F MYTOOL_TOKEN_FILE\"\n    )\n","python","",[54,185,186,195,213,220,229,237,245,258,271,276,284,297,302,315,320,325,338,344,349,366,371,394,409,414,429,441,446,460,468,473,487,495,500,521,544,549,564,580,585,590,606,612,617,622,652,673,679,698,736,755,774,791,802,810,828,839,848,857,870,902,911,931,937],{"__ignoreMap":183},[187,188,191],"span",{"class":189,"line":190},"line",1,[187,192,194],{"class":193},"sJ8bj","# src\u002Fmytool\u002Fcredentials.py\n",[187,196,198,202,206,209],{"class":189,"line":197},2,[187,199,201],{"class":200},"szBVR","from",[187,203,205],{"class":204},"sj4cs"," __future__",[187,207,208],{"class":200}," import",[187,210,212],{"class":211},"sVt8B"," annotations\n",[187,214,216],{"class":189,"line":215},3,[187,217,219],{"emptyLinePlaceholder":218},true,"\n",[187,221,223,226],{"class":189,"line":222},4,[187,224,225],{"class":200},"import",[187,227,228],{"class":211}," hmac\n",[187,230,232,234],{"class":189,"line":231},5,[187,233,225],{"class":200},[187,235,236],{"class":211}," os\n",[187,238,240,242],{"class":189,"line":239},6,[187,241,225],{"class":200},[187,243,244],{"class":211}," sys\n",[187,246,248,250,253,255],{"class":189,"line":247},7,[187,249,201],{"class":200},[187,251,252],{"class":211}," getpass ",[187,254,225],{"class":200},[187,256,257],{"class":211}," getpass\n",[187,259,261,263,266,268],{"class":189,"line":260},8,[187,262,201],{"class":200},[187,264,265],{"class":211}," pathlib ",[187,267,225],{"class":200},[187,269,270],{"class":211}," Path\n",[187,272,274],{"class":189,"line":273},9,[187,275,219],{"emptyLinePlaceholder":218},[187,277,279,281],{"class":189,"line":278},10,[187,280,225],{"class":200},[187,282,283],{"class":211}," keyring\n",[187,285,287,289,292,294],{"class":189,"line":286},11,[187,288,201],{"class":200},[187,290,291],{"class":211}," keyring.errors ",[187,293,225],{"class":200},[187,295,296],{"class":211}," KeyringError\n",[187,298,300],{"class":189,"line":299},12,[187,301,219],{"emptyLinePlaceholder":218},[187,303,305,308,311],{"class":189,"line":304},13,[187,306,307],{"class":204},"SERVICE",[187,309,310],{"class":200}," =",[187,312,314],{"class":313},"sZZnC"," \"mytool\"\n",[187,316,318],{"class":189,"line":317},14,[187,319,219],{"emptyLinePlaceholder":218},[187,321,323],{"class":189,"line":322},15,[187,324,219],{"emptyLinePlaceholder":218},[187,326,328,331,335],{"class":189,"line":327},16,[187,329,330],{"class":200},"class",[187,332,334],{"class":333},"sScJk"," Secret",[187,336,337],{"class":211},":\n",[187,339,341],{"class":189,"line":340},17,[187,342,343],{"class":313},"    \"\"\"A string that will not print itself.\"\"\"\n",[187,345,347],{"class":189,"line":346},18,[187,348,219],{"emptyLinePlaceholder":218},[187,350,352,355,357,360,363],{"class":189,"line":351},19,[187,353,354],{"class":204},"    __slots__",[187,356,310],{"class":200},[187,358,359],{"class":211}," (",[187,361,362],{"class":313},"\"_value\"",[187,364,365],{"class":211},",)\n",[187,367,369],{"class":189,"line":368},20,[187,370,219],{"emptyLinePlaceholder":218},[187,372,374,377,380,383,386,389,392],{"class":189,"line":373},21,[187,375,376],{"class":200},"    def",[187,378,379],{"class":204}," __init__",[187,381,382],{"class":211},"(self, value: ",[187,384,385],{"class":204},"str",[187,387,388],{"class":211},") -> ",[187,390,391],{"class":204},"None",[187,393,337],{"class":211},[187,395,397,400,403,406],{"class":189,"line":396},22,[187,398,399],{"class":204},"        self",[187,401,402],{"class":211},"._value ",[187,404,405],{"class":200},"=",[187,407,408],{"class":211}," value\n",[187,410,412],{"class":189,"line":411},23,[187,413,219],{"emptyLinePlaceholder":218},[187,415,417,419,422,425,427],{"class":189,"line":416},24,[187,418,376],{"class":200},[187,420,421],{"class":333}," reveal",[187,423,424],{"class":211},"(self) -> ",[187,426,385],{"class":204},[187,428,337],{"class":211},[187,430,432,435,438],{"class":189,"line":431},25,[187,433,434],{"class":200},"        return",[187,436,437],{"class":204}," self",[187,439,440],{"class":211},"._value\n",[187,442,444],{"class":189,"line":443},26,[187,445,219],{"emptyLinePlaceholder":218},[187,447,449,451,454,456,458],{"class":189,"line":448},27,[187,450,376],{"class":200},[187,452,453],{"class":204}," __str__",[187,455,424],{"class":211},[187,457,385],{"class":204},[187,459,337],{"class":211},[187,461,463,465],{"class":189,"line":462},28,[187,464,434],{"class":200},[187,466,467],{"class":313}," \"********\"\n",[187,469,471],{"class":189,"line":470},29,[187,472,219],{"emptyLinePlaceholder":218},[187,474,476,478,481,483,485],{"class":189,"line":475},30,[187,477,376],{"class":200},[187,479,480],{"class":204}," __repr__",[187,482,424],{"class":211},[187,484,385],{"class":204},[187,486,337],{"class":211},[187,488,490,492],{"class":189,"line":489},31,[187,491,434],{"class":200},[187,493,494],{"class":313}," \"Secret('********')\"\n",[187,496,498],{"class":189,"line":497},32,[187,499,219],{"emptyLinePlaceholder":218},[187,501,503,505,508,511,514,516,519],{"class":189,"line":502},33,[187,504,376],{"class":200},[187,506,507],{"class":204}," __eq__",[187,509,510],{"class":211},"(self, other: ",[187,512,513],{"class":204},"object",[187,515,388],{"class":211},[187,517,518],{"class":204},"bool",[187,520,337],{"class":211},[187,522,524,526,529,532,535,538,541],{"class":189,"line":523},34,[187,525,434],{"class":200},[187,527,528],{"class":204}," isinstance",[187,530,531],{"class":211},"(other, Secret) ",[187,533,534],{"class":200},"and",[187,536,537],{"class":211}," hmac.compare_digest(",[187,539,540],{"class":204},"self",[187,542,543],{"class":211},"._value, other._value)\n",[187,545,547],{"class":189,"line":546},35,[187,548,219],{"emptyLinePlaceholder":218},[187,550,552,554,557,559,562],{"class":189,"line":551},36,[187,553,376],{"class":200},[187,555,556],{"class":204}," __hash__",[187,558,424],{"class":211},[187,560,561],{"class":204},"int",[187,563,337],{"class":211},[187,565,567,569,572,575,577],{"class":189,"line":566},37,[187,568,434],{"class":200},[187,570,571],{"class":204}," hash",[187,573,574],{"class":211},"(",[187,576,540],{"class":204},[187,578,579],{"class":211},"._value)\n",[187,581,583],{"class":189,"line":582},38,[187,584,219],{"emptyLinePlaceholder":218},[187,586,588],{"class":189,"line":587},39,[187,589,219],{"emptyLinePlaceholder":218},[187,591,593,595,598,600,603],{"class":189,"line":592},40,[187,594,330],{"class":200},[187,596,597],{"class":333}," NoCredential",[187,599,574],{"class":211},[187,601,602],{"class":204},"Exception",[187,604,605],{"class":211},"):\n",[187,607,609],{"class":189,"line":608},41,[187,610,611],{"class":200},"    pass\n",[187,613,615],{"class":189,"line":614},42,[187,616,219],{"emptyLinePlaceholder":218},[187,618,620],{"class":189,"line":619},43,[187,621,219],{"emptyLinePlaceholder":218},[187,623,625,628,631,634,636,639,642,645,647,649],{"class":189,"line":624},44,[187,626,627],{"class":200},"def",[187,629,630],{"class":333}," resolve_token",[187,632,633],{"class":211},"(profile: ",[187,635,385],{"class":204},[187,637,638],{"class":211},", token_file: Path ",[187,640,641],{"class":200},"|",[187,643,644],{"class":204}," None",[187,646,310],{"class":200},[187,648,644],{"class":204},[187,650,651],{"class":211},",\n",[187,653,655,658,660,662,665,668,670],{"class":189,"line":654},45,[187,656,657],{"class":211},"                  allow_prompt: ",[187,659,518],{"class":204},[187,661,310],{"class":200},[187,663,664],{"class":204}," True",[187,666,667],{"class":211},") -> tuple[Secret, ",[187,669,385],{"class":204},[187,671,672],{"class":211},"]:\n",[187,674,676],{"class":189,"line":675},46,[187,677,678],{"class":313},"    \"\"\"Return (token, source), trying each source in precedence order.\"\"\"\n",[187,680,682,685,688,691,694,696],{"class":189,"line":681},47,[187,683,684],{"class":200},"    if",[187,686,687],{"class":211}," token_file ",[187,689,690],{"class":200},"is",[187,692,693],{"class":200}," not",[187,695,644],{"class":204},[187,697,337],{"class":211},[187,699,701,703,706,710,712,715,718,721,724,727,730,733],{"class":189,"line":700},48,[187,702,434],{"class":200},[187,704,705],{"class":211}," Secret(token_file.read_text(",[187,707,709],{"class":708},"s4XuR","encoding",[187,711,405],{"class":200},[187,713,714],{"class":313},"\"utf-8\"",[187,716,717],{"class":211},").strip()), ",[187,719,720],{"class":200},"f",[187,722,723],{"class":313},"\"file ",[187,725,726],{"class":204},"{",[187,728,729],{"class":211},"token_file",[187,731,732],{"class":204},"}",[187,734,735],{"class":313},"\"\n",[187,737,739,741,744,747,750,753],{"class":189,"line":738},49,[187,740,684],{"class":200},[187,742,743],{"class":211}," path ",[187,745,746],{"class":200},":=",[187,748,749],{"class":211}," os.environ.get(",[187,751,752],{"class":313},"\"MYTOOL_TOKEN_FILE\"",[187,754,605],{"class":211},[187,756,758,760,763,765,767,769,771],{"class":189,"line":757},50,[187,759,434],{"class":200},[187,761,762],{"class":211}," Secret(Path(path).read_text(",[187,764,709],{"class":708},[187,766,405],{"class":200},[187,768,714],{"class":313},[187,770,717],{"class":211},[187,772,773],{"class":313},"\"MYTOOL_TOKEN_FILE\"\n",[187,775,777,779,782,784,786,789],{"class":189,"line":776},51,[187,778,684],{"class":200},[187,780,781],{"class":211}," value ",[187,783,746],{"class":200},[187,785,749],{"class":211},[187,787,788],{"class":313},"\"MYTOOL_TOKEN\"",[187,790,605],{"class":211},[187,792,794,796,799],{"class":189,"line":793},52,[187,795,434],{"class":200},[187,797,798],{"class":211}," Secret(value), ",[187,800,801],{"class":313},"\"MYTOOL_TOKEN\"\n",[187,803,805,808],{"class":189,"line":804},53,[187,806,807],{"class":200},"    try",[187,809,337],{"class":211},[187,811,813,816,818,820,823,825],{"class":189,"line":812},54,[187,814,815],{"class":200},"        if",[187,817,781],{"class":211},[187,819,746],{"class":200},[187,821,822],{"class":211}," keyring.get_password(",[187,824,307],{"class":204},[187,826,827],{"class":211},", profile):\n",[187,829,831,834,836],{"class":189,"line":830},55,[187,832,833],{"class":200},"            return",[187,835,798],{"class":211},[187,837,838],{"class":313},"\"keychain\"\n",[187,840,842,845],{"class":189,"line":841},56,[187,843,844],{"class":200},"    except",[187,846,847],{"class":211}," KeyringError:\n",[187,849,851,854],{"class":189,"line":850},57,[187,852,853],{"class":200},"        pass",[187,855,856],{"class":193},"                               # no usable backend: fall through\n",[187,858,860,862,865,867],{"class":189,"line":859},58,[187,861,684],{"class":200},[187,863,864],{"class":211}," allow_prompt ",[187,866,534],{"class":200},[187,868,869],{"class":211}," sys.stdin.isatty():\n",[187,871,873,875,878,880,883,885,888,891,893,896,899],{"class":189,"line":872},59,[187,874,434],{"class":200},[187,876,877],{"class":211}," Secret(getpass(",[187,879,720],{"class":200},[187,881,882],{"class":313},"\"API token for profile ",[187,884,726],{"class":204},[187,886,887],{"class":211},"profile",[187,889,890],{"class":200},"!r",[187,892,732],{"class":204},[187,894,895],{"class":313},": \"",[187,897,898],{"class":211},")), ",[187,900,901],{"class":313},"\"prompt\"\n",[187,903,905,908],{"class":189,"line":904},60,[187,906,907],{"class":200},"    raise",[187,909,910],{"class":211}," NoCredential(\n",[187,912,914,917,920,922,924,926,928],{"class":189,"line":913},61,[187,915,916],{"class":200},"        f",[187,918,919],{"class":313},"\"no token for profile ",[187,921,726],{"class":204},[187,923,887],{"class":211},[187,925,890],{"class":200},[187,927,732],{"class":204},[187,929,930],{"class":313},": run 'mytool auth login', \"\n",[187,932,934],{"class":189,"line":933},62,[187,935,936],{"class":313},"        \"or set MYTOOL_TOKEN \u002F MYTOOL_TOKEN_FILE\"\n",[187,938,940],{"class":189,"line":939},63,[187,941,942],{"class":211},"    )\n",[10,944,945,946,949,950,953,954,958,959,67,963,967],{},"Returning the ",[43,947,948],{},"source"," alongside the token pays for itself the first time someone asks \"why is it using the wrong account?\" — a ",[54,951,952],{},"--verbose"," line saying \"token from MYTOOL_TOKEN\" answers it instantly. The individual sources each have a dedicated guide: ",[17,955,957],{"href":956},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring\u002F","storing tokens with keyring",", ",[17,960,962],{"href":961},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002F","reading secrets from env and files",[17,964,966],{"href":965},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely\u002F","prompting for passwords securely",".",[32,969,971],{"id":970},"a-type-that-cannot-leak","A type that cannot leak",[10,973,974,975,978,979,982,983,986,987,990,991,993],{},"The ",[54,976,977],{},"Secret"," wrapper above is small, but it changes the default. An f-string, a log call, a ",[54,980,981],{},"print(settings)"," for debugging or a traceback showing local variables all see ",[54,984,985],{},"********",". Getting at the real value requires calling ",[54,988,989],{},".reveal()",", which you do in exactly one or two places — building the ",[54,992,147],{}," header, passing a password to a database driver — and which is easy to audit with a search.",[28,995],{"name":996},"sec-secret-type",[10,998,999,1000,1003,1004,1007,1008,1011,1012,1015,1016,1020,1021,1024],{},"Wrap secrets ",[43,1001,1002],{},"at the moment they are read"," and unwrap them ",[43,1005,1006],{},"at the moment they are used",". Everything in between — settings objects, context objects, function arguments — carries the wrapper. If you already use pydantic, ",[54,1009,1010],{},"SecretStr"," provides the same behaviour; ",[54,1013,1014],{},"pydantic-settings"," reads environment variables straight into it, as covered in ",[17,1017,1019],{"href":1018},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings\u002F","typed settings with pydantic-settings",". Comparing with ",[54,1022,1023],{},"hmac.compare_digest"," avoids leaking a secret's contents through timing, which matters little in a CLI but costs nothing.",[32,1026,1028],{"id":1027},"storing-long-lived-credentials","Storing long-lived credentials",[10,1030,1031,1032,1034],{},"Users should log in once, not paste a token on every command. The right place to keep that token is the operating system's credential store — the macOS Keychain, Windows Credential Manager, or the Secret Service on Linux desktops — which encrypts at rest, is unlocked by the user's login, and is not part of any file your tool writes. The ",[54,1033,56],{}," package gives one API over all of them:",[178,1036,1038],{"className":180,"code":1037,"language":182,"meta":183,"style":183},"import keyring\n\nkeyring.set_password(\"mytool\", \"prod\", token)       # mytool auth login\ntoken = keyring.get_password(\"mytool\", \"prod\")        # every other command\nkeyring.delete_password(\"mytool\", \"prod\")             # mytool auth logout\n",[54,1039,1040,1046,1050,1069,1090],{"__ignoreMap":183},[187,1041,1042,1044],{"class":189,"line":190},[187,1043,225],{"class":200},[187,1045,283],{"class":211},[187,1047,1048],{"class":189,"line":197},[187,1049,219],{"emptyLinePlaceholder":218},[187,1051,1052,1055,1058,1060,1063,1066],{"class":189,"line":215},[187,1053,1054],{"class":211},"keyring.set_password(",[187,1056,1057],{"class":313},"\"mytool\"",[187,1059,958],{"class":211},[187,1061,1062],{"class":313},"\"prod\"",[187,1064,1065],{"class":211},", token)       ",[187,1067,1068],{"class":193},"# mytool auth login\n",[187,1070,1071,1074,1076,1078,1080,1082,1084,1087],{"class":189,"line":222},[187,1072,1073],{"class":211},"token ",[187,1075,405],{"class":200},[187,1077,822],{"class":211},[187,1079,1057],{"class":313},[187,1081,958],{"class":211},[187,1083,1062],{"class":313},[187,1085,1086],{"class":211},")        ",[187,1088,1089],{"class":193},"# every other command\n",[187,1091,1092,1095,1097,1099,1101,1104],{"class":189,"line":231},[187,1093,1094],{"class":211},"keyring.delete_password(",[187,1096,1057],{"class":313},[187,1098,958],{"class":211},[187,1100,1062],{"class":313},[187,1102,1103],{"class":211},")             ",[187,1105,1106],{"class":193},"# mytool auth logout\n",[10,1108,1109,1110,1112,1113,1116,1117,67,1120,1123],{},"Two practical complications deserve planning. Headless Linux machines — servers, containers, WSL without a desktop session — often have no keyring backend, and ",[54,1111,56],{}," raises an error there; your tool should fall back to environment variables and say so. And some organisations forbid storing tokens at all on shared machines; an environment-only mode respects that. The ",[17,1114,1115],{"href":956},"keyring guide"," covers backend detection, the fallbacks and ",[54,1118,1119],{},"auth status",[54,1121,1122],{},"logout"," commands.",[32,1125,1127],{"id":1126},"automation-environment-variables-and-secret-files","Automation: environment variables and secret files",[10,1129,1130,1131,1134,1135,1138,1139,1142],{},"CI systems and container orchestrators deliver secrets in two ways: as ",[43,1132,1133],{},"environment variables"," (GitHub Actions secrets, GitLab CI variables) or as ",[43,1136,1137],{},"files"," mounted into the container (Docker and Kubernetes secrets under ",[54,1140,1141],{},"\u002Frun\u002Fsecrets","). Support both. The environment variable is the most convenient; the file is safer, because it is not inherited by child processes and can be rotated without restarting a long-running process.",[10,1144,1145,1146,1149,1150,1152,1153,1155],{},"The convention popularised by official Docker images is a paired variable with a ",[54,1147,1148],{},"_FILE"," suffix: ",[54,1151,66],{}," holds the value, ",[54,1154,70],{}," holds a path to read it from. Operators already know the pattern, and supporting it costs three lines, as in the resolver above.",[10,1157,1158,1159,1162,1163,967],{},"When your CLI starts subprocesses, pass them an ",[43,1160,1161],{},"explicit"," environment containing only what they need, rather than inheriting everything. A tool that runs third-party build scripts with the user's cloud credentials in the environment is handing those credentials to code nobody reviewed. The mechanics are in ",[17,1164,1166],{"href":1165},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002F","running subprocesses from Python CLIs",[32,1168,1170],{"id":1169},"prompting-when-a-person-is-present","Prompting when a person is present",[10,1172,1173,1174,1177,1178,1181,1182,1185,1186,1189,1190,1192,1193,967],{},"When no stored or injected credential exists and a person is at the terminal, prompting is reasonable — for a password to unlock a local vault, or a one-off token. Use ",[54,1175,1176],{},"getpass.getpass()"," (or ",[54,1179,1180],{},"typer.prompt(..., hide_input=True)","), which disables echo and reads from the controlling terminal. And never prompt when nobody is there: a prompt in a CI job does not fail, it hangs until the job times out. Check ",[54,1183,1184],{},"sys.stdin.isatty()",", respect a ",[54,1187,1188],{},"--no-input"," flag, and when prompting is impossible, fail immediately with a message naming the environment variable to set. The full treatment, including confirmation for new passwords and ",[54,1191,127],{},", is in ",[17,1194,966],{"href":965},[32,1196,1198],{"id":1197},"keeping-secrets-out-of-output","Keeping secrets out of output",[10,1200,974,1201,1203,1204,1207,1208,1211],{},[54,1202,977],{}," type protects values you control. It cannot protect values that other libraries print: httpx's debug logging of request headers, a database driver's connection-string error, a URL with ",[54,1205,1206],{},"user:password@"," in an exception message. For those, add a ",[43,1209,1210],{},"redacting filter"," to your logging configuration that masks registered secret values and well-known token patterns before any handler writes the record:",[178,1213,1215],{"className":180,"code":1214,"language":182,"meta":183,"style":183},"import logging\nimport re\n\nTOKEN_PATTERNS = [\n    re.compile(r\"(Bearer\\s+)[A-Za-z0-9._~+\u002F-]+=*\"),\n    re.compile(r\"(:\u002F\u002F[^:\u002F\\s]+:)[^@\\s]+(@)\"),\n    re.compile(r\"\\b(ghp_|gho_|xoxb-|xoxp-)[A-Za-z0-9-]+\"),\n]\n\n\nclass RedactingFilter(logging.Filter):\n    def __init__(self, secrets: list[str]) -> None:\n        super().__init__()\n        self.secrets = [s for s in secrets if len(s) >= 6]\n\n    def filter(self, record: logging.LogRecord) -> bool:\n        message = record.getMessage()\n        for s in self.secrets:\n            message = message.replace(s, \"****\")\n        for pattern in TOKEN_PATTERNS:\n            message = pattern.sub(lambda m: m.group(1) + \"****\" + (m.group(2) if m.lastindex == 2 else \"\"), message)\n        record.msg, record.args = message, None\n        return True\n",[54,1216,1217,1224,1231,1235,1245,1283,1332,1370,1375,1379,1383,1402,1420,1434,1475,1479,1493,1503,1517,1533,1547,1604,1617],{"__ignoreMap":183},[187,1218,1219,1221],{"class":189,"line":190},[187,1220,225],{"class":200},[187,1222,1223],{"class":211}," logging\n",[187,1225,1226,1228],{"class":189,"line":197},[187,1227,225],{"class":200},[187,1229,1230],{"class":211}," re\n",[187,1232,1233],{"class":189,"line":215},[187,1234,219],{"emptyLinePlaceholder":218},[187,1236,1237,1240,1242],{"class":189,"line":222},[187,1238,1239],{"class":204},"TOKEN_PATTERNS",[187,1241,310],{"class":200},[187,1243,1244],{"class":211}," [\n",[187,1246,1247,1250,1253,1256,1258,1262,1265,1268,1271,1273,1275,1278,1280],{"class":189,"line":231},[187,1248,1249],{"class":211},"    re.compile(",[187,1251,1252],{"class":200},"r",[187,1254,1255],{"class":313},"\"",[187,1257,574],{"class":204},[187,1259,1261],{"class":1260},"sA_wV","Bearer",[187,1263,1264],{"class":204},"\\s",[187,1266,1267],{"class":200},"+",[187,1269,1270],{"class":204},")[A-Za-z0-9._~+\u002F-]",[187,1272,1267],{"class":200},[187,1274,405],{"class":1260},[187,1276,1277],{"class":200},"*",[187,1279,1255],{"class":313},[187,1281,1282],{"class":211},"),\n",[187,1284,1285,1287,1289,1291,1293,1296,1299,1302,1305,1307,1310,1313,1315,1318,1320,1322,1325,1328,1330],{"class":189,"line":239},[187,1286,1249],{"class":211},[187,1288,1252],{"class":200},[187,1290,1255],{"class":313},[187,1292,574],{"class":204},[187,1294,1295],{"class":1260},":\u002F\u002F",[187,1297,1298],{"class":204},"[",[187,1300,1301],{"class":200},"^",[187,1303,1304],{"class":204},":\u002F\\s]",[187,1306,1267],{"class":200},[187,1308,1309],{"class":1260},":",[187,1311,1312],{"class":204},")[",[187,1314,1301],{"class":200},[187,1316,1317],{"class":204},"@\\s]",[187,1319,1267],{"class":200},[187,1321,574],{"class":204},[187,1323,1324],{"class":1260},"@",[187,1326,1327],{"class":204},")",[187,1329,1255],{"class":313},[187,1331,1282],{"class":211},[187,1333,1334,1336,1338,1340,1343,1346,1348,1351,1353,1356,1358,1361,1364,1366,1368],{"class":189,"line":247},[187,1335,1249],{"class":211},[187,1337,1252],{"class":200},[187,1339,1255],{"class":313},[187,1341,1342],{"class":204},"\\b(",[187,1344,1345],{"class":1260},"ghp_",[187,1347,641],{"class":200},[187,1349,1350],{"class":1260},"gho_",[187,1352,641],{"class":200},[187,1354,1355],{"class":1260},"xoxb-",[187,1357,641],{"class":200},[187,1359,1360],{"class":1260},"xoxp-",[187,1362,1363],{"class":204},")[A-Za-z0-9-]",[187,1365,1267],{"class":200},[187,1367,1255],{"class":313},[187,1369,1282],{"class":211},[187,1371,1372],{"class":189,"line":260},[187,1373,1374],{"class":211},"]\n",[187,1376,1377],{"class":189,"line":273},[187,1378,219],{"emptyLinePlaceholder":218},[187,1380,1381],{"class":189,"line":278},[187,1382,219],{"emptyLinePlaceholder":218},[187,1384,1385,1387,1390,1392,1395,1397,1400],{"class":189,"line":286},[187,1386,330],{"class":200},[187,1388,1389],{"class":333}," RedactingFilter",[187,1391,574],{"class":211},[187,1393,1394],{"class":333},"logging",[187,1396,967],{"class":211},[187,1398,1399],{"class":333},"Filter",[187,1401,605],{"class":211},[187,1403,1404,1406,1408,1411,1413,1416,1418],{"class":189,"line":299},[187,1405,376],{"class":200},[187,1407,379],{"class":204},[187,1409,1410],{"class":211},"(self, secrets: list[",[187,1412,385],{"class":204},[187,1414,1415],{"class":211},"]) -> ",[187,1417,391],{"class":204},[187,1419,337],{"class":211},[187,1421,1422,1425,1428,1431],{"class":189,"line":304},[187,1423,1424],{"class":204},"        super",[187,1426,1427],{"class":211},"().",[187,1429,1430],{"class":204},"__init__",[187,1432,1433],{"class":211},"()\n",[187,1435,1436,1438,1441,1443,1446,1449,1452,1455,1458,1461,1464,1467,1470,1473],{"class":189,"line":317},[187,1437,399],{"class":204},[187,1439,1440],{"class":211},".secrets ",[187,1442,405],{"class":200},[187,1444,1445],{"class":211}," [s ",[187,1447,1448],{"class":200},"for",[187,1450,1451],{"class":211}," s ",[187,1453,1454],{"class":200},"in",[187,1456,1457],{"class":211}," secrets ",[187,1459,1460],{"class":200},"if",[187,1462,1463],{"class":204}," len",[187,1465,1466],{"class":211},"(s) ",[187,1468,1469],{"class":200},">=",[187,1471,1472],{"class":204}," 6",[187,1474,1374],{"class":211},[187,1476,1477],{"class":189,"line":322},[187,1478,219],{"emptyLinePlaceholder":218},[187,1480,1481,1483,1486,1489,1491],{"class":189,"line":327},[187,1482,376],{"class":200},[187,1484,1485],{"class":204}," filter",[187,1487,1488],{"class":211},"(self, record: logging.LogRecord) -> ",[187,1490,518],{"class":204},[187,1492,337],{"class":211},[187,1494,1495,1498,1500],{"class":189,"line":340},[187,1496,1497],{"class":211},"        message ",[187,1499,405],{"class":200},[187,1501,1502],{"class":211}," record.getMessage()\n",[187,1504,1505,1508,1510,1512,1514],{"class":189,"line":346},[187,1506,1507],{"class":200},"        for",[187,1509,1451],{"class":211},[187,1511,1454],{"class":200},[187,1513,437],{"class":204},[187,1515,1516],{"class":211},".secrets:\n",[187,1518,1519,1522,1524,1527,1530],{"class":189,"line":351},[187,1520,1521],{"class":211},"            message ",[187,1523,405],{"class":200},[187,1525,1526],{"class":211}," message.replace(s, ",[187,1528,1529],{"class":313},"\"****\"",[187,1531,1532],{"class":211},")\n",[187,1534,1535,1537,1540,1542,1545],{"class":189,"line":368},[187,1536,1507],{"class":200},[187,1538,1539],{"class":211}," pattern ",[187,1541,1454],{"class":200},[187,1543,1544],{"class":204}," TOKEN_PATTERNS",[187,1546,337],{"class":211},[187,1548,1549,1551,1553,1556,1559,1562,1565,1568,1570,1573,1576,1579,1582,1584,1586,1589,1592,1595,1598,1601],{"class":189,"line":373},[187,1550,1521],{"class":211},[187,1552,405],{"class":200},[187,1554,1555],{"class":211}," pattern.sub(",[187,1557,1558],{"class":200},"lambda",[187,1560,1561],{"class":211}," m: m.group(",[187,1563,1564],{"class":204},"1",[187,1566,1567],{"class":211},") ",[187,1569,1267],{"class":200},[187,1571,1572],{"class":313}," \"****\"",[187,1574,1575],{"class":200}," +",[187,1577,1578],{"class":211}," (m.group(",[187,1580,1581],{"class":204},"2",[187,1583,1567],{"class":211},[187,1585,1460],{"class":200},[187,1587,1588],{"class":211}," m.lastindex ",[187,1590,1591],{"class":200},"==",[187,1593,1594],{"class":204}," 2",[187,1596,1597],{"class":200}," else",[187,1599,1600],{"class":313}," \"\"",[187,1602,1603],{"class":211},"), message)\n",[187,1605,1606,1609,1611,1614],{"class":189,"line":396},[187,1607,1608],{"class":211},"        record.msg, record.args ",[187,1610,405],{"class":200},[187,1612,1613],{"class":211}," message, ",[187,1615,1616],{"class":204},"None\n",[187,1618,1619,1621],{"class":189,"line":411},[187,1620,434],{"class":200},[187,1622,1623],{"class":204}," True\n",[10,1625,1626,1627,1631,1632,1635,1636,1639,1640,967],{},"Attach it to the handlers, not a single logger, so it covers third-party libraries as well as your own code. ",[17,1628,1630],{"href":1629},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs\u002F","Redacting secrets from CLI output and logs"," builds it out with tests, covers tracebacks and Rich's ",[54,1633,1634],{},"show_locals",", and deals with the ",[54,1637,1638],{},"--debug"," flag users turn on right before they file a bug report. The logging setup it plugs into is described in ",[17,1641,1643],{"href":1642},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002F","structured logging for CLI apps",[32,1645,1647],{"id":1646},"secrets-in-ci-pipelines","Secrets in CI pipelines",[10,1649,1650,1651,1654],{},"CI is where a CLI's credential handling is tested hardest, because everything the tool prints is stored in a build log that many people can read, often for months. Most CI systems mask the values of secrets they injected — GitHub Actions replaces registered secrets with ",[54,1652,1653],{},"***"," in logs — but that masking is string matching on output, and it fails in predictable ways:",[37,1656,1657,1671,1677],{},[40,1658,1659,1662,1663,1666,1667,1670],{},[43,1660,1661],{},"Transformed secrets are not masked."," If your tool base64-encodes a token, URL-encodes it, or prints only part of it, the CI system does not recognise it. A ",[54,1664,1665],{},"Basic"," auth header is the base64 of ",[54,1668,1669],{},"user:password"," and will appear in full.",[40,1672,1673,1676],{},[43,1674,1675],{},"Derived secrets are unknown to CI."," A short-lived token your CLI obtains by exchanging the injected one was never registered, so it is printed verbatim if it is printed at all.",[40,1678,1679,1682],{},[43,1680,1681],{},"Multi-line secrets mask badly."," Private keys and JSON service-account files are often split across lines in ways the masker misses.",[10,1684,1685,1686,1688,1689,1692],{},"The defences are the ones already described, applied without exceptions: never print a credential in any form, keep debug HTTP logging behind an explicit flag with redaction on, and wrap derived tokens in the ",[54,1687,977],{}," type as soon as you receive them. For GitHub Actions specifically, a tool can register a derived value for masking by printing ",[54,1690,1691],{},"::add-mask::\u003Cvalue>"," to stdout before anything else could print it — useful when your CLI is designed to run in Actions and exchanges tokens.",[10,1694,1695,1696,109,1698,1700,1701,1703,1704,967],{},"Finally, give automation a way to use the tool without any interactive or stored state: every command should work with only ",[54,1697,66],{},[54,1699,70],{}," set, and ",[54,1702,1188],{}," (or detection of a non-terminal stdin) should turn any would-be prompt into an immediate, explanatory failure. The pipeline configuration side of this lives in ",[17,1705,1707],{"href":1706},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002F","CI\u002FCD pipelines for Python CLIs",[32,1709,1711],{"id":1710},"several-accounts-one-tool","Several accounts, one tool",[10,1713,1714,1715,1718,1719,1722,1723,1726,1727,1731],{},"Developers routinely have more than one identity for the same service: personal and work, staging and production, several customer tenants. A tool that supports only one credential forces them to log out and in repeatedly, and makes it far too easy to run a command against the wrong one. ",[43,1716,1717],{},"Profiles"," solve both: named bundles of non-secret settings (URL, account, region) in the config file, each with its own token in the keychain under a key derived from the profile name, selected with the usual precedence — ",[54,1720,1721],{},"--profile"," flag, ",[54,1724,1725],{},"MYTOOL_PROFILE"," environment variable, configured default. ",[17,1728,1730],{"href":1729},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts\u002F","Supporting multiple profiles and accounts"," implements it, including a \"protected\" marker that makes destructive commands against production ask for confirmation.",[32,1733,1735],{"id":1734},"testing-credential-handling","Testing credential handling",[10,1737,1738,1739,1741,1742,1745],{},"Credential code is security-relevant, so it deserves direct tests — and those tests must never touch the real keychain or environment. ",[54,1740,56],{}," can be pointed at an in-memory backend, and ",[54,1743,1744],{},"monkeypatch"," controls the environment:",[178,1747,1749],{"className":180,"code":1748,"language":182,"meta":183,"style":183},"import keyring\nimport pytest\nfrom keyring.backend import KeyringBackend\n\nfrom mytool.credentials import NoCredential, Secret, resolve_token\n\n\nclass MemoryKeyring(KeyringBackend):\n    priority = 1\n\n    def __init__(self) -> None:\n        self.store: dict[tuple[str, str], str] = {}\n\n    def get_password(self, service, username):\n        return self.store.get((service, username))\n\n    def set_password(self, service, username, password):\n        self.store[(service, username)] = password\n\n    def delete_password(self, service, username):\n        self.store.pop((service, username), None)\n\n\n@pytest.fixture(autouse=True)\ndef clean(monkeypatch):\n    monkeypatch.delenv(\"MYTOOL_TOKEN\", raising=False)\n    monkeypatch.delenv(\"MYTOOL_TOKEN_FILE\", raising=False)\n    kr = MemoryKeyring()\n    keyring.set_keyring(kr)\n    return kr\n\n\ndef test_env_beats_keychain(monkeypatch, clean):\n    clean.set_password(\"mytool\", \"prod\", \"from-keychain\")\n    monkeypatch.setenv(\"MYTOOL_TOKEN\", \"from-env\")\n    token, source = resolve_token(\"prod\", allow_prompt=False)\n    assert token.reveal() == \"from-env\" and source == \"MYTOOL_TOKEN\"\n\n\ndef test_secret_never_prints():\n    s = Secret(\"hunter2hunter2\")\n    assert \"hunter2\" not in f\"{s} {s!r} {[s]}\"\n\n\ndef test_missing_credential_explains(clean):\n    with pytest.raises(NoCredential, match=\"MYTOOL_TOKEN\"):\n        resolve_token(\"prod\", allow_prompt=False)\n",[54,1750,1751,1757,1764,1776,1780,1792,1796,1800,1814,1824,1828,1840,1866,1870,1880,1889,1893,1903,1915,1919,1928,1939,1943,1947,1964,1974,1993,2009,2019,2024,2032,2036,2040,2050,2068,2082,2105,2129,2133,2137,2147,2162,2204,2208,2212,2222,2239],{"__ignoreMap":183},[187,1752,1753,1755],{"class":189,"line":190},[187,1754,225],{"class":200},[187,1756,283],{"class":211},[187,1758,1759,1761],{"class":189,"line":197},[187,1760,225],{"class":200},[187,1762,1763],{"class":211}," pytest\n",[187,1765,1766,1768,1771,1773],{"class":189,"line":215},[187,1767,201],{"class":200},[187,1769,1770],{"class":211}," keyring.backend ",[187,1772,225],{"class":200},[187,1774,1775],{"class":211}," KeyringBackend\n",[187,1777,1778],{"class":189,"line":222},[187,1779,219],{"emptyLinePlaceholder":218},[187,1781,1782,1784,1787,1789],{"class":189,"line":231},[187,1783,201],{"class":200},[187,1785,1786],{"class":211}," mytool.credentials ",[187,1788,225],{"class":200},[187,1790,1791],{"class":211}," NoCredential, Secret, resolve_token\n",[187,1793,1794],{"class":189,"line":239},[187,1795,219],{"emptyLinePlaceholder":218},[187,1797,1798],{"class":189,"line":247},[187,1799,219],{"emptyLinePlaceholder":218},[187,1801,1802,1804,1807,1809,1812],{"class":189,"line":260},[187,1803,330],{"class":200},[187,1805,1806],{"class":333}," MemoryKeyring",[187,1808,574],{"class":211},[187,1810,1811],{"class":333},"KeyringBackend",[187,1813,605],{"class":211},[187,1815,1816,1819,1821],{"class":189,"line":273},[187,1817,1818],{"class":211},"    priority ",[187,1820,405],{"class":200},[187,1822,1823],{"class":204}," 1\n",[187,1825,1826],{"class":189,"line":278},[187,1827,219],{"emptyLinePlaceholder":218},[187,1829,1830,1832,1834,1836,1838],{"class":189,"line":286},[187,1831,376],{"class":200},[187,1833,379],{"class":204},[187,1835,424],{"class":211},[187,1837,391],{"class":204},[187,1839,337],{"class":211},[187,1841,1842,1844,1847,1849,1851,1853,1856,1858,1861,1863],{"class":189,"line":299},[187,1843,399],{"class":204},[187,1845,1846],{"class":211},".store: dict[tuple[",[187,1848,385],{"class":204},[187,1850,958],{"class":211},[187,1852,385],{"class":204},[187,1854,1855],{"class":211},"], ",[187,1857,385],{"class":204},[187,1859,1860],{"class":211},"] ",[187,1862,405],{"class":200},[187,1864,1865],{"class":211}," {}\n",[187,1867,1868],{"class":189,"line":304},[187,1869,219],{"emptyLinePlaceholder":218},[187,1871,1872,1874,1877],{"class":189,"line":317},[187,1873,376],{"class":200},[187,1875,1876],{"class":333}," get_password",[187,1878,1879],{"class":211},"(self, service, username):\n",[187,1881,1882,1884,1886],{"class":189,"line":322},[187,1883,434],{"class":200},[187,1885,437],{"class":204},[187,1887,1888],{"class":211},".store.get((service, username))\n",[187,1890,1891],{"class":189,"line":327},[187,1892,219],{"emptyLinePlaceholder":218},[187,1894,1895,1897,1900],{"class":189,"line":340},[187,1896,376],{"class":200},[187,1898,1899],{"class":333}," set_password",[187,1901,1902],{"class":211},"(self, service, username, password):\n",[187,1904,1905,1907,1910,1912],{"class":189,"line":346},[187,1906,399],{"class":204},[187,1908,1909],{"class":211},".store[(service, username)] ",[187,1911,405],{"class":200},[187,1913,1914],{"class":211}," password\n",[187,1916,1917],{"class":189,"line":351},[187,1918,219],{"emptyLinePlaceholder":218},[187,1920,1921,1923,1926],{"class":189,"line":368},[187,1922,376],{"class":200},[187,1924,1925],{"class":333}," delete_password",[187,1927,1879],{"class":211},[187,1929,1930,1932,1935,1937],{"class":189,"line":373},[187,1931,399],{"class":204},[187,1933,1934],{"class":211},".store.pop((service, username), ",[187,1936,391],{"class":204},[187,1938,1532],{"class":211},[187,1940,1941],{"class":189,"line":396},[187,1942,219],{"emptyLinePlaceholder":218},[187,1944,1945],{"class":189,"line":411},[187,1946,219],{"emptyLinePlaceholder":218},[187,1948,1949,1952,1954,1957,1959,1962],{"class":189,"line":416},[187,1950,1951],{"class":333},"@pytest.fixture",[187,1953,574],{"class":211},[187,1955,1956],{"class":708},"autouse",[187,1958,405],{"class":200},[187,1960,1961],{"class":204},"True",[187,1963,1532],{"class":211},[187,1965,1966,1968,1971],{"class":189,"line":431},[187,1967,627],{"class":200},[187,1969,1970],{"class":333}," clean",[187,1972,1973],{"class":211},"(monkeypatch):\n",[187,1975,1976,1979,1981,1983,1986,1988,1991],{"class":189,"line":443},[187,1977,1978],{"class":211},"    monkeypatch.delenv(",[187,1980,788],{"class":313},[187,1982,958],{"class":211},[187,1984,1985],{"class":708},"raising",[187,1987,405],{"class":200},[187,1989,1990],{"class":204},"False",[187,1992,1532],{"class":211},[187,1994,1995,1997,1999,2001,2003,2005,2007],{"class":189,"line":448},[187,1996,1978],{"class":211},[187,1998,752],{"class":313},[187,2000,958],{"class":211},[187,2002,1985],{"class":708},[187,2004,405],{"class":200},[187,2006,1990],{"class":204},[187,2008,1532],{"class":211},[187,2010,2011,2014,2016],{"class":189,"line":462},[187,2012,2013],{"class":211},"    kr ",[187,2015,405],{"class":200},[187,2017,2018],{"class":211}," MemoryKeyring()\n",[187,2020,2021],{"class":189,"line":470},[187,2022,2023],{"class":211},"    keyring.set_keyring(kr)\n",[187,2025,2026,2029],{"class":189,"line":475},[187,2027,2028],{"class":200},"    return",[187,2030,2031],{"class":211}," kr\n",[187,2033,2034],{"class":189,"line":489},[187,2035,219],{"emptyLinePlaceholder":218},[187,2037,2038],{"class":189,"line":497},[187,2039,219],{"emptyLinePlaceholder":218},[187,2041,2042,2044,2047],{"class":189,"line":502},[187,2043,627],{"class":200},[187,2045,2046],{"class":333}," test_env_beats_keychain",[187,2048,2049],{"class":211},"(monkeypatch, clean):\n",[187,2051,2052,2055,2057,2059,2061,2063,2066],{"class":189,"line":523},[187,2053,2054],{"class":211},"    clean.set_password(",[187,2056,1057],{"class":313},[187,2058,958],{"class":211},[187,2060,1062],{"class":313},[187,2062,958],{"class":211},[187,2064,2065],{"class":313},"\"from-keychain\"",[187,2067,1532],{"class":211},[187,2069,2070,2073,2075,2077,2080],{"class":189,"line":546},[187,2071,2072],{"class":211},"    monkeypatch.setenv(",[187,2074,788],{"class":313},[187,2076,958],{"class":211},[187,2078,2079],{"class":313},"\"from-env\"",[187,2081,1532],{"class":211},[187,2083,2084,2087,2089,2092,2094,2096,2099,2101,2103],{"class":189,"line":551},[187,2085,2086],{"class":211},"    token, source ",[187,2088,405],{"class":200},[187,2090,2091],{"class":211}," resolve_token(",[187,2093,1062],{"class":313},[187,2095,958],{"class":211},[187,2097,2098],{"class":708},"allow_prompt",[187,2100,405],{"class":200},[187,2102,1990],{"class":204},[187,2104,1532],{"class":211},[187,2106,2107,2110,2113,2115,2118,2121,2124,2126],{"class":189,"line":566},[187,2108,2109],{"class":200},"    assert",[187,2111,2112],{"class":211}," token.reveal() ",[187,2114,1591],{"class":200},[187,2116,2117],{"class":313}," \"from-env\"",[187,2119,2120],{"class":200}," and",[187,2122,2123],{"class":211}," source ",[187,2125,1591],{"class":200},[187,2127,2128],{"class":313}," \"MYTOOL_TOKEN\"\n",[187,2130,2131],{"class":189,"line":582},[187,2132,219],{"emptyLinePlaceholder":218},[187,2134,2135],{"class":189,"line":587},[187,2136,219],{"emptyLinePlaceholder":218},[187,2138,2139,2141,2144],{"class":189,"line":592},[187,2140,627],{"class":200},[187,2142,2143],{"class":333}," test_secret_never_prints",[187,2145,2146],{"class":211},"():\n",[187,2148,2149,2152,2154,2157,2160],{"class":189,"line":608},[187,2150,2151],{"class":211},"    s ",[187,2153,405],{"class":200},[187,2155,2156],{"class":211}," Secret(",[187,2158,2159],{"class":313},"\"hunter2hunter2\"",[187,2161,1532],{"class":211},[187,2163,2164,2166,2169,2171,2174,2177,2179,2181,2184,2186,2189,2191,2193,2195,2197,2200,2202],{"class":189,"line":614},[187,2165,2109],{"class":200},[187,2167,2168],{"class":313}," \"hunter2\"",[187,2170,693],{"class":200},[187,2172,2173],{"class":200}," in",[187,2175,2176],{"class":200}," f",[187,2178,1255],{"class":313},[187,2180,726],{"class":204},[187,2182,2183],{"class":211},"s",[187,2185,732],{"class":204},[187,2187,2188],{"class":204}," {",[187,2190,2183],{"class":211},[187,2192,890],{"class":200},[187,2194,732],{"class":204},[187,2196,2188],{"class":204},[187,2198,2199],{"class":211},"[s]",[187,2201,732],{"class":204},[187,2203,735],{"class":313},[187,2205,2206],{"class":189,"line":619},[187,2207,219],{"emptyLinePlaceholder":218},[187,2209,2210],{"class":189,"line":624},[187,2211,219],{"emptyLinePlaceholder":218},[187,2213,2214,2216,2219],{"class":189,"line":654},[187,2215,627],{"class":200},[187,2217,2218],{"class":333}," test_missing_credential_explains",[187,2220,2221],{"class":211},"(clean):\n",[187,2223,2224,2227,2230,2233,2235,2237],{"class":189,"line":675},[187,2225,2226],{"class":200},"    with",[187,2228,2229],{"class":211}," pytest.raises(NoCredential, ",[187,2231,2232],{"class":708},"match",[187,2234,405],{"class":200},[187,2236,788],{"class":313},[187,2238,605],{"class":211},[187,2240,2241,2244,2246,2248,2250,2252,2254],{"class":189,"line":681},[187,2242,2243],{"class":211},"        resolve_token(",[187,2245,1062],{"class":313},[187,2247,958],{"class":211},[187,2249,2098],{"class":708},[187,2251,405],{"class":200},[187,2253,1990],{"class":204},[187,2255,1532],{"class":211},[32,2257,2259],{"id":2258},"key-takeaways","Key takeaways",[37,2261,2262,2272,2278,2281,2284,2287,2290],{},[40,2263,2264,2265,2267,2268,2271],{},"Never take secrets as plain arguments; offer env vars, ",[54,2266,1148],{}," variables, ",[54,2269,2270],{},"--*-stdin"," flags and prompts.",[40,2273,2274,2275,2277],{},"Keep tokens in the system keychain via ",[54,2276,56],{},", with a deliberate fallback for headless machines.",[40,2279,2280],{},"Resolve credentials from an ordered list of sources and report which one won.",[40,2282,2283],{},"Wrap secrets in a type that masks itself, and unwrap only where the value is used.",[40,2285,2286],{},"Add a redacting logging filter for everything other libraries print.",[40,2288,2289],{},"Pass child processes only the environment they need.",[40,2291,2292],{},"Support named profiles, show which is active, and add friction for production.",[32,2294,2296],{"id":2295},"frequently-asked-questions","Frequently asked questions",[2298,2299,2301],"h3",{"id":2300},"is-an-environment-variable-really-safer-than-a-command-line-argument","Is an environment variable really safer than a command-line argument?",[10,2303,2304,2305,2307],{},"Yes, meaningfully. Arguments are readable by every user on the machine and land in shell history and CI logs; a process's environment is readable only by the same user (and root) and is not recorded in history. Files with restrictive permissions are safer still, which is why the ",[54,2306,1148],{}," convention exists.",[2298,2309,2311],{"id":2310},"should-my-cli-encrypt-its-config-file-instead-of-using-a-keychain","Should my CLI encrypt its config file instead of using a keychain?",[10,2313,2314],{},"Encrypting a file requires a key, and the key has to live somewhere — usually right next to the file, which protects nothing. The operating system's keychain solves the key problem with the user's login. Use a file-based encrypted store only where no keychain exists, and be honest in the docs about what it protects against.",[2298,2316,2318],{"id":2317},"how-long-should-stored-tokens-live","How long should stored tokens live?",[10,2320,2321,2322,2326,2327,2329],{},"As short as the API allows while remaining convenient: prefer short-lived access tokens with a refresh token (as the ",[17,2323,2325],{"href":2324},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis\u002F","OAuth device flow"," provides) over a long-lived personal access token. Show the expiry in ",[54,2328,1119],{}," so users are not surprised.",[2298,2331,2333],{"id":2332},"what-should-happen-when-a-token-is-rejected","What should happen when a token is rejected?",[10,2335,2336],{},"Say so clearly, name the profile and the source of the token, and tell the user how to fix it (\"token from keychain for profile prod was rejected — run: mytool auth login --profile prod\"). Do not silently fall through to the next source; that hides the real problem.",[2298,2338,2340],{"id":2339},"do-i-need-to-worry-about-secrets-in-memory","Do I need to worry about secrets in memory?",[10,2342,2343],{},"For a typical CLI, no. Python cannot reliably wipe strings from memory, and an attacker who can read your process memory has already won. Focus on the channels above, which are where real leaks happen.",[32,2345,2347],{"id":2346},"related","Related",[37,2349,2350,2355,2361,2366,2371,2375,2379,2385,2391],{},[40,2351,2352,2353],{},"Up: ",[17,2354,20],{"href":19},[40,2356,2357,2358],{},"Down: ",[17,2359,2360],{"href":956},"Storing tokens with keyring",[40,2362,2357,2363],{},[17,2364,2365],{"href":961},"Reading secrets from env and files",[40,2367,2357,2368],{},[17,2369,2370],{"href":965},"Prompting for passwords securely",[40,2372,2357,2373],{},[17,2374,1630],{"href":1629},[40,2376,2357,2377],{},[17,2378,1730],{"href":1729},[40,2380,2357,2381],{},[17,2382,2384],{"href":2383},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically\u002F","Refreshing Expired Tokens Automatically in a Python CLI",[40,2386,2387,2388],{},"Sideways: ",[17,2389,2390],{"href":24},"Calling HTTP APIs from Python CLIs",[40,2392,2387,2393],{},[17,2394,2396],{"href":2395},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002F","Handling configuration files and environment variables",[2398,2399,2400],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":183,"searchDepth":197,"depth":197,"links":2402},[2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2422],{"id":34,"depth":197,"text":35},{"id":92,"depth":197,"text":93},{"id":169,"depth":197,"text":170},{"id":970,"depth":197,"text":971},{"id":1027,"depth":197,"text":1028},{"id":1126,"depth":197,"text":1127},{"id":1169,"depth":197,"text":1170},{"id":1197,"depth":197,"text":1198},{"id":1646,"depth":197,"text":1647},{"id":1710,"depth":197,"text":1711},{"id":1734,"depth":197,"text":1735},{"id":2258,"depth":197,"text":2259},{"id":2295,"depth":197,"text":2296,"children":2416},[2417,2418,2419,2420,2421],{"id":2300,"depth":215,"text":2301},{"id":2310,"depth":215,"text":2311},{"id":2317,"depth":215,"text":2318},{"id":2332,"depth":215,"text":2333},{"id":2339,"depth":215,"text":2340},{"id":2346,"depth":197,"text":2347},"2026-09-18","Handle tokens and passwords in Python CLIs safely: keychain storage, env vars and secret files, secure prompts, redaction in logs, and multiple account profiles.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis",{"title":5,"description":2424},"cli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Findex",[2433,2434,2435,56],"security","secrets","credentials","nSzt1iYTNxXVbEQPzFfnWMRpAehJarEbPDiUvMIAjp8",[2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600,2603,2606,2609,2612,2615,2618,2621,2624,2627,2630,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660,2663,2666,2669,2672,2675,2678,2681,2684,2687,2689,2692,2695,2698,2701,2704,2707,2710,2713,2716,2719,2722,2725,2728,2731,2734,2737,2740,2743,2746,2749,2752,2755,2758,2761,2764,2767,2770,2773,2776,2779,2782,2785,2788,2791,2794,2797,2800,2803,2806,2809,2812,2815,2818,2819,2822,2825,2828,2830,2833,2836,2839,2842,2845,2848,2851,2854,2857,2860,2863,2866,2869,2872,2875,2878,2881,2884,2887,2890,2893,2896,2899,2902,2905,2908,2911,2914,2917,2920,2923,2926,2929,2932,2935,2938,2941,2944,2947,2950,2953,2956,2959,2962,2965,2968,2971,2974,2977,2980,2983,2986,2989,2992,2995,2998,3001,3004,3007,3010,3013,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3166,3169,3172,3175,3178,3181,3184,3187,3190,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3232,3235,3238,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277],{"path":2439,"title":2440},"\u002Fabout","About Python CLI Toolcraft",{"path":2442,"title":2443},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2445,"title":2446},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2448,"title":2449},"\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":2451,"title":2452},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2454,"title":2455},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2457,"title":2458},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2460,"title":2461},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2463,"title":2464},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2466,"title":2467},"\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":2469,"title":2470},"\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":2472,"title":2473},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2475,"title":2476},"\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":2478,"title":2479},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2481,"title":2482},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2484,"title":2485},"\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":2487,"title":2488},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2490,"title":2491},"\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":2493,"title":2494},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2496,"title":2497},"\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":2499,"title":2500},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2502,"title":2503},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2505,"title":2506},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2508,"title":2509},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2511,"title":2512},"\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":2514,"title":2515},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2517,"title":2518},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2520,"title":2521},"\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":2523,"title":2524},"\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":2526,"title":2527},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2529,"title":2530},"\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":2532,"title":2533},"\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":2535,"title":2536},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2538,"title":2539},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2541,"title":2542},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2544,"title":2545},"\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":2547,"title":2548},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2550,"title":2551},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2553,"title":2554},"\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":2556,"title":2557},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2559,"title":2560},"\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":2562,"title":2563},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2565,"title":2566},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2568,"title":2569},"\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":2571,"title":2572},"\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":2574,"title":2575},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2577,"title":2578},"\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":2580,"title":2581},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2583,"title":2584},"\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":2586,"title":2587},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2589,"title":2590},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2592,"title":2593},"\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":2595,"title":2596},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2598,"title":2599},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2601,"title":2602},"\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":2604,"title":2605},"\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":2607,"title":2608},"\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":2610,"title":2611},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2613,"title":2614},"\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":2616,"title":2617},"\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":2619,"title":2620},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2622,"title":2623},"\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":2625,"title":2626},"\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":2628,"title":2629},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2631,"title":2632},"\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":2634,"title":2635},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2637,"title":2638},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2640,"title":2641},"\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":2643,"title":2644},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2646,"title":2647},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2649,"title":2650},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2652,"title":2653},"\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":2655,"title":2656},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2658,"title":2659},"\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":2661,"title":2662},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2664,"title":2665},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2667,"title":2668},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2670,"title":2671},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2673,"title":2674},"\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":2676,"title":2677},"\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":2679,"title":2680},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2682,"title":2683},"\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":2685,"title":2686},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2688,"title":2390},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis",{"path":2690,"title":2691},"\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":2693,"title":2694},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2696,"title":2697},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2699,"title":2700},"\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":2702,"title":2703},"\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":2705,"title":2706},"\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":2708,"title":2709},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2711,"title":2712},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2714,"title":2715},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2717,"title":2718},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2720,"title":2721},"\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":2723,"title":2724},"\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":2726,"title":2727},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2729,"title":2730},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2732,"title":2733},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2735,"title":2736},"\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":2738,"title":2739},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2741,"title":2742},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2744,"title":2745},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2747,"title":2748},"\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":2750,"title":2751},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2753,"title":2754},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2756,"title":2757},"\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":2759,"title":2760},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2762,"title":2763},"\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":2765,"title":2766},"\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":2768,"title":2769},"\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":2771,"title":2772},"\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":2774,"title":2775},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2777,"title":2778},"\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":2780,"title":2781},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2783,"title":2784},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2786,"title":2787},"\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":2789,"title":2790},"\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":2792,"title":2793},"\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":2795,"title":2796},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2798,"title":2799},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2801,"title":2802},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2804,"title":2805},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2807,"title":2808},"\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":2810,"title":2811},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2813,"title":2814},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2816,"title":2817},"\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":2429,"title":5},{"path":2820,"title":2821},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2823,"title":2824},"\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":2826,"title":2827},"\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":2829,"title":2384},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically",{"path":2831,"title":2832},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2834,"title":2835},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2837,"title":2838},"\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":2840,"title":2841},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2843,"title":2844},"\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":2846,"title":2847},"\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":2849,"title":2850},"\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":2852,"title":2853},"\u002F","Python CLI Toolcraft",{"path":2855,"title":2856},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2858,"title":2859},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2861,"title":2862},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2864,"title":2865},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2867,"title":2868},"\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":2870,"title":2871},"\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":2873,"title":2874},"\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":2876,"title":2877},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2879,"title":2880},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2882,"title":2883},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2885,"title":2886},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2888,"title":2889},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2891,"title":2892},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2894,"title":2895},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2897,"title":2898},"\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":2900,"title":2901},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2903,"title":2904},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2906,"title":2907},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2909,"title":2910},"\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":2912,"title":2913},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2915,"title":2916},"\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":2918,"title":2919},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2921,"title":2922},"\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":2924,"title":2925},"\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":2927,"title":2928},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2930,"title":2931},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2933,"title":2934},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2936,"title":2937},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2939,"title":2940},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2942,"title":2943},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2945,"title":2946},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2948,"title":2949},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2951,"title":2952},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2954,"title":2955},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2957,"title":2958},"\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":2960,"title":2961},"\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":2963,"title":2964},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2966,"title":2967},"\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":2969,"title":2970},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2972,"title":2973},"\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":2975,"title":2976},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2978,"title":2979},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2981,"title":2982},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2984,"title":2985},"\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":2987,"title":2988},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2990,"title":2991},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2993,"title":2994},"\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":2996,"title":2997},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2999,"title":3000},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3002,"title":3003},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3005,"title":3006},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":3008,"title":3009},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3011,"title":3012},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":3014,"title":3015},"\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":3017,"title":3018},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3020,"title":3021},"\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":3023,"title":3024},"\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":3026,"title":3027},"\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":3029,"title":3030},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3032,"title":3033},"\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":3035,"title":3036},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3038,"title":3039},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3041,"title":3042},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3044,"title":3045},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3047,"title":3048},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3050,"title":3051},"\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":3053,"title":3054},"\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":3056,"title":3057},"\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":3059,"title":3060},"\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":3062,"title":3063},"\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":3065,"title":3066},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3068,"title":3069},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3071,"title":3072},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3074,"title":3075},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3077,"title":3078},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3080,"title":3081},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3083,"title":3084},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3086,"title":3087},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3089,"title":3090},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3092,"title":3093},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3095,"title":3096},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3098,"title":3099},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3101,"title":3102},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3104,"title":3105},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3107,"title":3108},"\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":3110,"title":3111},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3113,"title":3114},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3116,"title":3117},"\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":3119,"title":3120},"\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":3122,"title":3123},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3125,"title":3126},"\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":3128,"title":3129},"\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":3131,"title":3132},"\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":3134,"title":3135},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3137,"title":3138},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3140,"title":3141},"\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":3143,"title":3144},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3146,"title":3147},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3149,"title":3150},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3152,"title":3153},"\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":3155,"title":3156},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3158,"title":3159},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3161,"title":3162},"\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":3164,"title":3165},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3167,"title":3168},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3170,"title":3171},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3173,"title":3174},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3176,"title":3177},"\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":3179,"title":3180},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3182,"title":3183},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3185,"title":3186},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3188,"title":3189},"\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":3191,"title":3192},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3194,"title":3195},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3197,"title":3198},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3200,"title":3201},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3203,"title":3204},"\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":3206,"title":3207},"\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":3209,"title":3210},"\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":3212,"title":3213},"\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":3215,"title":3216},"\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":3218,"title":3219},"\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":3221,"title":3222},"\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":3224,"title":3225},"\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":3227,"title":3228},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3230,"title":3231},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3233,"title":3234},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3236,"title":3237},"\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":3239,"title":3240},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3242,"title":3243},"\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":3245,"title":3246},"\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":3248,"title":3249},"\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":3251,"title":3252},"\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":3254,"title":3255},"\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":3257,"title":3258},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3260,"title":3261},"\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":3263,"title":3264},"\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":3266,"title":3267},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3269,"title":3270},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3272,"title":3273},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3275,"title":3276},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3278,"title":3279},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967540108]