[{"data":1,"prerenderedAt":1591},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis\u002F":3,"content-directory":1344},{"id":4,"title":5,"body":6,"date":1330,"description":1331,"difficulty":1332,"draft":1333,"extension":1334,"meta":1335,"navigation":148,"path":1336,"seo":1337,"stem":1338,"tags":1339,"updated":1330,"__hash__":1343},"content\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis\u002Findex.md","Reading Piped Input in Python CLIs",{"type":7,"value":8,"toc":1310},"minimark",[9,13,18,62,66,73,77,81,94,418,425,429,432,448,455,529,536,547,551,557,780,791,795,798,807,862,873,876,894,897,901,911,917,928,934,938,941,1188,1191,1195,1201,1205,1210,1213,1217,1220,1224,1237,1241,1253,1257,1260,1264,1267,1271,1306],[10,11,12],"p",{},"A command that accepts piped input can be dropped into the middle of a pipeline, which is most of\nwhat makes a Unix tool useful. Getting it right is four small decisions: when to read stdin, how to\nlet the user ask for it, how to stream it, and which encoding to assume.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,37,48,59],"ul",{},[22,23,24,25,29],"li",{},"Check ",[26,27,28],"code",{},"sys.stdin.isatty()"," before reading — otherwise the tool hangs when run with no arguments.",[22,31,32,33,36],{},"Support ",[26,34,35],{},"-"," as a value meaning \"read stdin\", and document it in the help text.",[22,38,39,43,44,47],{},[40,41,42],"strong",{},"Stream"," rather than ",[26,45,46],{},"read()"," for anything that might be large.",[22,49,50,51,54,55,58],{},"Name the encoding: ",[26,52,53],{},"sys.stdin.reconfigure(encoding=\"utf-8\")",", or use ",[26,56,57],{},"sys.stdin.buffer"," for\nbytes.",[22,60,61],{},"Empty input is a real case — produce a clear message, not a parser traceback.",[14,63,65],{"id":64},"prerequisites","Prerequisites",[10,67,68,69,72],{},"A CLI with a command that takes a document or a list of records. The examples use Typer; the\n",[26,70,71],{},"sys.stdin"," handling is identical in Click and argparse.",[14,74,76],{"id":75},"deciding-whether-to-read-stdin","Deciding whether to read stdin",[78,79],"inline-diagram",{"name":80},"stdin-detection-decision",[10,82,83,85,86,89,90,93],{},[26,84,28],{}," answers the practical question: is there a person at a keyboard, or is\nsomething feeding me data? When it returns ",[26,87,88],{},"False",", stdin is a pipe or a redirected file, and\nreading it is safe. When it returns ",[26,91,92],{},"True",", reading blocks forever on a user who has no idea they\nare supposed to type anything.",[95,96,101],"pre",{"className":97,"code":98,"language":99,"meta":100,"style":100},"language-python shiki shiki-themes github-light github-dark","import sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\n@app.command()\ndef apply(\n    source: Annotated[Path, typer.Argument(help=\"Document to apply, or - for stdin.\")] = Path(\"-\"),\n) -> None:\n    text = read_document(source)\n    ...\n\ndef read_document(source: Path) -> str:\n    if str(source) != \"-\":\n        return source.read_text(encoding=\"utf-8\")\n\n    if sys.stdin.isatty():\n        raise typer.BadParameter(\"no input: pass a file, or pipe a document on stdin\")\n\n    text = sys.stdin.read()\n    if not text.strip():\n        typer.secho(\"no input on stdin (expected a JSON document)\", fg=\"red\", err=True)\n        raise typer.Exit(65)\n\n    return text\n","python","",[26,102,103,116,130,143,150,158,163,173,185,216,229,240,246,251,267,287,307,312,320,334,339,349,360,391,404,409],{"__ignoreMap":100},[104,105,108,112],"span",{"class":106,"line":107},"line",1,[104,109,111],{"class":110},"szBVR","import",[104,113,115],{"class":114},"sVt8B"," sys\n",[104,117,119,122,125,127],{"class":106,"line":118},2,[104,120,121],{"class":110},"from",[104,123,124],{"class":114}," pathlib ",[104,126,111],{"class":110},[104,128,129],{"class":114}," Path\n",[104,131,133,135,138,140],{"class":106,"line":132},3,[104,134,121],{"class":110},[104,136,137],{"class":114}," typing ",[104,139,111],{"class":110},[104,141,142],{"class":114}," Annotated\n",[104,144,146],{"class":106,"line":145},4,[104,147,149],{"emptyLinePlaceholder":148},true,"\n",[104,151,153,155],{"class":106,"line":152},5,[104,154,111],{"class":110},[104,156,157],{"class":114}," typer\n",[104,159,161],{"class":106,"line":160},6,[104,162,149],{"emptyLinePlaceholder":148},[104,164,166,170],{"class":106,"line":165},7,[104,167,169],{"class":168},"sScJk","@app.command",[104,171,172],{"class":114},"()\n",[104,174,176,179,182],{"class":106,"line":175},8,[104,177,178],{"class":110},"def",[104,180,181],{"class":168}," apply",[104,183,184],{"class":114},"(\n",[104,186,188,191,195,198,202,205,207,210,213],{"class":106,"line":187},9,[104,189,190],{"class":114},"    source: Annotated[Path, typer.Argument(",[104,192,194],{"class":193},"s4XuR","help",[104,196,197],{"class":110},"=",[104,199,201],{"class":200},"sZZnC","\"Document to apply, or - for stdin.\"",[104,203,204],{"class":114},")] ",[104,206,197],{"class":110},[104,208,209],{"class":114}," Path(",[104,211,212],{"class":200},"\"-\"",[104,214,215],{"class":114},"),\n",[104,217,219,222,226],{"class":106,"line":218},10,[104,220,221],{"class":114},") -> ",[104,223,225],{"class":224},"sj4cs","None",[104,227,228],{"class":114},":\n",[104,230,232,235,237],{"class":106,"line":231},11,[104,233,234],{"class":114},"    text ",[104,236,197],{"class":110},[104,238,239],{"class":114}," read_document(source)\n",[104,241,243],{"class":106,"line":242},12,[104,244,245],{"class":224},"    ...\n",[104,247,249],{"class":106,"line":248},13,[104,250,149],{"emptyLinePlaceholder":148},[104,252,254,256,259,262,265],{"class":106,"line":253},14,[104,255,178],{"class":110},[104,257,258],{"class":168}," read_document",[104,260,261],{"class":114},"(source: Path) -> ",[104,263,264],{"class":224},"str",[104,266,228],{"class":114},[104,268,270,273,276,279,282,285],{"class":106,"line":269},15,[104,271,272],{"class":110},"    if",[104,274,275],{"class":224}," str",[104,277,278],{"class":114},"(source) ",[104,280,281],{"class":110},"!=",[104,283,284],{"class":200}," \"-\"",[104,286,228],{"class":114},[104,288,290,293,296,299,301,304],{"class":106,"line":289},16,[104,291,292],{"class":110},"        return",[104,294,295],{"class":114}," source.read_text(",[104,297,298],{"class":193},"encoding",[104,300,197],{"class":110},[104,302,303],{"class":200},"\"utf-8\"",[104,305,306],{"class":114},")\n",[104,308,310],{"class":106,"line":309},17,[104,311,149],{"emptyLinePlaceholder":148},[104,313,315,317],{"class":106,"line":314},18,[104,316,272],{"class":110},[104,318,319],{"class":114}," sys.stdin.isatty():\n",[104,321,323,326,329,332],{"class":106,"line":322},19,[104,324,325],{"class":110},"        raise",[104,327,328],{"class":114}," typer.BadParameter(",[104,330,331],{"class":200},"\"no input: pass a file, or pipe a document on stdin\"",[104,333,306],{"class":114},[104,335,337],{"class":106,"line":336},20,[104,338,149],{"emptyLinePlaceholder":148},[104,340,342,344,346],{"class":106,"line":341},21,[104,343,234],{"class":114},[104,345,197],{"class":110},[104,347,348],{"class":114}," sys.stdin.read()\n",[104,350,352,354,357],{"class":106,"line":351},22,[104,353,272],{"class":110},[104,355,356],{"class":110}," not",[104,358,359],{"class":114}," text.strip():\n",[104,361,363,366,369,372,375,377,380,382,385,387,389],{"class":106,"line":362},23,[104,364,365],{"class":114},"        typer.secho(",[104,367,368],{"class":200},"\"no input on stdin (expected a JSON document)\"",[104,370,371],{"class":114},", ",[104,373,374],{"class":193},"fg",[104,376,197],{"class":110},[104,378,379],{"class":200},"\"red\"",[104,381,371],{"class":114},[104,383,384],{"class":193},"err",[104,386,197],{"class":110},[104,388,92],{"class":224},[104,390,306],{"class":114},[104,392,394,396,399,402],{"class":106,"line":393},24,[104,395,325],{"class":110},[104,397,398],{"class":114}," typer.Exit(",[104,400,401],{"class":224},"65",[104,403,306],{"class":114},[104,405,407],{"class":106,"line":406},25,[104,408,149],{"emptyLinePlaceholder":148},[104,410,412,415],{"class":106,"line":411},26,[104,413,414],{"class":110},"    return",[104,416,417],{"class":114}," text\n",[10,419,420,421,424],{},"The empty-input branch is the one people leave out, and it is the case real users hit — an upstream\ncommand produced nothing, and without the check the failure is a ",[26,422,423],{},"JSONDecodeError"," traceback that\nsays nothing about where the input was supposed to come from.",[14,426,428],{"id":427},"the-dash-convention","The dash convention",[78,430],{"name":431},"dash-convention",[10,433,434,435,437,438,371,441,371,444,447],{},"A bare ",[26,436,35],{}," where a filename is expected means \"use the standard stream\". It is a convention rather\nthan a language feature, which means you implement it — and users of ",[26,439,440],{},"cat",[26,442,443],{},"jq",[26,445,446],{},"tar"," and every\nother Unix tool expect it to be there.",[10,449,450,451,454],{},"Click's ",[26,452,453],{},"File"," type handles it for you:",[95,456,458],{"className":97,"code":457,"language":99,"meta":100,"style":100},"@click.command()\n@click.argument(\"source\", type=click.File(\"r\"), default=\"-\")\ndef apply(source) -> None:\n    text = source.read()          # a file object, or stdin when the user typed -\n",[26,459,460,467,503,516],{"__ignoreMap":100},[104,461,462,465],{"class":106,"line":107},[104,463,464],{"class":168},"@click.command",[104,466,172],{"class":114},[104,468,469,472,475,478,480,483,485,488,491,494,497,499,501],{"class":106,"line":118},[104,470,471],{"class":168},"@click.argument",[104,473,474],{"class":114},"(",[104,476,477],{"class":200},"\"source\"",[104,479,371],{"class":114},[104,481,482],{"class":193},"type",[104,484,197],{"class":110},[104,486,487],{"class":114},"click.File(",[104,489,490],{"class":200},"\"r\"",[104,492,493],{"class":114},"), ",[104,495,496],{"class":193},"default",[104,498,197],{"class":110},[104,500,212],{"class":200},[104,502,306],{"class":114},[104,504,505,507,509,512,514],{"class":106,"line":132},[104,506,178],{"class":110},[104,508,181],{"class":168},[104,510,511],{"class":114},"(source) -> ",[104,513,225],{"class":224},[104,515,228],{"class":114},[104,517,518,520,522,525],{"class":106,"line":145},[104,519,234],{"class":114},[104,521,197],{"class":110},[104,523,524],{"class":114}," source.read()          ",[104,526,528],{"class":527},"sJ8bj","# a file object, or stdin when the user typed -\n",[10,530,531,532,535],{},"In Typer, the equivalent is the explicit branch above, which is a few more lines and clearer about\nwhat happens. Either way, say so in the help text — ",[26,533,534],{},"\"Document to apply, or - for stdin\""," — because\na convention nobody documents is a convention half your users will not try.",[10,537,538,539,542,543,546],{},"Support it for output too. ",[26,540,541],{},"mytool export -o -"," writing to stdout lets someone pipe your results\ninto ",[26,544,545],{},"gzip"," or another tool without a temporary file.",[14,548,550],{"id":549},"streaming-large-inputs","Streaming large inputs",[10,552,553,556],{},[26,554,555],{},"sys.stdin.read()"," loads the whole stream into memory. For a config document that is fine; for a\nlog file, an export or anything a user might generate, it is a memory limit you did not intend to\nset.",[95,558,560],{"className":97,"code":559,"language":99,"meta":100,"style":100},"def read_records(source: Path) -> Iterator[dict]:\n    \"\"\"Yield one parsed record per line, from a file or stdin.\"\"\"\n    stream = sys.stdin if str(source) == \"-\" else source.open(encoding=\"utf-8\")\n    try:\n        for number, line in enumerate(stream, start=1):\n            line = line.strip()\n            if not line:\n                continue\n            try:\n                yield json.loads(line)\n            except json.JSONDecodeError as exc:\n                raise InputDataError(f\"line {number}: {exc.msg}\") from exc\n    finally:\n        if stream is not sys.stdin:\n            stream.close()\n",[26,561,562,578,583,619,626,654,664,674,679,686,694,708,752,759,775],{"__ignoreMap":100},[104,563,564,566,569,572,575],{"class":106,"line":107},[104,565,178],{"class":110},[104,567,568],{"class":168}," read_records",[104,570,571],{"class":114},"(source: Path) -> Iterator[",[104,573,574],{"class":224},"dict",[104,576,577],{"class":114},"]:\n",[104,579,580],{"class":106,"line":118},[104,581,582],{"class":200},"    \"\"\"Yield one parsed record per line, from a file or stdin.\"\"\"\n",[104,584,585,588,590,593,596,598,600,603,605,608,611,613,615,617],{"class":106,"line":132},[104,586,587],{"class":114},"    stream ",[104,589,197],{"class":110},[104,591,592],{"class":114}," sys.stdin ",[104,594,595],{"class":110},"if",[104,597,275],{"class":224},[104,599,278],{"class":114},[104,601,602],{"class":110},"==",[104,604,284],{"class":200},[104,606,607],{"class":110}," else",[104,609,610],{"class":114}," source.open(",[104,612,298],{"class":193},[104,614,197],{"class":110},[104,616,303],{"class":200},[104,618,306],{"class":114},[104,620,621,624],{"class":106,"line":145},[104,622,623],{"class":110},"    try",[104,625,228],{"class":114},[104,627,628,631,634,637,640,643,646,648,651],{"class":106,"line":152},[104,629,630],{"class":110},"        for",[104,632,633],{"class":114}," number, line ",[104,635,636],{"class":110},"in",[104,638,639],{"class":224}," enumerate",[104,641,642],{"class":114},"(stream, ",[104,644,645],{"class":193},"start",[104,647,197],{"class":110},[104,649,650],{"class":224},"1",[104,652,653],{"class":114},"):\n",[104,655,656,659,661],{"class":106,"line":160},[104,657,658],{"class":114},"            line ",[104,660,197],{"class":110},[104,662,663],{"class":114}," line.strip()\n",[104,665,666,669,671],{"class":106,"line":165},[104,667,668],{"class":110},"            if",[104,670,356],{"class":110},[104,672,673],{"class":114}," line:\n",[104,675,676],{"class":106,"line":175},[104,677,678],{"class":110},"                continue\n",[104,680,681,684],{"class":106,"line":187},[104,682,683],{"class":110},"            try",[104,685,228],{"class":114},[104,687,688,691],{"class":106,"line":218},[104,689,690],{"class":110},"                yield",[104,692,693],{"class":114}," json.loads(line)\n",[104,695,696,699,702,705],{"class":106,"line":231},[104,697,698],{"class":110},"            except",[104,700,701],{"class":114}," json.JSONDecodeError ",[104,703,704],{"class":110},"as",[104,706,707],{"class":114}," exc:\n",[104,709,710,713,716,719,722,725,728,731,734,736,739,741,744,747,749],{"class":106,"line":242},[104,711,712],{"class":110},"                raise",[104,714,715],{"class":114}," InputDataError(",[104,717,718],{"class":110},"f",[104,720,721],{"class":200},"\"line ",[104,723,724],{"class":224},"{",[104,726,727],{"class":114},"number",[104,729,730],{"class":224},"}",[104,732,733],{"class":200},": ",[104,735,724],{"class":224},[104,737,738],{"class":114},"exc.msg",[104,740,730],{"class":224},[104,742,743],{"class":200},"\"",[104,745,746],{"class":114},") ",[104,748,121],{"class":110},[104,750,751],{"class":114}," exc\n",[104,753,754,757],{"class":106,"line":248},[104,755,756],{"class":110},"    finally",[104,758,228],{"class":114},[104,760,761,764,767,770,772],{"class":106,"line":253},[104,762,763],{"class":110},"        if",[104,765,766],{"class":114}," stream ",[104,768,769],{"class":110},"is",[104,771,356],{"class":110},[104,773,774],{"class":114}," sys.stdin:\n",[104,776,777],{"class":106,"line":269},[104,778,779],{"class":114},"            stream.close()\n",[10,781,782,783,786,787,790],{},"Three properties are worth copying. It ",[40,784,785],{},"yields"," rather than collecting, so memory stays flat\nwhatever the input size. It reports the ",[40,788,789],{},"line number"," in errors, which is the difference between\na fixable message and a shrug on a million-line file. And it closes the file it opened while\nleaving stdin alone, because closing stdin in a library function is the kind of surprise that takes\nan afternoon to find.",[14,792,794],{"id":793},"encodings-and-binary-data","Encodings and binary data",[78,796],{"name":797},"encoding-pitfalls",[10,799,800,802,803,806],{},[26,801,71],{}," decodes using the platform default, which varies by operating system and locale. On a\nmachine with a non-UTF-8 locale, the same pipeline that works for you fails with a\n",[26,804,805],{},"UnicodeDecodeError"," for someone else.",[95,808,810],{"className":97,"code":809,"language":99,"meta":100,"style":100},"sys.stdin.reconfigure(encoding=\"utf-8\", errors=\"strict\")\nsys.stdout.reconfigure(encoding=\"utf-8\", newline=\"\\n\")\n",[26,811,812,835],{"__ignoreMap":100},[104,813,814,817,819,821,823,825,828,830,833],{"class":106,"line":107},[104,815,816],{"class":114},"sys.stdin.reconfigure(",[104,818,298],{"class":193},[104,820,197],{"class":110},[104,822,303],{"class":200},[104,824,371],{"class":114},[104,826,827],{"class":193},"errors",[104,829,197],{"class":110},[104,831,832],{"class":200},"\"strict\"",[104,834,306],{"class":114},[104,836,837,840,842,844,846,848,851,853,855,858,860],{"class":106,"line":118},[104,838,839],{"class":114},"sys.stdout.reconfigure(",[104,841,298],{"class":193},[104,843,197],{"class":110},[104,845,303],{"class":200},[104,847,371],{"class":114},[104,849,850],{"class":193},"newline",[104,852,197],{"class":110},[104,854,743],{"class":200},[104,856,857],{"class":224},"\\n",[104,859,743],{"class":200},[104,861,306],{"class":114},[10,863,864,865,868,869,872],{},"Two lines near the top of ",[26,866,867],{},"main()"," remove the variable entirely. Use ",[26,870,871],{},"errors=\"replace\""," only where\ngarbled text genuinely beats a failure — for a log viewer, perhaps; not for a config parser, where\nsilently mangling input is worse than refusing it.",[10,874,875],{},"When the input is not text at all — an image, a compressed archive, a binary protocol — read the\nbuffer directly:",[95,877,879],{"className":97,"code":878,"language":99,"meta":100,"style":100},"payload = sys.stdin.buffer.read()          # bytes, no decoding attempted\n",[26,880,881],{"__ignoreMap":100},[104,882,883,886,888,891],{"class":106,"line":107},[104,884,885],{"class":114},"payload ",[104,887,197],{"class":110},[104,889,890],{"class":114}," sys.stdin.buffer.read()          ",[104,892,893],{"class":527},"# bytes, no decoding attempted\n",[10,895,896],{},"And when both are possible, decide from a flag rather than by guessing: a heuristic that inspects\nthe first bytes is right most of the time and produces a baffling failure the rest.",[14,898,900],{"id":899},"ux-considerations","UX considerations",[10,902,903,906,907,910],{},[40,904,905],{},"Do not block silently."," If your command is waiting for stdin, the user sees nothing. Either\nrefuse (with a message naming the argument) or, if waiting is genuinely intended, say so on stderr:\n",[26,908,909],{},"reading from stdin; press Ctrl-D when done",".",[10,912,913,916],{},[40,914,915],{},"Report progress on stderr."," A command chewing through a large piped input should show that it is\nalive, and that narration must not enter the pipeline.",[10,918,919,922,923,910],{},[40,920,921],{},"Handle Ctrl-C cleanly."," Reading a long stream is exactly when people interrupt. Exit 130 without\na traceback, as described in\n",[924,925,927],"a",{"href":926},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002F","handling keyboard interrupt cleanly",[10,929,930,933],{},[40,931,932],{},"Never prompt while reading stdin."," If your command consumes piped data, stdin is not available\nfor questions — any confirmation must come from a flag. This is the constraint that most often\nreveals a command trying to be interactive and scriptable in the same code path.",[14,935,937],{"id":936},"testing-the-behaviour","Testing the behaviour",[10,939,940],{},"The runner supplies stdin directly, so every case is testable in-process:",[95,942,944],{"className":97,"code":943,"language":99,"meta":100,"style":100},"def test_reads_a_document_from_stdin(cli):\n    result = cli.invoke(app, [\"apply\", \"-\"], input='{\"replicas\": 3}')\n    assert result.exit_code == 0\n\ndef test_empty_stdin_is_a_clear_error(cli):\n    result = cli.invoke(app, [\"apply\", \"-\"], input=\"\")\n    assert result.exit_code == 65\n    assert \"no input\" in result.stderr\n\ndef test_malformed_line_names_the_line_number(cli):\n    result = cli.invoke(app, [\"apply\", \"-\"], input='{\"ok\": 1}\\nnot json\\n')\n    assert result.exit_code == 65\n    assert \"line 2\" in result.stderr\n\ndef test_file_argument_still_works(cli, tmp_path):\n    doc = tmp_path \u002F \"job.json\"\n    doc.write_text('{\"replicas\": 3}')\n    assert cli.invoke(app, [\"apply\", str(doc)]).exit_code == 0\n",[26,945,946,956,986,999,1003,1012,1037,1048,1061,1065,1074,1109,1119,1130,1134,1144,1160,1169],{"__ignoreMap":100},[104,947,948,950,953],{"class":106,"line":107},[104,949,178],{"class":110},[104,951,952],{"class":168}," test_reads_a_document_from_stdin",[104,954,955],{"class":114},"(cli):\n",[104,957,958,961,963,966,969,971,973,976,979,981,984],{"class":106,"line":118},[104,959,960],{"class":114},"    result ",[104,962,197],{"class":110},[104,964,965],{"class":114}," cli.invoke(app, [",[104,967,968],{"class":200},"\"apply\"",[104,970,371],{"class":114},[104,972,212],{"class":200},[104,974,975],{"class":114},"], ",[104,977,978],{"class":193},"input",[104,980,197],{"class":110},[104,982,983],{"class":200},"'{\"replicas\": 3}'",[104,985,306],{"class":114},[104,987,988,991,994,996],{"class":106,"line":132},[104,989,990],{"class":110},"    assert",[104,992,993],{"class":114}," result.exit_code ",[104,995,602],{"class":110},[104,997,998],{"class":224}," 0\n",[104,1000,1001],{"class":106,"line":145},[104,1002,149],{"emptyLinePlaceholder":148},[104,1004,1005,1007,1010],{"class":106,"line":152},[104,1006,178],{"class":110},[104,1008,1009],{"class":168}," test_empty_stdin_is_a_clear_error",[104,1011,955],{"class":114},[104,1013,1014,1016,1018,1020,1022,1024,1026,1028,1030,1032,1035],{"class":106,"line":160},[104,1015,960],{"class":114},[104,1017,197],{"class":110},[104,1019,965],{"class":114},[104,1021,968],{"class":200},[104,1023,371],{"class":114},[104,1025,212],{"class":200},[104,1027,975],{"class":114},[104,1029,978],{"class":193},[104,1031,197],{"class":110},[104,1033,1034],{"class":200},"\"\"",[104,1036,306],{"class":114},[104,1038,1039,1041,1043,1045],{"class":106,"line":165},[104,1040,990],{"class":110},[104,1042,993],{"class":114},[104,1044,602],{"class":110},[104,1046,1047],{"class":224}," 65\n",[104,1049,1050,1052,1055,1058],{"class":106,"line":175},[104,1051,990],{"class":110},[104,1053,1054],{"class":200}," \"no input\"",[104,1056,1057],{"class":110}," in",[104,1059,1060],{"class":114}," result.stderr\n",[104,1062,1063],{"class":106,"line":187},[104,1064,149],{"emptyLinePlaceholder":148},[104,1066,1067,1069,1072],{"class":106,"line":218},[104,1068,178],{"class":110},[104,1070,1071],{"class":168}," test_malformed_line_names_the_line_number",[104,1073,955],{"class":114},[104,1075,1076,1078,1080,1082,1084,1086,1088,1090,1092,1094,1097,1099,1102,1104,1107],{"class":106,"line":231},[104,1077,960],{"class":114},[104,1079,197],{"class":110},[104,1081,965],{"class":114},[104,1083,968],{"class":200},[104,1085,371],{"class":114},[104,1087,212],{"class":200},[104,1089,975],{"class":114},[104,1091,978],{"class":193},[104,1093,197],{"class":110},[104,1095,1096],{"class":200},"'{\"ok\": 1}",[104,1098,857],{"class":224},[104,1100,1101],{"class":200},"not json",[104,1103,857],{"class":224},[104,1105,1106],{"class":200},"'",[104,1108,306],{"class":114},[104,1110,1111,1113,1115,1117],{"class":106,"line":242},[104,1112,990],{"class":110},[104,1114,993],{"class":114},[104,1116,602],{"class":110},[104,1118,1047],{"class":224},[104,1120,1121,1123,1126,1128],{"class":106,"line":248},[104,1122,990],{"class":110},[104,1124,1125],{"class":200}," \"line 2\"",[104,1127,1057],{"class":110},[104,1129,1060],{"class":114},[104,1131,1132],{"class":106,"line":253},[104,1133,149],{"emptyLinePlaceholder":148},[104,1135,1136,1138,1141],{"class":106,"line":269},[104,1137,178],{"class":110},[104,1139,1140],{"class":168}," test_file_argument_still_works",[104,1142,1143],{"class":114},"(cli, tmp_path):\n",[104,1145,1146,1149,1151,1154,1157],{"class":106,"line":289},[104,1147,1148],{"class":114},"    doc ",[104,1150,197],{"class":110},[104,1152,1153],{"class":114}," tmp_path ",[104,1155,1156],{"class":110},"\u002F",[104,1158,1159],{"class":200}," \"job.json\"\n",[104,1161,1162,1165,1167],{"class":106,"line":309},[104,1163,1164],{"class":114},"    doc.write_text(",[104,1166,983],{"class":200},[104,1168,306],{"class":114},[104,1170,1171,1173,1175,1177,1179,1181,1184,1186],{"class":106,"line":314},[104,1172,990],{"class":110},[104,1174,965],{"class":114},[104,1176,968],{"class":200},[104,1178,371],{"class":114},[104,1180,264],{"class":224},[104,1182,1183],{"class":114},"(doc)]).exit_code ",[104,1185,602],{"class":110},[104,1187,998],{"class":224},[10,1189,1190],{},"Four tests covering stdin, the empty case, an error message that helps, and the file path — which\nbetween them are almost every way this code is used.",[14,1192,1194],{"id":1193},"conclusion","Conclusion",[10,1196,1197,1198,1200],{},"Reading stdin well is mostly about not surprising anyone: check before reading so the tool never\nhangs, honour ",[26,1199,35],{}," so it composes, stream so size is not a limit, and name the encoding so it behaves\nthe same on every machine. Four small decisions, and your command can sit anywhere in a pipeline.",[14,1202,1204],{"id":1203},"frequently-asked-questions","Frequently asked questions",[1206,1207,1209],"h3",{"id":1208},"can-i-tell-whether-piped-input-is-empty-before-reading-it","Can I tell whether piped input is empty before reading it?",[10,1211,1212],{},"Not reliably — a pipe with no data yet looks the same as one that will never have any. Read and\ncheck what you got, and produce a message that names both the expectation and the source, so the\nuser knows whether to look upstream or at their own command.",[1206,1214,1216],{"id":1215},"what-if-a-command-needs-both-piped-data-and-a-confirmation","What if a command needs both piped data and a confirmation?",[10,1218,1219],{},"Take the confirmation as a flag. Once stdin carries data it cannot also carry answers, so a prompt\nwould either consume part of your input or fail. This is a good example of a constraint that\nimproves the design: the command becomes scriptable because it has to.",[1206,1221,1223],{"id":1222},"how-do-i-read-stdin-in-argparse","How do I read stdin in argparse?",[10,1225,1226,1227,1229,1230,1233,1234,1236],{},"Identically — ",[26,1228,71],{}," is not framework-specific. ",[26,1231,1232],{},"argparse.FileType(\"r\")"," also handles the ",[26,1235,35],{},"\nconvention, at the cost of opening the file during parsing, which makes error handling slightly\nless controllable than opening it yourself.",[1206,1238,1240],{"id":1239},"should-i-support-reading-multiple-files-as-well-as-stdin","Should I support reading multiple files as well as stdin?",[10,1242,1243,1244,1246,1247,371,1249,1252],{},"If the command is a filter, yes: accept a list of paths, treat an empty list as \"read stdin\", and\ntreat ",[26,1245,35],{}," in the list as stdin too. That matches ",[26,1248,440],{},[26,1250,1251],{},"grep"," and everything else people expect to\ncombine your tool with.",[1206,1254,1256],{"id":1255},"is-there-a-performance-cost-to-iterating-stdin-line-by-line","Is there a performance cost to iterating stdin line by line?",[10,1258,1259],{},"Negligible — Python buffers the underlying reads, so line iteration is not one syscall per line.\nThe cost that does matter is what you do per line; parsing JSON a million times is the expensive\npart, not the reading.",[1206,1261,1263],{"id":1262},"can-i-accept-both-a-piped-document-and-command-line-overrides","Can I accept both a piped document and command-line overrides?",[10,1265,1266],{},"Yes, and the useful rule is that flags win. Read the document, apply it as the base, then overlay\nany values the user passed explicitly — the same precedence as configuration files. Document it in\none line, because a user piping a document and also passing a flag has a reasonable expectation\neither way and only one of them is true.",[14,1268,1270],{"id":1269},"related","Related",[19,1272,1273,1280,1287,1293,1300],{},[22,1274,1275,1276],{},"Up: ",[924,1277,1279],{"href":1278},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","Working with stdin, stdout and pipes",[22,1281,1282,1283],{},"Sideways: ",[924,1284,1286],{"href":1285},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F","Emitting JSON output for scripting",[22,1288,1282,1289],{},[924,1290,1292],{"href":1291},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe\u002F","Handling broken pipe and SIGPIPE",[22,1294,1295,1296],{},"Related: ",[924,1297,1299],{"href":1298},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis\u002F","Parsing nested JSON arguments in Python CLIs",[22,1301,1295,1302],{},[924,1303,1305],{"href":1304},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002F","Testing interactive prompts and stdin",[1307,1308,1309],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":100,"searchDepth":118,"depth":118,"links":1311},[1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1329],{"id":16,"depth":118,"text":17},{"id":64,"depth":118,"text":65},{"id":75,"depth":118,"text":76},{"id":427,"depth":118,"text":428},{"id":549,"depth":118,"text":550},{"id":793,"depth":118,"text":794},{"id":899,"depth":118,"text":900},{"id":936,"depth":118,"text":937},{"id":1193,"depth":118,"text":1194},{"id":1203,"depth":118,"text":1204,"children":1322},[1323,1324,1325,1326,1327,1328],{"id":1208,"depth":132,"text":1209},{"id":1215,"depth":132,"text":1216},{"id":1222,"depth":132,"text":1223},{"id":1239,"depth":132,"text":1240},{"id":1255,"depth":132,"text":1256},{"id":1262,"depth":132,"text":1263},{"id":1269,"depth":118,"text":1270},"2026-08-01","Read stdin safely in a Python CLI - detect piped data, support the dash convention, stream large inputs, and handle encodings without platform surprises.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis",{"title":5,"description":1331},"advanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis\u002Findex",[1340,1341,978,298,1342],"stdin","pipes","streaming","L45PpNMiFCpZ6xh-FU2z8FwYukcvloVEqsUFCQEaoCs",[1345,1348,1351,1354,1357,1360,1363,1366,1369,1372,1375,1378,1381,1384,1387,1390,1393,1396,1399,1402,1405,1408,1411,1414,1417,1420,1423,1426,1429,1432,1435,1436,1438,1441,1444,1447,1450,1453,1456,1459,1462,1465,1468,1471,1474,1477,1480,1483,1486,1489,1492,1495,1498,1501,1504,1507,1510,1513,1516,1519,1522,1525,1528,1531,1534,1537,1540,1543,1546,1549,1552,1555,1558,1561,1564,1567,1570,1573,1576,1579,1582,1585,1588],{"path":1346,"title":1347},"\u002Fabout","About Python CLI Toolcraft",{"path":1349,"title":1350},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1352,"title":1353},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1355,"title":1356},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1358,"title":1359},"\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":1361,"title":1362},"\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":1364,"title":1365},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1367,"title":1368},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1370,"title":1371},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1373,"title":1374},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1376,"title":1377},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1379,"title":1380},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1382,"title":1383},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1385,"title":1386},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1388,"title":1389},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1391,"title":1392},"\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":1394,"title":1395},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1397,"title":1398},"\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":1400,"title":1401},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1403,"title":1404},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1406,"title":1407},"\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":1409,"title":1410},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1412,"title":1413},"\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":1415,"title":1416},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1418,"title":1419},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1421,"title":1422},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1424,"title":1425},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1427,"title":1428},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1430,"title":1431},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1433,"title":1434},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1336,"title":5},{"path":1156,"title":1437},"Python CLI Toolcraft",{"path":1439,"title":1440},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1442,"title":1443},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1445,"title":1446},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1448,"title":1449},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1451,"title":1452},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1454,"title":1455},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1457,"title":1458},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1460,"title":1461},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1463,"title":1464},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1466,"title":1467},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1469,"title":1470},"\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":1472,"title":1473},"\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":1475,"title":1476},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1478,"title":1479},"\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":1481,"title":1482},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1484,"title":1485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1487,"title":1488},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1490,"title":1491},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1493,"title":1494},"\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":1496,"title":1497},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1499,"title":1500},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1502,"title":1503},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1505,"title":1506},"\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":1508,"title":1509},"\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":1511,"title":1512},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1514,"title":1515},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1517,"title":1518},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1520,"title":1521},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1523,"title":1524},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1526,"title":1527},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1529,"title":1530},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1532,"title":1533},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1535,"title":1536},"\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":1538,"title":1539},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1541,"title":1542},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1544,"title":1545},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1547,"title":1548},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1550,"title":1551},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1553,"title":1554},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1556,"title":1557},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1559,"title":1560},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1562,"title":1563},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1565,"title":1566},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1568,"title":1569},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1571,"title":1572},"\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":1574,"title":1575},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1577,"title":1578},"\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":1580,"title":1581},"\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":1583,"title":1584},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1586,"title":1587},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1589,"title":1590},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690031]