[{"data":1,"prerenderedAt":2978},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F":3,"content-directory":2431},{"id":4,"title":5,"body":6,"date":2417,"description":2418,"difficulty":2419,"draft":2420,"extension":2421,"meta":2422,"navigation":218,"path":2423,"seo":2424,"stem":2425,"tags":2426,"updated":2417,"__hash__":2430},"content\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Findex.md","Secrets and Credentials in Python CLIs",{"type":7,"value":8,"toc":2395},"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,2391],[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],{},[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,2381,2382],{},"Sideways: ",[17,2383,2384],{"href":24},"Calling HTTP APIs from Python CLIs",[40,2386,2381,2387],{},[17,2388,2390],{"href":2389},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002F","Handling configuration files and environment variables",[2392,2393,2394],"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":2396},[2397,2398,2399,2400,2401,2402,2403,2404,2405,2406,2407,2408,2409,2416],{"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":2410},[2411,2412,2413,2414,2415],{"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":2418},"cli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Findex",[2427,2428,2429,56],"security","secrets","credentials","9h4ILUd7jhs_524ij2lI8B043Jlqxy-r-3BCuq-NPig",[2432,2435,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,2605,2608,2611,2614,2617,2620,2623,2626,2629,2632,2635,2638,2641,2644,2647,2650,2653,2656,2659,2662,2665,2668,2671,2674,2677,2680,2683,2686,2687,2690,2693,2696,2699,2702,2705,2708,2711,2714,2717,2720,2723,2726,2729,2732,2735,2738,2741,2744,2747,2750,2753,2756,2759,2762,2765,2768,2771,2774,2777,2780,2783,2786,2789,2792,2795,2798,2801,2804,2807,2810,2813,2816,2819,2822,2825,2828,2831,2834,2837,2840,2843,2846,2849,2852,2855,2858,2861,2864,2867,2870,2873,2876,2879,2882,2885,2888,2891,2894,2897,2900,2903,2906,2909,2912,2915,2918,2921,2924,2927,2930,2933,2936,2939,2942,2945,2948,2951,2954,2957,2960,2963,2966,2969,2972,2975],{"path":2433,"title":2434},"\u002Fabout","About Python CLI Toolcraft",{"path":2436,"title":2437},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2439,"title":2440},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2442,"title":2443},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2445,"title":2446},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2448,"title":2449},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2451,"title":2452},"\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":2454,"title":2455},"\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":2457,"title":2458},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2460,"title":2461},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2463,"title":2464},"\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":2466,"title":2467},"\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":2469,"title":2470},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2472,"title":2473},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2475,"title":2476},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2478,"title":2479},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2481,"title":2482},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2484,"title":2485},"\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":2487,"title":2488},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2490,"title":2491},"\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":2493,"title":2494},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2496,"title":2497},"\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":2499,"title":2500},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2502,"title":2503},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2505,"title":2506},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2508,"title":2509},"\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":2511,"title":2512},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2514,"title":2515},"\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":2517,"title":2518},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2520,"title":2521},"\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":2523,"title":2524},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2526,"title":2527},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2529,"title":2530},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2532,"title":2533},"\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":2535,"title":2536},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2538,"title":2539},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2541,"title":2542},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2544,"title":2545},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2547,"title":2548},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2550,"title":2551},"\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":2553,"title":2554},"\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":2556,"title":2557},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2559,"title":2560},"\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":2562,"title":2563},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2565,"title":2566},"\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":2568,"title":2569},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2571,"title":2572},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2574,"title":2575},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2577,"title":2578},"\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":2580,"title":2581},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2583,"title":2584},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2586,"title":2587},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2589,"title":2590},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2592,"title":2593},"\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":2595,"title":2596},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2598,"title":2599},"\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":2601,"title":2602},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2604,"title":2384},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis",{"path":2606,"title":2607},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2609,"title":2610},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2612,"title":2613},"\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":2615,"title":2616},"\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":2618,"title":2619},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2621,"title":2622},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2624,"title":2625},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2627,"title":2628},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2630,"title":2631},"\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":2633,"title":2634},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2636,"title":2637},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2639,"title":2640},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2642,"title":2643},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2645,"title":2646},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2648,"title":2649},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2651,"title":2652},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2654,"title":2655},"\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":2657,"title":2658},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2660,"title":2661},"\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":2663,"title":2664},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2666,"title":2667},"\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":2669,"title":2670},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2672,"title":2673},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2675,"title":2676},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2678,"title":2679},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2681,"title":2682},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2684,"title":2685},"\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":2423,"title":5},{"path":2688,"title":2689},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2691,"title":2692},"\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":2694,"title":2695},"\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":2697,"title":2698},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2700,"title":2701},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2703,"title":2704},"\u002F","Python CLI Toolcraft",{"path":2706,"title":2707},"\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":2709,"title":2710},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2712,"title":2713},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2715,"title":2716},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2718,"title":2719},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2721,"title":2722},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2724,"title":2725},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2727,"title":2728},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2730,"title":2731},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2733,"title":2734},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2736,"title":2737},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2739,"title":2740},"\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":2742,"title":2743},"\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":2745,"title":2746},"\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":2748,"title":2749},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2751,"title":2752},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2754,"title":2755},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2757,"title":2758},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2760,"title":2761},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2763,"title":2764},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2766,"title":2767},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2769,"title":2770},"\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":2772,"title":2773},"\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":2775,"title":2776},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2778,"title":2779},"\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":2781,"title":2782},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2784,"title":2785},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2787,"title":2788},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2790,"title":2791},"\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":2793,"title":2794},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2796,"title":2797},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2799,"title":2800},"\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":2802,"title":2803},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2805,"title":2806},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2808,"title":2809},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2811,"title":2812},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2814,"title":2815},"\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":2817,"title":2818},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2820,"title":2821},"\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":2823,"title":2824},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2826,"title":2827},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2829,"title":2830},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2832,"title":2833},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2835,"title":2836},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2838,"title":2839},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2841,"title":2842},"\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":2844,"title":2845},"\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":2847,"title":2848},"\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":2850,"title":2851},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2853,"title":2854},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2856,"title":2857},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2859,"title":2860},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2862,"title":2863},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2865,"title":2866},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2868,"title":2869},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2871,"title":2872},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2874,"title":2875},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2877,"title":2878},"\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":2880,"title":2881},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2883,"title":2884},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2886,"title":2887},"\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":2889,"title":2890},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2892,"title":2893},"\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":2895,"title":2896},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2898,"title":2899},"\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":2901,"title":2902},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2904,"title":2905},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2907,"title":2908},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2910,"title":2911},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2913,"title":2914},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2916,"title":2917},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2919,"title":2920},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2922,"title":2923},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2925,"title":2926},"\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":2928,"title":2929},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2931,"title":2932},"\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":2934,"title":2935},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2937,"title":2938},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2940,"title":2941},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2943,"title":2944},"\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":2946,"title":2947},"\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":2949,"title":2950},"\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":2952,"title":2953},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2955,"title":2956},"\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":2958,"title":2959},"\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":2961,"title":2962},"\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":2964,"title":2965},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2967,"title":2968},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2970,"title":2971},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2973,"title":2974},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2976,"title":2977},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905051]