[{"data":1,"prerenderedAt":2744},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely\u002F":3,"content-directory":2196},{"id":4,"title":5,"body":6,"date":2182,"description":2183,"difficulty":2184,"draft":2185,"extension":2186,"meta":2187,"navigation":135,"path":2188,"seo":2189,"stem":2190,"tags":2191,"updated":2182,"__hash__":2195},"content\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely\u002Findex.md","Prompting for Passwords Securely in Python CLIs",{"type":7,"value":8,"toc":2164},"minimark",[9,24,29,48,52,55,59,90,94,907,920,923,1376,1395,1398,1402,1405,1472,1476,1486,2008,2022,2026,2041,2045,2057,2063,2067,2084,2088,2098,2102,2105,2109,2120,2124,2127,2131,2160],[10,11,12,13,17,18,23],"p",{},"Some secrets have to be typed by a person: the passphrase that unlocks a local vault, the password for a database the tool is about to configure, a one-time token pasted from a web page. Prompting for them looks trivial — ",[14,15,16],"code",{},"input(\"Password: \")"," — and gets several things wrong at once: the password is echoed to the screen for anyone looking over a shoulder, the prompt is written to stdout where a pipe may capture it, and when the command runs in CI the prompt waits forever for a keyboard that does not exist. This guide builds prompt helpers that hide input, confirm new passwords, fall back to stdin for automation, refuse to hang when nobody is there, and can be tested without a terminal. It is part of the ",[19,20,22],"a",{"href":21},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F","secrets and credentials topic",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,32,36],"ul",{},[33,34,35],"li",{},"Python 3.10+ and Typer (Click works identically — Typer's prompt is Click's).",[33,37,38,39,43,44,23],{},"A command that genuinely needs interactive secret input. If the secret can be stored or injected instead, prefer ",[19,40,42],{"href":41},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring\u002F","the keychain"," or ",[19,45,47],{"href":46},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002F","environment variables and files",[25,49,51],{"id":50},"when-a-prompt-is-allowed-at-all","When a prompt is allowed at all",[10,53,54],{},"The first decision is not how to prompt but whether to. A prompt only makes sense when a person is attached to the terminal and has not asked the tool to be non-interactive.",[56,57],"inline-diagram",{"name":58},"sec-prompt-decision",[10,60,61,64,65,68,69,72,73,76,77,80,81,84,85,89],{},[14,62,63],{},"sys.stdin.isatty()"," answers the first half: it is ",[14,66,67],{},"False"," under CI, cron, systemd, ",[14,70,71],{},"docker run"," without ",[14,74,75],{},"-t",", and whenever input is piped. A ",[14,78,79],{},"--no-input"," flag (or an environment variable such as ",[14,82,83],{},"CI=true",") answers the second — some users run tools in a terminal but inside scripts where a surprise prompt is just as unwelcome. When prompting is not allowed, fail ",[86,87,88],"strong",{},"immediately",", with a message saying which non-interactive option to use instead. A hanging CI job that times out after an hour is the most expensive possible way to report \"missing password\".",[25,91,93],{"id":92},"the-recipe","The recipe",[95,96,101],"pre",{"className":97,"code":98,"language":99,"meta":100,"style":100},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fprompts.py\nfrom __future__ import annotations\n\nimport getpass\nimport os\nimport sys\n\n\nclass PromptUnavailable(Exception):\n    pass\n\n\ndef can_prompt(no_input: bool = False) -> bool:\n    return (not no_input and sys.stdin.isatty()\n            and os.environ.get(\"MYTOOL_NO_INPUT\", \"\").lower() not in (\"1\", \"true\", \"yes\"))\n\n\ndef ask_secret(label: str, *, no_input: bool = False, hint: str = \"\") -> str:\n    \"\"\"Prompt once for an existing secret, without echo.\"\"\"\n    if not can_prompt(no_input):\n        raise PromptUnavailable(f\"cannot prompt for {label} here; {hint}\".rstrip(\"; \"))\n    value = getpass.getpass(f\"{label}: \")\n    if not value:\n        raise PromptUnavailable(f\"no {label} entered\")\n    return value\n\n\ndef ask_new_secret(label: str, *, min_length: int = 12, attempts: int = 3,\n                   no_input: bool = False, hint: str = \"\") -> str:\n    \"\"\"Prompt for a new secret twice and insist they match.\"\"\"\n    if not can_prompt(no_input):\n        raise PromptUnavailable(f\"cannot prompt for {label} here; {hint}\".rstrip(\"; \"))\n    for remaining in range(attempts - 1, -1, -1):\n        first = getpass.getpass(f\"New {label}: \")\n        if len(first) \u003C min_length:\n            print(f\"{label} must be at least {min_length} characters\", file=sys.stderr)\n        elif first != getpass.getpass(f\"Repeat {label}: \"):\n            print(f\"{label}s do not match\", file=sys.stderr)\n        else:\n            return first\n        if remaining:\n            print(f\"try again ({remaining} attempt{'s' if remaining > 1 else ''} left)\", file=sys.stderr)\n    raise PromptUnavailable(f\"no {label} set after {attempts} attempts\")\n","python","",[14,102,103,112,130,137,146,154,162,167,172,191,197,202,207,236,257,302,307,312,356,362,374,419,447,457,480,488,493,498,538,564,570,579,612,651,676,694,736,765,793,801,810,818,874],{"__ignoreMap":100},[104,105,108],"span",{"class":106,"line":107},"line",1,[104,109,111],{"class":110},"sJ8bj","# src\u002Fmytool\u002Fprompts.py\n",[104,113,115,119,123,126],{"class":106,"line":114},2,[104,116,118],{"class":117},"szBVR","from",[104,120,122],{"class":121},"sj4cs"," __future__",[104,124,125],{"class":117}," import",[104,127,129],{"class":128},"sVt8B"," annotations\n",[104,131,133],{"class":106,"line":132},3,[104,134,136],{"emptyLinePlaceholder":135},true,"\n",[104,138,140,143],{"class":106,"line":139},4,[104,141,142],{"class":117},"import",[104,144,145],{"class":128}," getpass\n",[104,147,149,151],{"class":106,"line":148},5,[104,150,142],{"class":117},[104,152,153],{"class":128}," os\n",[104,155,157,159],{"class":106,"line":156},6,[104,158,142],{"class":117},[104,160,161],{"class":128}," sys\n",[104,163,165],{"class":106,"line":164},7,[104,166,136],{"emptyLinePlaceholder":135},[104,168,170],{"class":106,"line":169},8,[104,171,136],{"emptyLinePlaceholder":135},[104,173,175,178,182,185,188],{"class":106,"line":174},9,[104,176,177],{"class":117},"class",[104,179,181],{"class":180},"sScJk"," PromptUnavailable",[104,183,184],{"class":128},"(",[104,186,187],{"class":121},"Exception",[104,189,190],{"class":128},"):\n",[104,192,194],{"class":106,"line":193},10,[104,195,196],{"class":117},"    pass\n",[104,198,200],{"class":106,"line":199},11,[104,201,136],{"emptyLinePlaceholder":135},[104,203,205],{"class":106,"line":204},12,[104,206,136],{"emptyLinePlaceholder":135},[104,208,210,213,216,219,222,225,228,231,233],{"class":106,"line":209},13,[104,211,212],{"class":117},"def",[104,214,215],{"class":180}," can_prompt",[104,217,218],{"class":128},"(no_input: ",[104,220,221],{"class":121},"bool",[104,223,224],{"class":117}," =",[104,226,227],{"class":121}," False",[104,229,230],{"class":128},") -> ",[104,232,221],{"class":121},[104,234,235],{"class":128},":\n",[104,237,239,242,245,248,251,254],{"class":106,"line":238},14,[104,240,241],{"class":117},"    return",[104,243,244],{"class":128}," (",[104,246,247],{"class":117},"not",[104,249,250],{"class":128}," no_input ",[104,252,253],{"class":117},"and",[104,255,256],{"class":128}," sys.stdin.isatty()\n",[104,258,260,263,266,270,273,276,279,281,284,286,289,291,294,296,299],{"class":106,"line":259},15,[104,261,262],{"class":117},"            and",[104,264,265],{"class":128}," os.environ.get(",[104,267,269],{"class":268},"sZZnC","\"MYTOOL_NO_INPUT\"",[104,271,272],{"class":128},", ",[104,274,275],{"class":268},"\"\"",[104,277,278],{"class":128},").lower() ",[104,280,247],{"class":117},[104,282,283],{"class":117}," in",[104,285,244],{"class":128},[104,287,288],{"class":268},"\"1\"",[104,290,272],{"class":128},[104,292,293],{"class":268},"\"true\"",[104,295,272],{"class":128},[104,297,298],{"class":268},"\"yes\"",[104,300,301],{"class":128},"))\n",[104,303,305],{"class":106,"line":304},16,[104,306,136],{"emptyLinePlaceholder":135},[104,308,310],{"class":106,"line":309},17,[104,311,136],{"emptyLinePlaceholder":135},[104,313,315,317,320,323,326,328,331,334,336,338,340,343,345,347,350,352,354],{"class":106,"line":314},18,[104,316,212],{"class":117},[104,318,319],{"class":180}," ask_secret",[104,321,322],{"class":128},"(label: ",[104,324,325],{"class":121},"str",[104,327,272],{"class":128},[104,329,330],{"class":117},"*",[104,332,333],{"class":128},", no_input: ",[104,335,221],{"class":121},[104,337,224],{"class":117},[104,339,227],{"class":121},[104,341,342],{"class":128},", hint: ",[104,344,325],{"class":121},[104,346,224],{"class":117},[104,348,349],{"class":268}," \"\"",[104,351,230],{"class":128},[104,353,325],{"class":121},[104,355,235],{"class":128},[104,357,359],{"class":106,"line":358},19,[104,360,361],{"class":268},"    \"\"\"Prompt once for an existing secret, without echo.\"\"\"\n",[104,363,365,368,371],{"class":106,"line":364},20,[104,366,367],{"class":117},"    if",[104,369,370],{"class":117}," not",[104,372,373],{"class":128}," can_prompt(no_input):\n",[104,375,377,380,383,386,389,392,395,398,401,403,406,408,411,414,417],{"class":106,"line":376},21,[104,378,379],{"class":117},"        raise",[104,381,382],{"class":128}," PromptUnavailable(",[104,384,385],{"class":117},"f",[104,387,388],{"class":268},"\"cannot prompt for ",[104,390,391],{"class":121},"{",[104,393,394],{"class":128},"label",[104,396,397],{"class":121},"}",[104,399,400],{"class":268}," here; ",[104,402,391],{"class":121},[104,404,405],{"class":128},"hint",[104,407,397],{"class":121},[104,409,410],{"class":268},"\"",[104,412,413],{"class":128},".rstrip(",[104,415,416],{"class":268},"\"; \"",[104,418,301],{"class":128},[104,420,422,425,428,431,433,435,437,439,441,444],{"class":106,"line":421},22,[104,423,424],{"class":128},"    value ",[104,426,427],{"class":117},"=",[104,429,430],{"class":128}," getpass.getpass(",[104,432,385],{"class":117},[104,434,410],{"class":268},[104,436,391],{"class":121},[104,438,394],{"class":128},[104,440,397],{"class":121},[104,442,443],{"class":268},": \"",[104,445,446],{"class":128},")\n",[104,448,450,452,454],{"class":106,"line":449},23,[104,451,367],{"class":117},[104,453,370],{"class":117},[104,455,456],{"class":128}," value:\n",[104,458,460,462,464,466,469,471,473,475,478],{"class":106,"line":459},24,[104,461,379],{"class":117},[104,463,382],{"class":128},[104,465,385],{"class":117},[104,467,468],{"class":268},"\"no ",[104,470,391],{"class":121},[104,472,394],{"class":128},[104,474,397],{"class":121},[104,476,477],{"class":268}," entered\"",[104,479,446],{"class":128},[104,481,483,485],{"class":106,"line":482},25,[104,484,241],{"class":117},[104,486,487],{"class":128}," value\n",[104,489,491],{"class":106,"line":490},26,[104,492,136],{"emptyLinePlaceholder":135},[104,494,496],{"class":106,"line":495},27,[104,497,136],{"emptyLinePlaceholder":135},[104,499,501,503,506,508,510,512,514,517,520,522,525,528,530,532,535],{"class":106,"line":500},28,[104,502,212],{"class":117},[104,504,505],{"class":180}," ask_new_secret",[104,507,322],{"class":128},[104,509,325],{"class":121},[104,511,272],{"class":128},[104,513,330],{"class":117},[104,515,516],{"class":128},", min_length: ",[104,518,519],{"class":121},"int",[104,521,224],{"class":117},[104,523,524],{"class":121}," 12",[104,526,527],{"class":128},", attempts: ",[104,529,519],{"class":121},[104,531,224],{"class":117},[104,533,534],{"class":121}," 3",[104,536,537],{"class":128},",\n",[104,539,541,544,546,548,550,552,554,556,558,560,562],{"class":106,"line":540},29,[104,542,543],{"class":128},"                   no_input: ",[104,545,221],{"class":121},[104,547,224],{"class":117},[104,549,227],{"class":121},[104,551,342],{"class":128},[104,553,325],{"class":121},[104,555,224],{"class":117},[104,557,349],{"class":268},[104,559,230],{"class":128},[104,561,325],{"class":121},[104,563,235],{"class":128},[104,565,567],{"class":106,"line":566},30,[104,568,569],{"class":268},"    \"\"\"Prompt for a new secret twice and insist they match.\"\"\"\n",[104,571,573,575,577],{"class":106,"line":572},31,[104,574,367],{"class":117},[104,576,370],{"class":117},[104,578,373],{"class":128},[104,580,582,584,586,588,590,592,594,596,598,600,602,604,606,608,610],{"class":106,"line":581},32,[104,583,379],{"class":117},[104,585,382],{"class":128},[104,587,385],{"class":117},[104,589,388],{"class":268},[104,591,391],{"class":121},[104,593,394],{"class":128},[104,595,397],{"class":121},[104,597,400],{"class":268},[104,599,391],{"class":121},[104,601,405],{"class":128},[104,603,397],{"class":121},[104,605,410],{"class":268},[104,607,413],{"class":128},[104,609,416],{"class":268},[104,611,301],{"class":128},[104,613,615,618,621,624,627,630,633,636,638,640,643,645,647,649],{"class":106,"line":614},33,[104,616,617],{"class":117},"    for",[104,619,620],{"class":128}," remaining ",[104,622,623],{"class":117},"in",[104,625,626],{"class":121}," range",[104,628,629],{"class":128},"(attempts ",[104,631,632],{"class":117},"-",[104,634,635],{"class":121}," 1",[104,637,272],{"class":128},[104,639,632],{"class":117},[104,641,642],{"class":121},"1",[104,644,272],{"class":128},[104,646,632],{"class":117},[104,648,642],{"class":121},[104,650,190],{"class":128},[104,652,654,657,659,661,663,666,668,670,672,674],{"class":106,"line":653},34,[104,655,656],{"class":128},"        first ",[104,658,427],{"class":117},[104,660,430],{"class":128},[104,662,385],{"class":117},[104,664,665],{"class":268},"\"New ",[104,667,391],{"class":121},[104,669,394],{"class":128},[104,671,397],{"class":121},[104,673,443],{"class":268},[104,675,446],{"class":128},[104,677,679,682,685,688,691],{"class":106,"line":678},35,[104,680,681],{"class":117},"        if",[104,683,684],{"class":121}," len",[104,686,687],{"class":128},"(first) ",[104,689,690],{"class":117},"\u003C",[104,692,693],{"class":128}," min_length:\n",[104,695,697,700,702,704,706,708,710,712,715,717,720,722,725,727,731,733],{"class":106,"line":696},36,[104,698,699],{"class":121},"            print",[104,701,184],{"class":128},[104,703,385],{"class":117},[104,705,410],{"class":268},[104,707,391],{"class":121},[104,709,394],{"class":128},[104,711,397],{"class":121},[104,713,714],{"class":268}," must be at least ",[104,716,391],{"class":121},[104,718,719],{"class":128},"min_length",[104,721,397],{"class":121},[104,723,724],{"class":268}," characters\"",[104,726,272],{"class":128},[104,728,730],{"class":729},"s4XuR","file",[104,732,427],{"class":117},[104,734,735],{"class":128},"sys.stderr)\n",[104,737,739,742,745,748,750,752,755,757,759,761,763],{"class":106,"line":738},37,[104,740,741],{"class":117},"        elif",[104,743,744],{"class":128}," first ",[104,746,747],{"class":117},"!=",[104,749,430],{"class":128},[104,751,385],{"class":117},[104,753,754],{"class":268},"\"Repeat ",[104,756,391],{"class":121},[104,758,394],{"class":128},[104,760,397],{"class":121},[104,762,443],{"class":268},[104,764,190],{"class":128},[104,766,768,770,772,774,776,778,780,782,785,787,789,791],{"class":106,"line":767},38,[104,769,699],{"class":121},[104,771,184],{"class":128},[104,773,385],{"class":117},[104,775,410],{"class":268},[104,777,391],{"class":121},[104,779,394],{"class":128},[104,781,397],{"class":121},[104,783,784],{"class":268},"s do not match\"",[104,786,272],{"class":128},[104,788,730],{"class":729},[104,790,427],{"class":117},[104,792,735],{"class":128},[104,794,796,799],{"class":106,"line":795},39,[104,797,798],{"class":117},"        else",[104,800,235],{"class":128},[104,802,804,807],{"class":106,"line":803},40,[104,805,806],{"class":117},"            return",[104,808,809],{"class":128}," first\n",[104,811,813,815],{"class":106,"line":812},41,[104,814,681],{"class":117},[104,816,817],{"class":128}," remaining:\n",[104,819,821,823,825,827,830,832,835,837,840,842,845,848,850,853,855,858,861,863,866,868,870,872],{"class":106,"line":820},42,[104,822,699],{"class":121},[104,824,184],{"class":128},[104,826,385],{"class":117},[104,828,829],{"class":268},"\"try again (",[104,831,391],{"class":121},[104,833,834],{"class":128},"remaining",[104,836,397],{"class":121},[104,838,839],{"class":268}," attempt",[104,841,391],{"class":121},[104,843,844],{"class":268},"'s'",[104,846,847],{"class":117}," if",[104,849,620],{"class":128},[104,851,852],{"class":117},">",[104,854,635],{"class":121},[104,856,857],{"class":117}," else",[104,859,860],{"class":268}," ''",[104,862,397],{"class":121},[104,864,865],{"class":268}," left)\"",[104,867,272],{"class":128},[104,869,730],{"class":729},[104,871,427],{"class":117},[104,873,735],{"class":128},[104,875,877,880,882,884,886,888,890,892,895,897,900,902,905],{"class":106,"line":876},43,[104,878,879],{"class":117},"    raise",[104,881,382],{"class":128},[104,883,385],{"class":117},[104,885,468],{"class":268},[104,887,391],{"class":121},[104,889,394],{"class":128},[104,891,397],{"class":121},[104,893,894],{"class":268}," set after ",[104,896,391],{"class":121},[104,898,899],{"class":128},"attempts",[104,901,397],{"class":121},[104,903,904],{"class":268}," attempts\"",[104,906,446],{"class":128},[10,908,909,912,913,244,916,919],{},[14,910,911],{},"getpass.getpass()"," does the important low-level work: it turns off terminal echo, reads a line, and restores echo even if the user presses Ctrl+C. On POSIX it reads from the ",[86,914,915],{},"controlling terminal",[14,917,918],{},"\u002Fdev\u002Ftty",") rather than stdin, and writes its prompt there too — so it still works when stdin or stdout are redirected, and the prompt never pollutes a pipe. On Windows it uses the console directly.",[10,921,922],{},"The command layer wires the helpers to flags:",[95,924,926],{"className":97,"code":925,"language":99,"meta":100,"style":100},"# src\u002Fmytool\u002Fcli.py\nimport sys\n\nimport typer\n\nfrom mytool.prompts import PromptUnavailable, ask_new_secret\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Local vault.\"\"\"\n\n\n@app.command()\ndef init(\n    password_stdin: bool = typer.Option(False, \"--password-stdin\", help=\"Read the password from stdin.\"),\n    no_input: bool = typer.Option(False, \"--no-input\", help=\"Never prompt; fail instead.\"),\n) -> None:\n    \"\"\"Create a new encrypted vault.\"\"\"\n    if password_stdin:\n        password = sys.stdin.readline().rstrip(\"\\r\\n\")\n        if len(password) \u003C 12:\n            typer.echo(\"error: password from stdin must be at least 12 characters\", err=True)\n            raise typer.Exit(2)\n    else:\n        try:\n            password = ask_new_secret(\"vault password\", no_input=no_input,\n                                      hint=\"use --password-stdin or set MYTOOL_VAULT_PASSWORD_FILE\")\n        except PromptUnavailable as exc:\n            typer.echo(f\"error: {exc}\", err=True)\n            raise typer.Exit(2)\n    create_vault(password)\n    typer.echo(\"vault created\", err=True)\n\n\ndef create_vault(password: str) -> None:\n    ...  # derive a key with hashlib.scrypt and write the vault\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,927,928,933,939,943,950,954,966,970,980,984,988,996,1011,1016,1020,1024,1031,1041,1073,1102,1110,1115,1122,1141,1156,1176,1189,1196,1203,1226,1238,1252,1280,1290,1295,1313,1317,1321,1339,1347,1351,1355,1371],{"__ignoreMap":100},[104,929,930],{"class":106,"line":107},[104,931,932],{"class":110},"# src\u002Fmytool\u002Fcli.py\n",[104,934,935,937],{"class":106,"line":114},[104,936,142],{"class":117},[104,938,161],{"class":128},[104,940,941],{"class":106,"line":132},[104,942,136],{"emptyLinePlaceholder":135},[104,944,945,947],{"class":106,"line":139},[104,946,142],{"class":117},[104,948,949],{"class":128}," typer\n",[104,951,952],{"class":106,"line":148},[104,953,136],{"emptyLinePlaceholder":135},[104,955,956,958,961,963],{"class":106,"line":156},[104,957,118],{"class":117},[104,959,960],{"class":128}," mytool.prompts ",[104,962,142],{"class":117},[104,964,965],{"class":128}," PromptUnavailable, ask_new_secret\n",[104,967,968],{"class":106,"line":164},[104,969,136],{"emptyLinePlaceholder":135},[104,971,972,975,977],{"class":106,"line":169},[104,973,974],{"class":128},"app ",[104,976,427],{"class":117},[104,978,979],{"class":128}," typer.Typer()\n",[104,981,982],{"class":106,"line":174},[104,983,136],{"emptyLinePlaceholder":135},[104,985,986],{"class":106,"line":193},[104,987,136],{"emptyLinePlaceholder":135},[104,989,990,993],{"class":106,"line":199},[104,991,992],{"class":180},"@app.callback",[104,994,995],{"class":128},"()\n",[104,997,998,1000,1003,1006,1009],{"class":106,"line":204},[104,999,212],{"class":117},[104,1001,1002],{"class":180}," main",[104,1004,1005],{"class":128},"() -> ",[104,1007,1008],{"class":121},"None",[104,1010,235],{"class":128},[104,1012,1013],{"class":106,"line":209},[104,1014,1015],{"class":268},"    \"\"\"Local vault.\"\"\"\n",[104,1017,1018],{"class":106,"line":238},[104,1019,136],{"emptyLinePlaceholder":135},[104,1021,1022],{"class":106,"line":259},[104,1023,136],{"emptyLinePlaceholder":135},[104,1025,1026,1029],{"class":106,"line":304},[104,1027,1028],{"class":180},"@app.command",[104,1030,995],{"class":128},[104,1032,1033,1035,1038],{"class":106,"line":309},[104,1034,212],{"class":117},[104,1036,1037],{"class":180}," init",[104,1039,1040],{"class":128},"(\n",[104,1042,1043,1046,1048,1050,1053,1055,1057,1060,1062,1065,1067,1070],{"class":106,"line":314},[104,1044,1045],{"class":128},"    password_stdin: ",[104,1047,221],{"class":121},[104,1049,224],{"class":117},[104,1051,1052],{"class":128}," typer.Option(",[104,1054,67],{"class":121},[104,1056,272],{"class":128},[104,1058,1059],{"class":268},"\"--password-stdin\"",[104,1061,272],{"class":128},[104,1063,1064],{"class":729},"help",[104,1066,427],{"class":117},[104,1068,1069],{"class":268},"\"Read the password from stdin.\"",[104,1071,1072],{"class":128},"),\n",[104,1074,1075,1078,1080,1082,1084,1086,1088,1091,1093,1095,1097,1100],{"class":106,"line":358},[104,1076,1077],{"class":128},"    no_input: ",[104,1079,221],{"class":121},[104,1081,224],{"class":117},[104,1083,1052],{"class":128},[104,1085,67],{"class":121},[104,1087,272],{"class":128},[104,1089,1090],{"class":268},"\"--no-input\"",[104,1092,272],{"class":128},[104,1094,1064],{"class":729},[104,1096,427],{"class":117},[104,1098,1099],{"class":268},"\"Never prompt; fail instead.\"",[104,1101,1072],{"class":128},[104,1103,1104,1106,1108],{"class":106,"line":364},[104,1105,230],{"class":128},[104,1107,1008],{"class":121},[104,1109,235],{"class":128},[104,1111,1112],{"class":106,"line":376},[104,1113,1114],{"class":268},"    \"\"\"Create a new encrypted vault.\"\"\"\n",[104,1116,1117,1119],{"class":106,"line":421},[104,1118,367],{"class":117},[104,1120,1121],{"class":128}," password_stdin:\n",[104,1123,1124,1127,1129,1132,1134,1137,1139],{"class":106,"line":449},[104,1125,1126],{"class":128},"        password ",[104,1128,427],{"class":117},[104,1130,1131],{"class":128}," sys.stdin.readline().rstrip(",[104,1133,410],{"class":268},[104,1135,1136],{"class":121},"\\r\\n",[104,1138,410],{"class":268},[104,1140,446],{"class":128},[104,1142,1143,1145,1147,1150,1152,1154],{"class":106,"line":459},[104,1144,681],{"class":117},[104,1146,684],{"class":121},[104,1148,1149],{"class":128},"(password) ",[104,1151,690],{"class":117},[104,1153,524],{"class":121},[104,1155,235],{"class":128},[104,1157,1158,1161,1164,1166,1169,1171,1174],{"class":106,"line":482},[104,1159,1160],{"class":128},"            typer.echo(",[104,1162,1163],{"class":268},"\"error: password from stdin must be at least 12 characters\"",[104,1165,272],{"class":128},[104,1167,1168],{"class":729},"err",[104,1170,427],{"class":117},[104,1172,1173],{"class":121},"True",[104,1175,446],{"class":128},[104,1177,1178,1181,1184,1187],{"class":106,"line":490},[104,1179,1180],{"class":117},"            raise",[104,1182,1183],{"class":128}," typer.Exit(",[104,1185,1186],{"class":121},"2",[104,1188,446],{"class":128},[104,1190,1191,1194],{"class":106,"line":495},[104,1192,1193],{"class":117},"    else",[104,1195,235],{"class":128},[104,1197,1198,1201],{"class":106,"line":500},[104,1199,1200],{"class":117},"        try",[104,1202,235],{"class":128},[104,1204,1205,1208,1210,1213,1216,1218,1221,1223],{"class":106,"line":540},[104,1206,1207],{"class":128},"            password ",[104,1209,427],{"class":117},[104,1211,1212],{"class":128}," ask_new_secret(",[104,1214,1215],{"class":268},"\"vault password\"",[104,1217,272],{"class":128},[104,1219,1220],{"class":729},"no_input",[104,1222,427],{"class":117},[104,1224,1225],{"class":128},"no_input,\n",[104,1227,1228,1231,1233,1236],{"class":106,"line":566},[104,1229,1230],{"class":729},"                                      hint",[104,1232,427],{"class":117},[104,1234,1235],{"class":268},"\"use --password-stdin or set MYTOOL_VAULT_PASSWORD_FILE\"",[104,1237,446],{"class":128},[104,1239,1240,1243,1246,1249],{"class":106,"line":572},[104,1241,1242],{"class":117},"        except",[104,1244,1245],{"class":128}," PromptUnavailable ",[104,1247,1248],{"class":117},"as",[104,1250,1251],{"class":128}," exc:\n",[104,1253,1254,1256,1258,1261,1263,1266,1268,1270,1272,1274,1276,1278],{"class":106,"line":581},[104,1255,1160],{"class":128},[104,1257,385],{"class":117},[104,1259,1260],{"class":268},"\"error: ",[104,1262,391],{"class":121},[104,1264,1265],{"class":128},"exc",[104,1267,397],{"class":121},[104,1269,410],{"class":268},[104,1271,272],{"class":128},[104,1273,1168],{"class":729},[104,1275,427],{"class":117},[104,1277,1173],{"class":121},[104,1279,446],{"class":128},[104,1281,1282,1284,1286,1288],{"class":106,"line":614},[104,1283,1180],{"class":117},[104,1285,1183],{"class":128},[104,1287,1186],{"class":121},[104,1289,446],{"class":128},[104,1291,1292],{"class":106,"line":653},[104,1293,1294],{"class":128},"    create_vault(password)\n",[104,1296,1297,1300,1303,1305,1307,1309,1311],{"class":106,"line":678},[104,1298,1299],{"class":128},"    typer.echo(",[104,1301,1302],{"class":268},"\"vault created\"",[104,1304,272],{"class":128},[104,1306,1168],{"class":729},[104,1308,427],{"class":117},[104,1310,1173],{"class":121},[104,1312,446],{"class":128},[104,1314,1315],{"class":106,"line":696},[104,1316,136],{"emptyLinePlaceholder":135},[104,1318,1319],{"class":106,"line":738},[104,1320,136],{"emptyLinePlaceholder":135},[104,1322,1323,1325,1328,1331,1333,1335,1337],{"class":106,"line":767},[104,1324,212],{"class":117},[104,1326,1327],{"class":180}," create_vault",[104,1329,1330],{"class":128},"(password: ",[104,1332,325],{"class":121},[104,1334,230],{"class":128},[104,1336,1008],{"class":121},[104,1338,235],{"class":128},[104,1340,1341,1344],{"class":106,"line":795},[104,1342,1343],{"class":121},"    ...",[104,1345,1346],{"class":110},"  # derive a key with hashlib.scrypt and write the vault\n",[104,1348,1349],{"class":106,"line":803},[104,1350,136],{"emptyLinePlaceholder":135},[104,1352,1353],{"class":106,"line":812},[104,1354,136],{"emptyLinePlaceholder":135},[104,1356,1357,1360,1363,1366,1369],{"class":106,"line":820},[104,1358,1359],{"class":117},"if",[104,1361,1362],{"class":121}," __name__",[104,1364,1365],{"class":117}," ==",[104,1367,1368],{"class":268}," \"__main__\"",[104,1370,235],{"class":128},[104,1372,1373],{"class":106,"line":876},[104,1374,1375],{"class":128},"    app()\n",[10,1377,1378,1379,1382,1383,1386,1387,1390,1391,1394],{},"Typer and Click also offer the one-liner ",[14,1380,1381],{},"typer.prompt(\"Password\", hide_input=True, confirmation_prompt=True)",", which disables echo and asks twice. It is a good choice for simple cases; the hand-written helper above adds the ",[14,1384,1385],{},"isatty"," guard, a length policy and a bounded number of attempts, which the built-in prompt does not. Whichever you use, route the prompt to stderr (",[14,1388,1389],{},"err=True",") when you are not using ",[14,1392,1393],{},"getpass",", so it never lands in captured stdout.",[56,1396],{"name":1397},"sec-getpass-terminal",[25,1399,1401],{"id":1400},"ux-considerations","UX considerations",[56,1403],{"name":1404},"sec-prompt-rules",[30,1406,1407,1418,1424,1430,1440,1454,1460],{},[33,1408,1409,1412,1413,1417],{},[86,1410,1411],{},"Confirm new secrets, not existing ones."," A typo in a new vault password locks the user out; asking twice prevents it. Asking twice to ",[1414,1415,1416],"em",{},"unlock"," is just friction.",[33,1419,1420,1423],{},[86,1421,1422],{},"Validate before confirming."," Check length or strength on the first entry, so the user does not type a too-short password twice before being told.",[33,1425,1426,1429],{},[86,1427,1428],{},"Bound the attempts."," Three mismatches is a strong signal of a problem; exit with a clear message instead of looping.",[33,1431,1432,1435,1436,1439],{},[86,1433,1434],{},"Never show a stored secret as a default."," Prompts like ",[14,1437,1438],{},"Token [ghp_abc...]:"," put the secret on screen. If a value exists, say \"press Enter to keep the current token\" without showing it.",[33,1441,1442,1445,1446,1449,1450,1453],{},[86,1443,1444],{},"Always offer a non-interactive path."," ",[14,1447,1448],{},"--password-stdin"," for pipes from password managers; a ",[14,1451,1452],{},"_FILE"," environment variable for containers. Mention them in the error when prompting is impossible.",[33,1455,1456,1459],{},[86,1457,1458],{},"Do not print the secret back."," \"Password set\" is the confirmation users need; \"Password set to hunter2\" is a leak waiting for a screen share.",[33,1461,1462,1445,1465,1467,1468,23],{},[86,1463,1464],{},"Handle Ctrl+C at the prompt.",[14,1466,1393],{}," restores echo; your top-level handler should print a short \"cancelled\" and exit 130 rather than a traceback, as in ",[19,1469,1471],{"href":1470},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002F","handling KeyboardInterrupt cleanly",[25,1473,1475],{"id":1474},"testing-the-behaviour","Testing the behaviour",[10,1477,1478,1479,1482,1483,1485],{},"Prompts are awkward to test through a real terminal, so replace ",[14,1480,1481],{},"getpass.getpass"," with a scripted fake and control ",[14,1484,1385],{}," directly. Everything else runs normally:",[95,1487,1489],{"className":97,"code":1488,"language":99,"meta":100,"style":100},"# tests\u002Ftest_prompts.py\nimport io\n\nimport pytest\n\nfrom mytool import prompts\nfrom mytool.prompts import PromptUnavailable, ask_new_secret, ask_secret\n\n\nclass FakeTTY(io.StringIO):\n    def isatty(self) -> bool:\n        return True\n\n\n@pytest.fixture\ndef tty(monkeypatch):\n    monkeypatch.setattr(prompts.sys, \"stdin\", FakeTTY())\n    monkeypatch.delenv(\"MYTOOL_NO_INPUT\", raising=False)\n\n\ndef script(monkeypatch, *answers):\n    it = iter(answers)\n    monkeypatch.setattr(prompts.getpass, \"getpass\", lambda prompt=\"\": next(it))\n\n\ndef test_new_secret_matching(tty, monkeypatch):\n    script(monkeypatch, \"correct horse battery\", \"correct horse battery\")\n    assert ask_new_secret(\"password\") == \"correct horse battery\"\n\n\ndef test_mismatch_then_success(tty, monkeypatch, capsys):\n    script(monkeypatch, \"correct horse battery\", \"typo horse battery\",\n           \"correct horse battery\", \"correct horse battery\")\n    assert ask_new_secret(\"password\") == \"correct horse battery\"\n    assert \"do not match\" in capsys.readouterr().err\n\n\ndef test_gives_up_after_attempts(tty, monkeypatch):\n    script(monkeypatch, *[\"short\"] * 3)\n    with pytest.raises(PromptUnavailable, match=\"3 attempts\"):\n        ask_new_secret(\"password\")\n\n\ndef test_refuses_without_terminal(monkeypatch):\n    monkeypatch.setattr(prompts.sys, \"stdin\", io.StringIO())   # isatty() is False\n    with pytest.raises(PromptUnavailable, match=\"--password-stdin\"):\n        ask_secret(\"password\", hint=\"use --password-stdin\")\n\n\ndef test_no_input_env(tty, monkeypatch):\n    monkeypatch.setenv(\"MYTOOL_NO_INPUT\", \"1\")\n    with pytest.raises(PromptUnavailable):\n        ask_secret(\"password\")\n",[14,1490,1491,1496,1503,1507,1514,1518,1530,1541,1545,1549,1568,1583,1591,1595,1599,1604,1614,1625,1643,1647,1651,1666,1679,1708,1712,1716,1726,1740,1759,1763,1767,1777,1790,1801,1815,1827,1831,1835,1844,1865,1883,1892,1896,1900,1910,1923,1938,1957,1962,1967,1977,1991,1999],{"__ignoreMap":100},[104,1492,1493],{"class":106,"line":107},[104,1494,1495],{"class":110},"# tests\u002Ftest_prompts.py\n",[104,1497,1498,1500],{"class":106,"line":114},[104,1499,142],{"class":117},[104,1501,1502],{"class":128}," io\n",[104,1504,1505],{"class":106,"line":132},[104,1506,136],{"emptyLinePlaceholder":135},[104,1508,1509,1511],{"class":106,"line":139},[104,1510,142],{"class":117},[104,1512,1513],{"class":128}," pytest\n",[104,1515,1516],{"class":106,"line":148},[104,1517,136],{"emptyLinePlaceholder":135},[104,1519,1520,1522,1525,1527],{"class":106,"line":156},[104,1521,118],{"class":117},[104,1523,1524],{"class":128}," mytool ",[104,1526,142],{"class":117},[104,1528,1529],{"class":128}," prompts\n",[104,1531,1532,1534,1536,1538],{"class":106,"line":164},[104,1533,118],{"class":117},[104,1535,960],{"class":128},[104,1537,142],{"class":117},[104,1539,1540],{"class":128}," PromptUnavailable, ask_new_secret, ask_secret\n",[104,1542,1543],{"class":106,"line":169},[104,1544,136],{"emptyLinePlaceholder":135},[104,1546,1547],{"class":106,"line":174},[104,1548,136],{"emptyLinePlaceholder":135},[104,1550,1551,1553,1556,1558,1561,1563,1566],{"class":106,"line":193},[104,1552,177],{"class":117},[104,1554,1555],{"class":180}," FakeTTY",[104,1557,184],{"class":128},[104,1559,1560],{"class":180},"io",[104,1562,23],{"class":128},[104,1564,1565],{"class":180},"StringIO",[104,1567,190],{"class":128},[104,1569,1570,1573,1576,1579,1581],{"class":106,"line":199},[104,1571,1572],{"class":117},"    def",[104,1574,1575],{"class":180}," isatty",[104,1577,1578],{"class":128},"(self) -> ",[104,1580,221],{"class":121},[104,1582,235],{"class":128},[104,1584,1585,1588],{"class":106,"line":204},[104,1586,1587],{"class":117},"        return",[104,1589,1590],{"class":121}," True\n",[104,1592,1593],{"class":106,"line":209},[104,1594,136],{"emptyLinePlaceholder":135},[104,1596,1597],{"class":106,"line":238},[104,1598,136],{"emptyLinePlaceholder":135},[104,1600,1601],{"class":106,"line":259},[104,1602,1603],{"class":180},"@pytest.fixture\n",[104,1605,1606,1608,1611],{"class":106,"line":304},[104,1607,212],{"class":117},[104,1609,1610],{"class":180}," tty",[104,1612,1613],{"class":128},"(monkeypatch):\n",[104,1615,1616,1619,1622],{"class":106,"line":309},[104,1617,1618],{"class":128},"    monkeypatch.setattr(prompts.sys, ",[104,1620,1621],{"class":268},"\"stdin\"",[104,1623,1624],{"class":128},", FakeTTY())\n",[104,1626,1627,1630,1632,1634,1637,1639,1641],{"class":106,"line":314},[104,1628,1629],{"class":128},"    monkeypatch.delenv(",[104,1631,269],{"class":268},[104,1633,272],{"class":128},[104,1635,1636],{"class":729},"raising",[104,1638,427],{"class":117},[104,1640,67],{"class":121},[104,1642,446],{"class":128},[104,1644,1645],{"class":106,"line":358},[104,1646,136],{"emptyLinePlaceholder":135},[104,1648,1649],{"class":106,"line":364},[104,1650,136],{"emptyLinePlaceholder":135},[104,1652,1653,1655,1658,1661,1663],{"class":106,"line":376},[104,1654,212],{"class":117},[104,1656,1657],{"class":180}," script",[104,1659,1660],{"class":128},"(monkeypatch, ",[104,1662,330],{"class":117},[104,1664,1665],{"class":128},"answers):\n",[104,1667,1668,1671,1673,1676],{"class":106,"line":421},[104,1669,1670],{"class":128},"    it ",[104,1672,427],{"class":117},[104,1674,1675],{"class":121}," iter",[104,1677,1678],{"class":128},"(answers)\n",[104,1680,1681,1684,1687,1689,1692,1695,1697,1699,1702,1705],{"class":106,"line":449},[104,1682,1683],{"class":128},"    monkeypatch.setattr(prompts.getpass, ",[104,1685,1686],{"class":268},"\"getpass\"",[104,1688,272],{"class":128},[104,1690,1691],{"class":117},"lambda",[104,1693,1694],{"class":128}," prompt",[104,1696,427],{"class":117},[104,1698,275],{"class":268},[104,1700,1701],{"class":128},": ",[104,1703,1704],{"class":121},"next",[104,1706,1707],{"class":128},"(it))\n",[104,1709,1710],{"class":106,"line":459},[104,1711,136],{"emptyLinePlaceholder":135},[104,1713,1714],{"class":106,"line":482},[104,1715,136],{"emptyLinePlaceholder":135},[104,1717,1718,1720,1723],{"class":106,"line":490},[104,1719,212],{"class":117},[104,1721,1722],{"class":180}," test_new_secret_matching",[104,1724,1725],{"class":128},"(tty, monkeypatch):\n",[104,1727,1728,1731,1734,1736,1738],{"class":106,"line":495},[104,1729,1730],{"class":128},"    script(monkeypatch, ",[104,1732,1733],{"class":268},"\"correct horse battery\"",[104,1735,272],{"class":128},[104,1737,1733],{"class":268},[104,1739,446],{"class":128},[104,1741,1742,1745,1747,1750,1753,1756],{"class":106,"line":500},[104,1743,1744],{"class":117},"    assert",[104,1746,1212],{"class":128},[104,1748,1749],{"class":268},"\"password\"",[104,1751,1752],{"class":128},") ",[104,1754,1755],{"class":117},"==",[104,1757,1758],{"class":268}," \"correct horse battery\"\n",[104,1760,1761],{"class":106,"line":540},[104,1762,136],{"emptyLinePlaceholder":135},[104,1764,1765],{"class":106,"line":566},[104,1766,136],{"emptyLinePlaceholder":135},[104,1768,1769,1771,1774],{"class":106,"line":572},[104,1770,212],{"class":117},[104,1772,1773],{"class":180}," test_mismatch_then_success",[104,1775,1776],{"class":128},"(tty, monkeypatch, capsys):\n",[104,1778,1779,1781,1783,1785,1788],{"class":106,"line":581},[104,1780,1730],{"class":128},[104,1782,1733],{"class":268},[104,1784,272],{"class":128},[104,1786,1787],{"class":268},"\"typo horse battery\"",[104,1789,537],{"class":128},[104,1791,1792,1795,1797,1799],{"class":106,"line":614},[104,1793,1794],{"class":268},"           \"correct horse battery\"",[104,1796,272],{"class":128},[104,1798,1733],{"class":268},[104,1800,446],{"class":128},[104,1802,1803,1805,1807,1809,1811,1813],{"class":106,"line":653},[104,1804,1744],{"class":117},[104,1806,1212],{"class":128},[104,1808,1749],{"class":268},[104,1810,1752],{"class":128},[104,1812,1755],{"class":117},[104,1814,1758],{"class":268},[104,1816,1817,1819,1822,1824],{"class":106,"line":678},[104,1818,1744],{"class":117},[104,1820,1821],{"class":268}," \"do not match\"",[104,1823,283],{"class":117},[104,1825,1826],{"class":128}," capsys.readouterr().err\n",[104,1828,1829],{"class":106,"line":696},[104,1830,136],{"emptyLinePlaceholder":135},[104,1832,1833],{"class":106,"line":738},[104,1834,136],{"emptyLinePlaceholder":135},[104,1836,1837,1839,1842],{"class":106,"line":767},[104,1838,212],{"class":117},[104,1840,1841],{"class":180}," test_gives_up_after_attempts",[104,1843,1725],{"class":128},[104,1845,1846,1848,1850,1853,1856,1859,1861,1863],{"class":106,"line":795},[104,1847,1730],{"class":128},[104,1849,330],{"class":117},[104,1851,1852],{"class":128},"[",[104,1854,1855],{"class":268},"\"short\"",[104,1857,1858],{"class":128},"] ",[104,1860,330],{"class":117},[104,1862,534],{"class":121},[104,1864,446],{"class":128},[104,1866,1867,1870,1873,1876,1878,1881],{"class":106,"line":803},[104,1868,1869],{"class":117},"    with",[104,1871,1872],{"class":128}," pytest.raises(PromptUnavailable, ",[104,1874,1875],{"class":729},"match",[104,1877,427],{"class":117},[104,1879,1880],{"class":268},"\"3 attempts\"",[104,1882,190],{"class":128},[104,1884,1885,1888,1890],{"class":106,"line":812},[104,1886,1887],{"class":128},"        ask_new_secret(",[104,1889,1749],{"class":268},[104,1891,446],{"class":128},[104,1893,1894],{"class":106,"line":820},[104,1895,136],{"emptyLinePlaceholder":135},[104,1897,1898],{"class":106,"line":876},[104,1899,136],{"emptyLinePlaceholder":135},[104,1901,1903,1905,1908],{"class":106,"line":1902},44,[104,1904,212],{"class":117},[104,1906,1907],{"class":180}," test_refuses_without_terminal",[104,1909,1613],{"class":128},[104,1911,1913,1915,1917,1920],{"class":106,"line":1912},45,[104,1914,1618],{"class":128},[104,1916,1621],{"class":268},[104,1918,1919],{"class":128},", io.StringIO())   ",[104,1921,1922],{"class":110},"# isatty() is False\n",[104,1924,1926,1928,1930,1932,1934,1936],{"class":106,"line":1925},46,[104,1927,1869],{"class":117},[104,1929,1872],{"class":128},[104,1931,1875],{"class":729},[104,1933,427],{"class":117},[104,1935,1059],{"class":268},[104,1937,190],{"class":128},[104,1939,1941,1944,1946,1948,1950,1952,1955],{"class":106,"line":1940},47,[104,1942,1943],{"class":128},"        ask_secret(",[104,1945,1749],{"class":268},[104,1947,272],{"class":128},[104,1949,405],{"class":729},[104,1951,427],{"class":117},[104,1953,1954],{"class":268},"\"use --password-stdin\"",[104,1956,446],{"class":128},[104,1958,1960],{"class":106,"line":1959},48,[104,1961,136],{"emptyLinePlaceholder":135},[104,1963,1965],{"class":106,"line":1964},49,[104,1966,136],{"emptyLinePlaceholder":135},[104,1968,1970,1972,1975],{"class":106,"line":1969},50,[104,1971,212],{"class":117},[104,1973,1974],{"class":180}," test_no_input_env",[104,1976,1725],{"class":128},[104,1978,1980,1983,1985,1987,1989],{"class":106,"line":1979},51,[104,1981,1982],{"class":128},"    monkeypatch.setenv(",[104,1984,269],{"class":268},[104,1986,272],{"class":128},[104,1988,288],{"class":268},[104,1990,446],{"class":128},[104,1992,1994,1996],{"class":106,"line":1993},52,[104,1995,1869],{"class":117},[104,1997,1998],{"class":128}," pytest.raises(PromptUnavailable):\n",[104,2000,2002,2004,2006],{"class":106,"line":2001},53,[104,2003,1943],{"class":128},[104,2005,1749],{"class":268},[104,2007,446],{"class":128},[10,2009,2010,2011,2013,2014,2017,2018,23],{},"The refusal tests matter most: they guarantee the tool fails fast in CI rather than hanging. For end-to-end tests of the ",[14,2012,1448],{}," path, ",[14,2015,2016],{},"CliRunner.invoke(app, [\"init\", \"--password-stdin\"], input=\"a-long-password\\n\")"," feeds stdin directly, as covered in ",[19,2019,2021],{"href":2020},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002F","testing interactive prompts and stdin",[25,2023,2025],{"id":2024},"conclusion","Conclusion",[10,2027,2028,2029,43,2031,2034,2035,2037,2038,2040],{},"A secure prompt is short to write and easy to get subtly wrong. Only prompt when a person is present and has not opted out; use ",[14,2030,1393],{},[14,2032,2033],{},"hide_input"," so nothing is echoed and nothing reaches stdout; confirm new secrets and bound the retries; and always provide ",[14,2036,1448],{}," or a file variable for automation. With the prompt logic in a small module and ",[14,2039,1393],{}," patched in tests, every branch — including \"never hang in CI\" — is covered.",[25,2042,2044],{"id":2043},"frequently-asked-questions","Frequently asked questions",[2046,2047,2049,2050,2052,2053,2056],"h3",{"id":2048},"why-does-getpass-print-a-getpasswarning-in-my-ide","Why does ",[14,2051,1393],{}," print a ",[14,2054,2055],{},"GetPassWarning"," in my IDE?",[10,2058,2059,2060,2062],{},"Some IDE consoles are not real terminals, so ",[14,2061,1393],{}," cannot disable echo and falls back to reading visibly, with a warning. Run the tool in a real terminal, or use the IDE's run configuration option to emulate one.",[2046,2064,2066],{"id":2065},"should-i-show-asterisks-as-the-user-types","Should I show asterisks as the user types?",[10,2068,2069,2070,2072,2073,43,2076,2079,2080,23],{},"Neither ",[14,2071,1393],{}," nor Click does, and showing them leaks the length. If you want visible feedback, ",[14,2074,2075],{},"prompt_toolkit",[14,2077,2078],{},"questionary"," can mask input with a character — see ",[19,2081,2083],{"href":2082},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus\u002F","building interactive prompts and menus",[2046,2085,2087],{"id":2086},"how-do-i-read-a-password-from-stdin-without-the-trailing-newline","How do I read a password from stdin without the trailing newline?",[10,2089,2090,2093,2094,2097],{},[14,2091,2092],{},"sys.stdin.readline().rstrip(\"\\r\\n\")"," — strip only line endings so passwords with trailing spaces survive. Reading with ",[14,2095,2096],{},".strip()"," silently changes such passwords.",[2046,2099,2101],{"id":2100},"when-in-the-command-should-the-prompt-happen","When in the command should the prompt happen?",[10,2103,2104],{},"After every cheap check has passed, and before any slow or irreversible work starts. Validate arguments, confirm files exist and check that you can reach the server first — being asked for a password and then told the target directory does not exist is irritating. But collect the secret before a long download or a multi-step change, so the user is not called back to the keyboard ten minutes in, and so a mistyped password cannot leave the work half-done.",[2046,2106,2108],{"id":2107},"can-one-prompt-cover-several-commands-in-a-row","Can one prompt cover several commands in a row?",[10,2110,2111,2112,2115,2116,2119],{},"Not by caching the password yourself. If users run many commands that need the same secret, that is a sign it belongs in the keychain or an agent process, the way ",[14,2113,2114],{},"ssh-agent"," and ",[14,2117,2118],{},"gpg-agent"," hold keys for a session. A short-lived session token obtained with the password and stored like any other token achieves the same effect without the password ever being written anywhere.",[2046,2121,2123],{"id":2122},"is-it-safe-to-keep-the-password-in-a-variable","Is it safe to keep the password in a variable?",[10,2125,2126],{},"For the lifetime of one command, yes. Pass it straight to the key-derivation or authentication call and let it go out of scope; avoid storing it on long-lived objects or in logs, and never cache it to disk.",[25,2128,2130],{"id":2129},"related","Related",[30,2132,2133,2139,2144,2149,2154],{},[33,2134,2135,2136],{},"Up: ",[19,2137,2138],{"href":21},"Secrets and credentials in Python CLIs",[33,2140,2141],{},[19,2142,2143],{"href":46},"Reading secrets from env and files",[33,2145,2146],{},[19,2147,2148],{"href":41},"Storing tokens with keyring",[33,2150,2151],{},[19,2152,2153],{"href":2020},"Testing interactive prompts and stdin",[33,2155,2156],{},[19,2157,2159],{"href":2158},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002F","Detecting a TTY and adapting output",[2161,2162,2163],"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .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);}",{"title":100,"searchDepth":114,"depth":114,"links":2165},[2166,2167,2168,2169,2170,2171,2172,2181],{"id":27,"depth":114,"text":28},{"id":50,"depth":114,"text":51},{"id":92,"depth":114,"text":93},{"id":1400,"depth":114,"text":1401},{"id":1474,"depth":114,"text":1475},{"id":2024,"depth":114,"text":2025},{"id":2043,"depth":114,"text":2044,"children":2173},[2174,2176,2177,2178,2179,2180],{"id":2048,"depth":132,"text":2175},"Why does getpass print a GetPassWarning in my IDE?",{"id":2065,"depth":132,"text":2066},{"id":2086,"depth":132,"text":2087},{"id":2100,"depth":132,"text":2101},{"id":2107,"depth":132,"text":2108},{"id":2122,"depth":132,"text":2123},{"id":2129,"depth":114,"text":2130},"2026-09-18","Prompt for passwords and tokens in a Python CLI without echo or hangs: getpass and hide_input, confirmation, --password-stdin, --no-input and testable prompts.","beginner",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely",{"title":5,"description":2183},"cli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely\u002Findex",[2192,2193,1393,2194],"security","prompts","typer","UwXNGZ0ShhVmGu5cxQSm2bifZU6AWAdmGpZjeITtPdw",[2197,2200,2203,2206,2209,2212,2215,2218,2221,2224,2227,2230,2233,2236,2239,2242,2245,2248,2251,2254,2257,2260,2263,2266,2269,2272,2275,2278,2281,2284,2287,2290,2293,2296,2299,2302,2305,2308,2311,2314,2317,2320,2323,2326,2329,2332,2335,2338,2341,2344,2347,2350,2353,2356,2359,2362,2365,2368,2371,2374,2377,2380,2383,2386,2389,2392,2395,2398,2401,2404,2407,2410,2413,2416,2419,2422,2425,2428,2431,2434,2437,2440,2443,2446,2449,2452,2455,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600,2603,2606,2609,2612,2615,2618,2621,2624,2627,2630,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660,2663,2666,2669,2672,2675,2678,2681,2684,2687,2690,2693,2696,2699,2702,2705,2708,2711,2714,2717,2720,2723,2726,2729,2732,2735,2738,2741],{"path":2198,"title":2199},"\u002Fabout","About Python CLI Toolcraft",{"path":2201,"title":2202},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2204,"title":2205},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2207,"title":2208},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2210,"title":2211},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2213,"title":2214},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2216,"title":2217},"\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":2219,"title":2220},"\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":2222,"title":2223},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2225,"title":2226},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2228,"title":2229},"\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":2231,"title":2232},"\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":2234,"title":2235},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2237,"title":2238},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2240,"title":2241},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2243,"title":2244},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2246,"title":2247},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2249,"title":2250},"\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":2252,"title":2253},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2255,"title":2256},"\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":2258,"title":2259},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2261,"title":2262},"\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":2264,"title":2265},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2267,"title":2268},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2270,"title":2271},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2273,"title":2274},"\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":2276,"title":2277},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2279,"title":2280},"\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":2282,"title":2283},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2285,"title":2286},"\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":2288,"title":2289},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2291,"title":2292},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2294,"title":2295},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2297,"title":2298},"\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":2300,"title":2301},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2303,"title":2304},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2306,"title":2307},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2309,"title":2310},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2312,"title":2313},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2315,"title":2316},"\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":2318,"title":2319},"\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":2321,"title":2322},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2324,"title":2325},"\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":2327,"title":2328},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2330,"title":2331},"\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":2333,"title":2334},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2336,"title":2337},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2339,"title":2340},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2342,"title":2343},"\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":2345,"title":2346},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2348,"title":2349},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2351,"title":2352},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2354,"title":2355},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2357,"title":2358},"\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":2360,"title":2361},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2363,"title":2364},"\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":2366,"title":2367},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2369,"title":2370},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2372,"title":2373},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2375,"title":2376},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2378,"title":2379},"\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":2381,"title":2382},"\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":2384,"title":2385},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2387,"title":2388},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2390,"title":2391},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2393,"title":2394},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2396,"title":2397},"\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":2399,"title":2400},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2402,"title":2403},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2405,"title":2406},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2408,"title":2409},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2411,"title":2412},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2414,"title":2415},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2417,"title":2418},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2420,"title":2421},"\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":2423,"title":2424},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2426,"title":2427},"\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":2429,"title":2430},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2432,"title":2433},"\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":2435,"title":2436},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2438,"title":2439},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2441,"title":2442},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2444,"title":2445},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2447,"title":2448},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2450,"title":2451},"\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":2453,"title":2454},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2188,"title":5},{"path":2457,"title":2458},"\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":2460,"title":2461},"\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":2463,"title":2464},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2466,"title":2467},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2469,"title":2470},"\u002F","Python CLI Toolcraft",{"path":2472,"title":2473},"\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":2475,"title":2476},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2478,"title":2479},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2481,"title":2482},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2484,"title":2485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2487,"title":2488},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2490,"title":2491},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2493,"title":2494},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2496,"title":2497},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2499,"title":2500},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2502,"title":2503},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2505,"title":2506},"\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":2508,"title":2509},"\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":2511,"title":2512},"\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":2514,"title":2515},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2517,"title":2518},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2520,"title":2521},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2523,"title":2524},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2526,"title":2527},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2529,"title":2530},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2532,"title":2533},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2535,"title":2536},"\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":2538,"title":2539},"\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":2541,"title":2542},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2544,"title":2545},"\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":2547,"title":2548},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2550,"title":2551},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2553,"title":2554},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2556,"title":2557},"\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":2559,"title":2560},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2562,"title":2563},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2565,"title":2566},"\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":2568,"title":2569},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2571,"title":2572},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2574,"title":2575},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2577,"title":2578},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2580,"title":2581},"\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":2583,"title":2584},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2586,"title":2587},"\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":2589,"title":2590},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2592,"title":2593},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2595,"title":2596},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2598,"title":2599},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2601,"title":2602},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2604,"title":2605},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2607,"title":2608},"\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":2610,"title":2611},"\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":2613,"title":2614},"\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":2616,"title":2617},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2619,"title":2620},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2622,"title":2623},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2625,"title":2626},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2628,"title":2629},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2631,"title":2632},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2634,"title":2635},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2637,"title":2638},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2640,"title":2641},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2643,"title":2644},"\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":2646,"title":2647},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2649,"title":2650},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2652,"title":2653},"\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":2655,"title":2656},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2658,"title":2659},"\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":2661,"title":2662},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2664,"title":2665},"\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":2667,"title":2668},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2670,"title":2671},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2673,"title":2674},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2676,"title":2677},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2679,"title":2680},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2682,"title":2683},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2685,"title":2686},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2688,"title":2689},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2691,"title":2692},"\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":2694,"title":2695},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2697,"title":2698},"\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":2700,"title":2701},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2703,"title":2704},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2706,"title":2707},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2709,"title":2710},"\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":2712,"title":2713},"\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":2715,"title":2716},"\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":2718,"title":2719},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2721,"title":2722},"\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":2724,"title":2725},"\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":2727,"title":2728},"\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":2730,"title":2731},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2733,"title":2734},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2736,"title":2737},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2739,"title":2740},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2742,"title":2743},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905051]