[{"data":1,"prerenderedAt":3222},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility\u002F":3,"content-directory":2378},{"id":4,"title":5,"body":6,"date":2364,"description":2365,"difficulty":2366,"draft":2367,"extension":2368,"meta":2369,"navigation":164,"path":2370,"seo":2371,"stem":2372,"tags":2373,"updated":2364,"__hash__":2377},"content\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility\u002Findex.md","Null-Delimited Input and xargs Compatibility in Python CLIs",{"type":7,"value":8,"toc":2342},"minimark",[9,48,53,78,82,86,119,123,672,1100,1103,1181,1186,1200,1206,1210,1246,1250,1267,1270,1274,1277,1327,1331,1400,1403,1407,1418,2197,2208,2212,2232,2236,2240,2249,2253,2264,2268,2280,2284,2287,2294,2305,2309,2338],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37,38,41,42,47],"p",{},"File names may contain spaces, tabs, quotes, non-UTF-8 bytes and even newlines. The only bytes a Unix file name cannot contain are ",[14,15,16],"code",{},"\u002F"," and NUL. That is why the classic pipeline idiom for file lists is ",[14,19,20],{},"find . -print0 | xargs -0 …",": separating names with NUL instead of newline is the only format that cannot be fooled by any name. A CLI that reads paths from stdin one per line works fine — until somebody has a file called ",[14,23,24],{},"notes\\nold.txt",", and the tool sees two paths that do not exist. If your CLI reads or prints lists of paths or other arbitrary strings, supporting NUL-delimited records is how it joins these pipelines safely. This guide adds the conventional ",[14,27,28],{},"-0"," (input) and ",[14,31,32],{},"-z"," (output) flags, streams records without loading everything into memory, preserves undecodable file names byte for byte, and tests the whole thing against real ",[14,35,36],{},"find"," and ",[14,39,40],{},"xargs",". It belongs to the ",[43,44,46],"a",{"href":45},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","stdin, stdout and pipes topic",".",[49,50,52],"h2",{"id":51},"prerequisites","Prerequisites",[54,55,56,64],"ul",{},[57,58,59,60,47],"li",{},"A CLI that reads from stdin, as in ",[43,61,63],{"href":62},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis\u002F","reading piped input in Python CLIs",[57,65,66,67,37,69,71,72,37,75,77],{},"A Unix-like shell with ",[14,68,36],{},[14,70,40],{}," to try the pipelines (GNU and BSD versions both support ",[14,73,74],{},"-print0",[14,76,28],{},").",[49,79,81],{"id":80},"why-newline-is-not-enough","Why newline is not enough",[83,84],"inline-diagram",{"name":85},"nul-why",[10,87,88,89,92,93,96,97,96,100,96,103,96,106,96,109,96,112,37,115,118],{},"With newline-separated input, the separator is a character that can appear inside a record, so a single name with a newline is indistinguishable from two names. Quoting schemes (",[14,90,91],{},"ls -b",", shell escaping) solve this only if both sides agree on them. NUL works because it is guaranteed never to appear in a path, nor in most text arguments — C strings, and therefore every Unix command-line argument and environment variable, cannot contain it. The ecosystem converged on it: ",[14,94,95],{},"find -print0",", ",[14,98,99],{},"xargs -0",[14,101,102],{},"git ls-files -z",[14,104,105],{},"grep -lZ",[14,107,108],{},"sort -z",[14,110,111],{},"fd -0",[14,113,114],{},"rg --null",[14,116,117],{},"du -0"," all speak the format.",[49,120,122],{"id":121},"the-recipe","The recipe",[124,125,130],"pre",{"className":126,"code":127,"language":128,"meta":129,"style":129},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Frecords.py\nfrom __future__ import annotations\n\nimport sys\nfrom collections.abc import Iterable, Iterator\nfrom typing import BinaryIO\n\n\ndef read_records(stream: BinaryIO, *, null: bool, chunk_size: int = 65536) -> Iterator[str]:\n    \"\"\"Yield records separated by NUL (null=True) or newline, decoded like file names.\"\"\"\n    sep = b\"\\0\" if null else b\"\\n\"\n    buffer = b\"\"\n    while chunk := stream.read(chunk_size):\n        buffer += chunk\n        *complete, buffer = buffer.split(sep)\n        for raw in complete:\n            yield from _decode(raw, null)\n    if buffer:\n        yield from _decode(buffer, null)\n\n\ndef _decode(raw: bytes, null: bool) -> Iterator[str]:\n    if not null:\n        raw = raw.removesuffix(b\"\\r\")             # tolerate CRLF input on newline mode\n    if raw:                                       # empty records (blank lines, \"\\0\\0\") are skipped\n        yield raw.decode(sys.getfilesystemencoding(), \"surrogateescape\")\n\n\ndef write_records(records: Iterable[str], stream: BinaryIO, *, null: bool) -> None:\n    sep = b\"\\0\" if null else b\"\\n\"\n    for record in records:\n        data = record.encode(sys.getfilesystemencoding(), \"surrogateescape\")\n        if not null and b\"\\n\" in data:\n            raise ValueError(f\"{record!r} contains a newline; use -0\u002F--null\")\n        stream.write(data + sep)\n    stream.flush()\n","python","",[14,131,132,141,159,166,175,188,201,206,211,254,261,300,313,328,340,354,369,378,387,396,401,406,430,441,468,479,494,499,504,535,564,578,593,620,654,666],{"__ignoreMap":129},[133,134,137],"span",{"class":135,"line":136},"line",1,[133,138,140],{"class":139},"sJ8bj","# src\u002Fmytool\u002Frecords.py\n",[133,142,144,148,152,155],{"class":135,"line":143},2,[133,145,147],{"class":146},"szBVR","from",[133,149,151],{"class":150},"sj4cs"," __future__",[133,153,154],{"class":146}," import",[133,156,158],{"class":157},"sVt8B"," annotations\n",[133,160,162],{"class":135,"line":161},3,[133,163,165],{"emptyLinePlaceholder":164},true,"\n",[133,167,169,172],{"class":135,"line":168},4,[133,170,171],{"class":146},"import",[133,173,174],{"class":157}," sys\n",[133,176,178,180,183,185],{"class":135,"line":177},5,[133,179,147],{"class":146},[133,181,182],{"class":157}," collections.abc ",[133,184,171],{"class":146},[133,186,187],{"class":157}," Iterable, Iterator\n",[133,189,191,193,196,198],{"class":135,"line":190},6,[133,192,147],{"class":146},[133,194,195],{"class":157}," typing ",[133,197,171],{"class":146},[133,199,200],{"class":157}," BinaryIO\n",[133,202,204],{"class":135,"line":203},7,[133,205,165],{"emptyLinePlaceholder":164},[133,207,209],{"class":135,"line":208},8,[133,210,165],{"emptyLinePlaceholder":164},[133,212,214,217,221,224,227,230,233,236,239,242,245,248,251],{"class":135,"line":213},9,[133,215,216],{"class":146},"def",[133,218,220],{"class":219},"sScJk"," read_records",[133,222,223],{"class":157},"(stream: BinaryIO, ",[133,225,226],{"class":146},"*",[133,228,229],{"class":157},", null: ",[133,231,232],{"class":150},"bool",[133,234,235],{"class":157},", chunk_size: ",[133,237,238],{"class":150},"int",[133,240,241],{"class":146}," =",[133,243,244],{"class":150}," 65536",[133,246,247],{"class":157},") -> Iterator[",[133,249,250],{"class":150},"str",[133,252,253],{"class":157},"]:\n",[133,255,257],{"class":135,"line":256},10,[133,258,260],{"class":259},"sZZnC","    \"\"\"Yield records separated by NUL (null=True) or newline, decoded like file names.\"\"\"\n",[133,262,264,267,270,273,276,279,281,284,287,290,292,294,297],{"class":135,"line":263},11,[133,265,266],{"class":157},"    sep ",[133,268,269],{"class":146},"=",[133,271,272],{"class":146}," b",[133,274,275],{"class":259},"\"",[133,277,278],{"class":150},"\\0",[133,280,275],{"class":259},[133,282,283],{"class":146}," if",[133,285,286],{"class":157}," null ",[133,288,289],{"class":146},"else",[133,291,272],{"class":146},[133,293,275],{"class":259},[133,295,296],{"class":150},"\\n",[133,298,299],{"class":259},"\"\n",[133,301,303,306,308,310],{"class":135,"line":302},12,[133,304,305],{"class":157},"    buffer ",[133,307,269],{"class":146},[133,309,272],{"class":146},[133,311,312],{"class":259},"\"\"\n",[133,314,316,319,322,325],{"class":135,"line":315},13,[133,317,318],{"class":146},"    while",[133,320,321],{"class":157}," chunk ",[133,323,324],{"class":146},":=",[133,326,327],{"class":157}," stream.read(chunk_size):\n",[133,329,331,334,337],{"class":135,"line":330},14,[133,332,333],{"class":157},"        buffer ",[133,335,336],{"class":146},"+=",[133,338,339],{"class":157}," chunk\n",[133,341,343,346,349,351],{"class":135,"line":342},15,[133,344,345],{"class":146},"        *",[133,347,348],{"class":157},"complete, buffer ",[133,350,269],{"class":146},[133,352,353],{"class":157}," buffer.split(sep)\n",[133,355,357,360,363,366],{"class":135,"line":356},16,[133,358,359],{"class":146},"        for",[133,361,362],{"class":157}," raw ",[133,364,365],{"class":146},"in",[133,367,368],{"class":157}," complete:\n",[133,370,372,375],{"class":135,"line":371},17,[133,373,374],{"class":146},"            yield from",[133,376,377],{"class":157}," _decode(raw, null)\n",[133,379,381,384],{"class":135,"line":380},18,[133,382,383],{"class":146},"    if",[133,385,386],{"class":157}," buffer:\n",[133,388,390,393],{"class":135,"line":389},19,[133,391,392],{"class":146},"        yield from",[133,394,395],{"class":157}," _decode(buffer, null)\n",[133,397,399],{"class":135,"line":398},20,[133,400,165],{"emptyLinePlaceholder":164},[133,402,404],{"class":135,"line":403},21,[133,405,165],{"emptyLinePlaceholder":164},[133,407,409,411,414,417,420,422,424,426,428],{"class":135,"line":408},22,[133,410,216],{"class":146},[133,412,413],{"class":219}," _decode",[133,415,416],{"class":157},"(raw: ",[133,418,419],{"class":150},"bytes",[133,421,229],{"class":157},[133,423,232],{"class":150},[133,425,247],{"class":157},[133,427,250],{"class":150},[133,429,253],{"class":157},[133,431,433,435,438],{"class":135,"line":432},23,[133,434,383],{"class":146},[133,436,437],{"class":146}," not",[133,439,440],{"class":157}," null:\n",[133,442,444,447,449,452,455,457,460,462,465],{"class":135,"line":443},24,[133,445,446],{"class":157},"        raw ",[133,448,269],{"class":146},[133,450,451],{"class":157}," raw.removesuffix(",[133,453,454],{"class":146},"b",[133,456,275],{"class":259},[133,458,459],{"class":150},"\\r",[133,461,275],{"class":259},[133,463,464],{"class":157},")             ",[133,466,467],{"class":139},"# tolerate CRLF input on newline mode\n",[133,469,471,473,476],{"class":135,"line":470},25,[133,472,383],{"class":146},[133,474,475],{"class":157}," raw:                                       ",[133,477,478],{"class":139},"# empty records (blank lines, \"\\0\\0\") are skipped\n",[133,480,482,485,488,491],{"class":135,"line":481},26,[133,483,484],{"class":146},"        yield",[133,486,487],{"class":157}," raw.decode(sys.getfilesystemencoding(), ",[133,489,490],{"class":259},"\"surrogateescape\"",[133,492,493],{"class":157},")\n",[133,495,497],{"class":135,"line":496},27,[133,498,165],{"emptyLinePlaceholder":164},[133,500,502],{"class":135,"line":501},28,[133,503,165],{"emptyLinePlaceholder":164},[133,505,507,509,512,515,517,520,522,524,526,529,532],{"class":135,"line":506},29,[133,508,216],{"class":146},[133,510,511],{"class":219}," write_records",[133,513,514],{"class":157},"(records: Iterable[",[133,516,250],{"class":150},[133,518,519],{"class":157},"], stream: BinaryIO, ",[133,521,226],{"class":146},[133,523,229],{"class":157},[133,525,232],{"class":150},[133,527,528],{"class":157},") -> ",[133,530,531],{"class":150},"None",[133,533,534],{"class":157},":\n",[133,536,538,540,542,544,546,548,550,552,554,556,558,560,562],{"class":135,"line":537},30,[133,539,266],{"class":157},[133,541,269],{"class":146},[133,543,272],{"class":146},[133,545,275],{"class":259},[133,547,278],{"class":150},[133,549,275],{"class":259},[133,551,283],{"class":146},[133,553,286],{"class":157},[133,555,289],{"class":146},[133,557,272],{"class":146},[133,559,275],{"class":259},[133,561,296],{"class":150},[133,563,299],{"class":259},[133,565,567,570,573,575],{"class":135,"line":566},31,[133,568,569],{"class":146},"    for",[133,571,572],{"class":157}," record ",[133,574,365],{"class":146},[133,576,577],{"class":157}," records:\n",[133,579,581,584,586,589,591],{"class":135,"line":580},32,[133,582,583],{"class":157},"        data ",[133,585,269],{"class":146},[133,587,588],{"class":157}," record.encode(sys.getfilesystemencoding(), ",[133,590,490],{"class":259},[133,592,493],{"class":157},[133,594,596,599,601,603,606,608,610,612,614,617],{"class":135,"line":595},33,[133,597,598],{"class":146},"        if",[133,600,437],{"class":146},[133,602,286],{"class":157},[133,604,605],{"class":146},"and",[133,607,272],{"class":146},[133,609,275],{"class":259},[133,611,296],{"class":150},[133,613,275],{"class":259},[133,615,616],{"class":146}," in",[133,618,619],{"class":157}," data:\n",[133,621,623,626,629,632,635,637,640,643,646,649,652],{"class":135,"line":622},34,[133,624,625],{"class":146},"            raise",[133,627,628],{"class":150}," ValueError",[133,630,631],{"class":157},"(",[133,633,634],{"class":146},"f",[133,636,275],{"class":259},[133,638,639],{"class":150},"{",[133,641,642],{"class":157},"record",[133,644,645],{"class":146},"!r",[133,647,648],{"class":150},"}",[133,650,651],{"class":259}," contains a newline; use -0\u002F--null\"",[133,653,493],{"class":157},[133,655,657,660,663],{"class":135,"line":656},35,[133,658,659],{"class":157},"        stream.write(data ",[133,661,662],{"class":146},"+",[133,664,665],{"class":157}," sep)\n",[133,667,669],{"class":135,"line":668},36,[133,670,671],{"class":157},"    stream.flush()\n",[124,673,675],{"className":126,"code":674,"language":128,"meta":129,"style":129},"# src\u002Fmytool\u002Fcli.py\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\nfrom mytool.records import read_records, write_records\n\napp = typer.Typer()\n\nNullIn = Annotated[bool, typer.Option(\"-0\", \"--null\", help=\"Input is NUL-separated (find -print0).\")]\nNullOut = Annotated[bool, typer.Option(\"-z\", \"--print0\", help=\"Separate output with NUL, for xargs -0.\")]\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"File tool.\"\"\"\n\n\n@app.command()\ndef stale(null: NullIn = False, print0: NullOut = False,\n          days: Annotated[int, typer.Option(help=\"Older than this many days.\")] = 30) -> None:\n    \"\"\"Read paths from stdin and print those not modified for DAYS days.\"\"\"\n    import time\n\n    cutoff = time.time() - days * 86400\n    paths = read_records(sys.stdin.buffer, null=null)\n    old = (p for p in paths if Path(p).is_file() and Path(p).stat().st_mtime \u003C cutoff)\n    try:\n        write_records(old, sys.stdout.buffer, null=print0)\n    except ValueError as exc:\n        typer.echo(f\"error: {exc}\", err=True)\n        raise typer.Exit(1)\n",[14,676,677,682,688,700,711,715,722,726,738,742,752,756,793,825,829,833,841,855,860,864,868,875,900,930,935,943,947,968,986,1024,1031,1043,1056,1087],{"__ignoreMap":129},[133,678,679],{"class":135,"line":136},[133,680,681],{"class":139},"# src\u002Fmytool\u002Fcli.py\n",[133,683,684,686],{"class":135,"line":143},[133,685,171],{"class":146},[133,687,174],{"class":157},[133,689,690,692,695,697],{"class":135,"line":161},[133,691,147],{"class":146},[133,693,694],{"class":157}," pathlib ",[133,696,171],{"class":146},[133,698,699],{"class":157}," Path\n",[133,701,702,704,706,708],{"class":135,"line":168},[133,703,147],{"class":146},[133,705,195],{"class":157},[133,707,171],{"class":146},[133,709,710],{"class":157}," Annotated\n",[133,712,713],{"class":135,"line":177},[133,714,165],{"emptyLinePlaceholder":164},[133,716,717,719],{"class":135,"line":190},[133,718,171],{"class":146},[133,720,721],{"class":157}," typer\n",[133,723,724],{"class":135,"line":203},[133,725,165],{"emptyLinePlaceholder":164},[133,727,728,730,733,735],{"class":135,"line":208},[133,729,147],{"class":146},[133,731,732],{"class":157}," mytool.records ",[133,734,171],{"class":146},[133,736,737],{"class":157}," read_records, write_records\n",[133,739,740],{"class":135,"line":213},[133,741,165],{"emptyLinePlaceholder":164},[133,743,744,747,749],{"class":135,"line":256},[133,745,746],{"class":157},"app ",[133,748,269],{"class":146},[133,750,751],{"class":157}," typer.Typer()\n",[133,753,754],{"class":135,"line":263},[133,755,165],{"emptyLinePlaceholder":164},[133,757,758,761,763,766,768,771,774,776,779,781,785,787,790],{"class":135,"line":302},[133,759,760],{"class":157},"NullIn ",[133,762,269],{"class":146},[133,764,765],{"class":157}," Annotated[",[133,767,232],{"class":150},[133,769,770],{"class":157},", typer.Option(",[133,772,773],{"class":259},"\"-0\"",[133,775,96],{"class":157},[133,777,778],{"class":259},"\"--null\"",[133,780,96],{"class":157},[133,782,784],{"class":783},"s4XuR","help",[133,786,269],{"class":146},[133,788,789],{"class":259},"\"Input is NUL-separated (find -print0).\"",[133,791,792],{"class":157},")]\n",[133,794,795,798,800,802,804,806,809,811,814,816,818,820,823],{"class":135,"line":315},[133,796,797],{"class":157},"NullOut ",[133,799,269],{"class":146},[133,801,765],{"class":157},[133,803,232],{"class":150},[133,805,770],{"class":157},[133,807,808],{"class":259},"\"-z\"",[133,810,96],{"class":157},[133,812,813],{"class":259},"\"--print0\"",[133,815,96],{"class":157},[133,817,784],{"class":783},[133,819,269],{"class":146},[133,821,822],{"class":259},"\"Separate output with NUL, for xargs -0.\"",[133,824,792],{"class":157},[133,826,827],{"class":135,"line":330},[133,828,165],{"emptyLinePlaceholder":164},[133,830,831],{"class":135,"line":342},[133,832,165],{"emptyLinePlaceholder":164},[133,834,835,838],{"class":135,"line":356},[133,836,837],{"class":219},"@app.callback",[133,839,840],{"class":157},"()\n",[133,842,843,845,848,851,853],{"class":135,"line":371},[133,844,216],{"class":146},[133,846,847],{"class":219}," main",[133,849,850],{"class":157},"() -> ",[133,852,531],{"class":150},[133,854,534],{"class":157},[133,856,857],{"class":135,"line":380},[133,858,859],{"class":259},"    \"\"\"File tool.\"\"\"\n",[133,861,862],{"class":135,"line":389},[133,863,165],{"emptyLinePlaceholder":164},[133,865,866],{"class":135,"line":398},[133,867,165],{"emptyLinePlaceholder":164},[133,869,870,873],{"class":135,"line":403},[133,871,872],{"class":219},"@app.command",[133,874,840],{"class":157},[133,876,877,879,882,885,887,890,893,895,897],{"class":135,"line":408},[133,878,216],{"class":146},[133,880,881],{"class":219}," stale",[133,883,884],{"class":157},"(null: NullIn ",[133,886,269],{"class":146},[133,888,889],{"class":150}," False",[133,891,892],{"class":157},", print0: NullOut ",[133,894,269],{"class":146},[133,896,889],{"class":150},[133,898,899],{"class":157},",\n",[133,901,902,905,907,909,911,913,916,919,921,924,926,928],{"class":135,"line":432},[133,903,904],{"class":157},"          days: Annotated[",[133,906,238],{"class":150},[133,908,770],{"class":157},[133,910,784],{"class":783},[133,912,269],{"class":146},[133,914,915],{"class":259},"\"Older than this many days.\"",[133,917,918],{"class":157},")] ",[133,920,269],{"class":146},[133,922,923],{"class":150}," 30",[133,925,528],{"class":157},[133,927,531],{"class":150},[133,929,534],{"class":157},[133,931,932],{"class":135,"line":443},[133,933,934],{"class":259},"    \"\"\"Read paths from stdin and print those not modified for DAYS days.\"\"\"\n",[133,936,937,940],{"class":135,"line":470},[133,938,939],{"class":146},"    import",[133,941,942],{"class":157}," time\n",[133,944,945],{"class":135,"line":481},[133,946,165],{"emptyLinePlaceholder":164},[133,948,949,952,954,957,960,963,965],{"class":135,"line":496},[133,950,951],{"class":157},"    cutoff ",[133,953,269],{"class":146},[133,955,956],{"class":157}," time.time() ",[133,958,959],{"class":146},"-",[133,961,962],{"class":157}," days ",[133,964,226],{"class":146},[133,966,967],{"class":150}," 86400\n",[133,969,970,973,975,978,981,983],{"class":135,"line":501},[133,971,972],{"class":157},"    paths ",[133,974,269],{"class":146},[133,976,977],{"class":157}," read_records(sys.stdin.buffer, ",[133,979,980],{"class":783},"null",[133,982,269],{"class":146},[133,984,985],{"class":157},"null)\n",[133,987,988,991,993,996,999,1002,1004,1007,1010,1013,1015,1018,1021],{"class":135,"line":506},[133,989,990],{"class":157},"    old ",[133,992,269],{"class":146},[133,994,995],{"class":157}," (p ",[133,997,998],{"class":146},"for",[133,1000,1001],{"class":157}," p ",[133,1003,365],{"class":146},[133,1005,1006],{"class":157}," paths ",[133,1008,1009],{"class":146},"if",[133,1011,1012],{"class":157}," Path(p).is_file() ",[133,1014,605],{"class":146},[133,1016,1017],{"class":157}," Path(p).stat().st_mtime ",[133,1019,1020],{"class":146},"\u003C",[133,1022,1023],{"class":157}," cutoff)\n",[133,1025,1026,1029],{"class":135,"line":537},[133,1027,1028],{"class":146},"    try",[133,1030,534],{"class":157},[133,1032,1033,1036,1038,1040],{"class":135,"line":566},[133,1034,1035],{"class":157},"        write_records(old, sys.stdout.buffer, ",[133,1037,980],{"class":783},[133,1039,269],{"class":146},[133,1041,1042],{"class":157},"print0)\n",[133,1044,1045,1048,1050,1053],{"class":135,"line":580},[133,1046,1047],{"class":146},"    except",[133,1049,628],{"class":150},[133,1051,1052],{"class":146}," as",[133,1054,1055],{"class":157}," exc:\n",[133,1057,1058,1061,1063,1066,1068,1071,1073,1075,1077,1080,1082,1085],{"class":135,"line":595},[133,1059,1060],{"class":157},"        typer.echo(",[133,1062,634],{"class":146},[133,1064,1065],{"class":259},"\"error: ",[133,1067,639],{"class":150},[133,1069,1070],{"class":157},"exc",[133,1072,648],{"class":150},[133,1074,275],{"class":259},[133,1076,96],{"class":157},[133,1078,1079],{"class":783},"err",[133,1081,269],{"class":146},[133,1083,1084],{"class":150},"True",[133,1086,493],{"class":157},[133,1088,1089,1092,1095,1098],{"class":135,"line":622},[133,1090,1091],{"class":146},"        raise",[133,1093,1094],{"class":157}," typer.Exit(",[133,1096,1097],{"class":150},"1",[133,1099,493],{"class":157},[10,1101,1102],{},"The command reads paths, keeps the ones not modified recently, and writes them out — a typical filter in a pipeline:",[124,1104,1108],{"className":1105,"code":1106,"language":1107,"meta":129,"style":129},"language-bash shiki shiki-themes github-light github-dark","find ~\u002FDownloads -type f -print0 | mytool stale -0 -z --days 90 | xargs -0 rm -i --\ngit ls-files -z | mytool stale -0\n","bash",[14,1109,1110,1162],{"__ignoreMap":129},[133,1111,1112,1114,1117,1120,1123,1126,1129,1132,1134,1137,1140,1143,1146,1148,1151,1153,1156,1159],{"class":135,"line":136},[133,1113,36],{"class":219},[133,1115,1116],{"class":259}," ~\u002FDownloads",[133,1118,1119],{"class":150}," -type",[133,1121,1122],{"class":259}," f",[133,1124,1125],{"class":150}," -print0",[133,1127,1128],{"class":146}," |",[133,1130,1131],{"class":219}," mytool",[133,1133,881],{"class":259},[133,1135,1136],{"class":150}," -0",[133,1138,1139],{"class":150}," -z",[133,1141,1142],{"class":150}," --days",[133,1144,1145],{"class":150}," 90",[133,1147,1128],{"class":146},[133,1149,1150],{"class":219}," xargs",[133,1152,1136],{"class":150},[133,1154,1155],{"class":259}," rm",[133,1157,1158],{"class":150}," -i",[133,1160,1161],{"class":150}," --\n",[133,1163,1164,1167,1170,1172,1174,1176,1178],{"class":135,"line":143},[133,1165,1166],{"class":219},"git",[133,1168,1169],{"class":259}," ls-files",[133,1171,1139],{"class":150},[133,1173,1128],{"class":146},[133,1175,1131],{"class":219},[133,1177,881],{"class":259},[133,1179,1180],{"class":150}," -0\n",[1182,1183,1185],"h3",{"id":1184},"reading-records-in-binary","Reading records in binary",[10,1187,1188,1191,1192,1195,1196,1199],{},[14,1189,1190],{},"sys.stdin"," is a text stream that decodes with the locale encoding and translates newlines; neither is what you want for file names. ",[14,1193,1194],{},"sys.stdin.buffer"," gives the raw bytes. The reader pulls fixed-size chunks, splits on the separator and keeps the incomplete tail for the next round, so memory stays flat whether the input has ten names or ten million, and the first results flow downstream immediately. A record that straddles a chunk boundary is reassembled correctly — the test with ",[14,1197,1198],{},"chunk_size=3"," exists to prove it.",[10,1201,1202,1203,1205],{},"Empty records are skipped: blank lines in newline mode, and doubled NULs, which some generators produce. In newline mode, a trailing ",[14,1204,459],{}," is removed, so input generated on Windows does not produce names ending in an invisible carriage return.",[1182,1207,1209],{"id":1208},"decoding-like-the-operating-system-does","Decoding like the operating system does",[10,1211,1212,1213,1216,1217,1220,1221,1224,1225,37,1228,1231,1232,1238,1239,1241,1242,1245],{},"File names on Linux are bytes. Most are UTF-8, but a disk copied from an old system may have Latin-1 names that are not valid UTF-8. Decoding with ",[14,1214,1215],{},"errors=\"strict\""," crashes on them; ",[14,1218,1219],{},"errors=\"replace\""," turns them into ",[14,1222,1223],{},"�",", a different name that does not exist. Python’s own answer — the one ",[14,1226,1227],{},"os.listdir",[14,1229,1230],{},"sys.argv"," use — is the file system encoding with the ",[1233,1234,1235],"strong",{},[14,1236,1237],{},"surrogateescape"," error handler: undecodable bytes become lone surrogate code points in the ",[14,1240,250],{},", and encoding back with the same handler restores the original bytes. A ",[14,1243,1244],{},"Path"," built from such a string opens the right file, and writing it back out reproduces it exactly. Using the same encoding and handler on both sides makes the tool transparent to any name.",[1182,1247,1249],{"id":1248},"writing-records","Writing records",[10,1251,1252,1254,1255,96,1257,1259,1260,1262,1263,1266],{},[14,1253,32],{}," writes NUL after each record, which is what ",[14,1256,99],{},[14,1258,108],{}," and the next tool’s ",[14,1261,28],{}," expect. In newline mode, a record containing a newline cannot be written unambiguously, so the writer refuses with an error that names the fix instead of producing output that another program will silently misread. Writing through ",[14,1264,1265],{},"sys.stdout.buffer"," avoids text-mode newline translation on Windows and encoding errors for surrogate-escaped names.",[83,1268],{"name":1269},"nul-terminal",[49,1271,1273],{"id":1272},"flag-conventions","Flag conventions",[10,1275,1276],{},"There is no single standard, but there are strong habits worth following so users can guess your flags:",[54,1278,1279,1293,1321],{},[57,1280,1281,1290,1291,47],{},[1233,1282,1283,1285,1286,1289],{},[14,1284,28],{}," \u002F ",[14,1287,1288],{},"--null"," for input",", as in ",[14,1292,99],{},[57,1294,1295,1290,1307,96,1309,37,1312,1314,1315,1317,1318,1320],{},[1233,1296,1297,1285,1299,1302,1303,1306],{},[14,1298,32],{},[14,1300,1301],{},"--print0"," (or ",[14,1304,1305],{},"--zero",") for output",[14,1308,102],{},[14,1310,1311],{},"grep -z",[14,1313,95],{},". Some tools use ",[14,1316,32],{}," for both directions, like ",[14,1319,108],{},"; if your command only ever reads or only ever writes lists, one flag for both is fine.",[57,1322,1323,1326],{},[1233,1324,1325],{},"Document them together"," in the help, with a pipeline example — users who need them search for “print0”.",[49,1328,1330],{"id":1329},"ux-considerations","UX considerations",[54,1332,1333,1339,1360,1377,1390],{},[57,1334,1335,1338],{},[1233,1336,1337],{},"Keep newline mode the default."," It is what people type interactively and read on screen; NUL output on a terminal looks like names glued together.",[57,1340,1341,1348,1349,1352,1353,1356,1357,1359],{},[1233,1342,1343,1344,1347],{},"Use ",[14,1345,1346],{},"--"," before paths"," when passing names to other commands (",[14,1350,1351],{},"xargs -0 rm --","), so a file called ",[14,1354,1355],{},"-rf"," is not read as an option. Your own CLI should accept ",[14,1358,1346],{}," too; Click and Typer do.",[57,1361,1362,1365,1366,1368,1369,1372,1373,47],{},[1233,1363,1364],{},"Stream, do not collect."," Emit each result as soon as it is known; downstream ",[14,1367,40],{}," starts work in parallel and ",[14,1370,1371],{},"head -z"," can stop early. Handle the resulting broken pipe as in ",[43,1374,1376],{"href":1375},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe\u002F","handling broken pipe and SIGPIPE",[57,1378,1379,1382,1383,1386,1387,1389],{},[1233,1380,1381],{},"Accept arguments as well as stdin."," ",[14,1384,1385],{},"mytool stale a.txt b.txt"," should work too; read stdin only when no paths are given, so the tool is equally comfortable as an ",[14,1388,40],{}," target.",[57,1391,1392,1395,1396,47],{},[1233,1393,1394],{},"Apply it to JSON as well."," NDJSON has the same property NUL gives paths — one record per line, with newlines inside values escaped — which is why it suits structured records; see ",[43,1397,1399],{"href":1398},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams\u002F","processing large files and NDJSON streams",[83,1401],{"name":1402},"nul-tools",[49,1404,1406],{"id":1405},"testing-the-behaviour","Testing the behaviour",[10,1408,1409,1410,1413,1414,1417],{},"Unit tests feed ",[14,1411,1412],{},"BytesIO"," objects with the nastiest names you can think of; one end-to-end test runs a real ",[14,1415,1416],{},"find -print0 | mytool | xargs -0"," pipeline:",[124,1419,1421],{"className":126,"code":1420,"language":128,"meta":129,"style":129},"# tests\u002Ftest_records.py\nimport io\nimport os\nimport shlex\nimport shutil\nimport subprocess\nimport sys\n\nimport pytest\n\nfrom mytool.records import read_records, write_records\n\nAWKWARD = [\"plain.txt\", \"with space.txt\", \"new\\nline.txt\", \"tab\\there.txt\", \"café.txt\"]\n\n\ndef test_null_round_trip_survives_any_name():\n    buf = io.BytesIO()\n    write_records(AWKWARD, buf, null=True)\n    assert list(read_records(io.BytesIO(buf.getvalue()), null=True)) == AWKWARD\n\n\ndef test_records_split_across_chunks():\n    data = b\"\\0\".join(name.encode() for name in AWKWARD) + b\"\\0\"\n    assert list(read_records(io.BytesIO(data), null=True, chunk_size=3)) == AWKWARD\n\n\ndef test_newline_mode_skips_blanks_and_crlf():\n    data = b\"a.txt\\r\\n\\nb.txt\\n\"\n    assert list(read_records(io.BytesIO(data), null=False)) == [\"a.txt\", \"b.txt\"]\n\n\ndef test_newline_output_refuses_ambiguous_names():\n    with pytest.raises(ValueError, match=\"use -0\"):\n        write_records([\"new\\nline.txt\"], io.BytesIO(), null=False)\n\n\ndef test_undecodable_bytes_round_trip():\n    raw = b\"bad-\\xff.txt\\0\"\n    [name] = read_records(io.BytesIO(raw), null=True)\n    out = io.BytesIO()\n    write_records([name], out, null=True)\n    assert out.getvalue() == raw\n\n\n@pytest.mark.skipif(not shutil.which(\"find\") or not shutil.which(\"xargs\"), reason=\"needs find\u002Fxargs\")\ndef test_find_print0_to_xargs0(tmp_path):\n    for name in AWKWARD:\n        (tmp_path \u002F name).write_text(\"x\")\n        os.utime(tmp_path \u002F name, (0, 0))                        # very old\n    tool = f\"{shlex.quote(sys.executable)} -c 'from mytool.cli import app; app()'\"\n    cmd = f\"find {shlex.quote(str(tmp_path))} -type f -print0 | {tool} stale -0 -z | xargs -0 ls -1d\"\n    out = subprocess.run(cmd, shell=True, capture_output=True, check=True)\n    for name in [\"with space.txt\", \"tab\\there.txt\", \"café.txt\"]:\n        assert str(tmp_path \u002F name) in out.stdout.decode()\n",[14,1422,1423,1428,1435,1442,1449,1456,1463,1469,1473,1480,1484,1494,1498,1545,1549,1553,1563,1573,1591,1617,1621,1625,1634,1675,1706,1710,1714,1723,1744,1775,1779,1783,1792,1816,1838,1842,1846,1856,1879,1898,1908,1922,1935,1940,1945,1987,1998,2011,2027,2051,2073,2111,2148,2175],{"__ignoreMap":129},[133,1424,1425],{"class":135,"line":136},[133,1426,1427],{"class":139},"# tests\u002Ftest_records.py\n",[133,1429,1430,1432],{"class":135,"line":143},[133,1431,171],{"class":146},[133,1433,1434],{"class":157}," io\n",[133,1436,1437,1439],{"class":135,"line":161},[133,1438,171],{"class":146},[133,1440,1441],{"class":157}," os\n",[133,1443,1444,1446],{"class":135,"line":168},[133,1445,171],{"class":146},[133,1447,1448],{"class":157}," shlex\n",[133,1450,1451,1453],{"class":135,"line":177},[133,1452,171],{"class":146},[133,1454,1455],{"class":157}," shutil\n",[133,1457,1458,1460],{"class":135,"line":190},[133,1459,171],{"class":146},[133,1461,1462],{"class":157}," subprocess\n",[133,1464,1465,1467],{"class":135,"line":203},[133,1466,171],{"class":146},[133,1468,174],{"class":157},[133,1470,1471],{"class":135,"line":208},[133,1472,165],{"emptyLinePlaceholder":164},[133,1474,1475,1477],{"class":135,"line":213},[133,1476,171],{"class":146},[133,1478,1479],{"class":157}," pytest\n",[133,1481,1482],{"class":135,"line":256},[133,1483,165],{"emptyLinePlaceholder":164},[133,1485,1486,1488,1490,1492],{"class":135,"line":263},[133,1487,147],{"class":146},[133,1489,732],{"class":157},[133,1491,171],{"class":146},[133,1493,737],{"class":157},[133,1495,1496],{"class":135,"line":302},[133,1497,165],{"emptyLinePlaceholder":164},[133,1499,1500,1503,1505,1508,1511,1513,1516,1518,1521,1523,1526,1528,1531,1534,1537,1539,1542],{"class":135,"line":315},[133,1501,1502],{"class":150},"AWKWARD",[133,1504,241],{"class":146},[133,1506,1507],{"class":157}," [",[133,1509,1510],{"class":259},"\"plain.txt\"",[133,1512,96],{"class":157},[133,1514,1515],{"class":259},"\"with space.txt\"",[133,1517,96],{"class":157},[133,1519,1520],{"class":259},"\"new",[133,1522,296],{"class":150},[133,1524,1525],{"class":259},"line.txt\"",[133,1527,96],{"class":157},[133,1529,1530],{"class":259},"\"tab",[133,1532,1533],{"class":150},"\\t",[133,1535,1536],{"class":259},"here.txt\"",[133,1538,96],{"class":157},[133,1540,1541],{"class":259},"\"café.txt\"",[133,1543,1544],{"class":157},"]\n",[133,1546,1547],{"class":135,"line":330},[133,1548,165],{"emptyLinePlaceholder":164},[133,1550,1551],{"class":135,"line":342},[133,1552,165],{"emptyLinePlaceholder":164},[133,1554,1555,1557,1560],{"class":135,"line":356},[133,1556,216],{"class":146},[133,1558,1559],{"class":219}," test_null_round_trip_survives_any_name",[133,1561,1562],{"class":157},"():\n",[133,1564,1565,1568,1570],{"class":135,"line":371},[133,1566,1567],{"class":157},"    buf ",[133,1569,269],{"class":146},[133,1571,1572],{"class":157}," io.BytesIO()\n",[133,1574,1575,1578,1580,1583,1585,1587,1589],{"class":135,"line":380},[133,1576,1577],{"class":157},"    write_records(",[133,1579,1502],{"class":150},[133,1581,1582],{"class":157},", buf, ",[133,1584,980],{"class":783},[133,1586,269],{"class":146},[133,1588,1084],{"class":150},[133,1590,493],{"class":157},[133,1592,1593,1596,1599,1602,1604,1606,1608,1611,1614],{"class":135,"line":389},[133,1594,1595],{"class":146},"    assert",[133,1597,1598],{"class":150}," list",[133,1600,1601],{"class":157},"(read_records(io.BytesIO(buf.getvalue()), ",[133,1603,980],{"class":783},[133,1605,269],{"class":146},[133,1607,1084],{"class":150},[133,1609,1610],{"class":157},")) ",[133,1612,1613],{"class":146},"==",[133,1615,1616],{"class":150}," AWKWARD\n",[133,1618,1619],{"class":135,"line":398},[133,1620,165],{"emptyLinePlaceholder":164},[133,1622,1623],{"class":135,"line":403},[133,1624,165],{"emptyLinePlaceholder":164},[133,1626,1627,1629,1632],{"class":135,"line":408},[133,1628,216],{"class":146},[133,1630,1631],{"class":219}," test_records_split_across_chunks",[133,1633,1562],{"class":157},[133,1635,1636,1639,1641,1643,1645,1647,1649,1652,1654,1657,1659,1662,1665,1667,1669,1671,1673],{"class":135,"line":432},[133,1637,1638],{"class":157},"    data ",[133,1640,269],{"class":146},[133,1642,272],{"class":146},[133,1644,275],{"class":259},[133,1646,278],{"class":150},[133,1648,275],{"class":259},[133,1650,1651],{"class":157},".join(name.encode() ",[133,1653,998],{"class":146},[133,1655,1656],{"class":157}," name ",[133,1658,365],{"class":146},[133,1660,1661],{"class":150}," AWKWARD",[133,1663,1664],{"class":157},") ",[133,1666,662],{"class":146},[133,1668,272],{"class":146},[133,1670,275],{"class":259},[133,1672,278],{"class":150},[133,1674,299],{"class":259},[133,1676,1677,1679,1681,1684,1686,1688,1690,1692,1695,1697,1700,1702,1704],{"class":135,"line":443},[133,1678,1595],{"class":146},[133,1680,1598],{"class":150},[133,1682,1683],{"class":157},"(read_records(io.BytesIO(data), ",[133,1685,980],{"class":783},[133,1687,269],{"class":146},[133,1689,1084],{"class":150},[133,1691,96],{"class":157},[133,1693,1694],{"class":783},"chunk_size",[133,1696,269],{"class":146},[133,1698,1699],{"class":150},"3",[133,1701,1610],{"class":157},[133,1703,1613],{"class":146},[133,1705,1616],{"class":150},[133,1707,1708],{"class":135,"line":470},[133,1709,165],{"emptyLinePlaceholder":164},[133,1711,1712],{"class":135,"line":481},[133,1713,165],{"emptyLinePlaceholder":164},[133,1715,1716,1718,1721],{"class":135,"line":496},[133,1717,216],{"class":146},[133,1719,1720],{"class":219}," test_newline_mode_skips_blanks_and_crlf",[133,1722,1562],{"class":157},[133,1724,1725,1727,1729,1731,1734,1737,1740,1742],{"class":135,"line":501},[133,1726,1638],{"class":157},[133,1728,269],{"class":146},[133,1730,272],{"class":146},[133,1732,1733],{"class":259},"\"a.txt",[133,1735,1736],{"class":150},"\\r\\n\\n",[133,1738,1739],{"class":259},"b.txt",[133,1741,296],{"class":150},[133,1743,299],{"class":259},[133,1745,1746,1748,1750,1752,1754,1756,1759,1761,1763,1765,1768,1770,1773],{"class":135,"line":506},[133,1747,1595],{"class":146},[133,1749,1598],{"class":150},[133,1751,1683],{"class":157},[133,1753,980],{"class":783},[133,1755,269],{"class":146},[133,1757,1758],{"class":150},"False",[133,1760,1610],{"class":157},[133,1762,1613],{"class":146},[133,1764,1507],{"class":157},[133,1766,1767],{"class":259},"\"a.txt\"",[133,1769,96],{"class":157},[133,1771,1772],{"class":259},"\"b.txt\"",[133,1774,1544],{"class":157},[133,1776,1777],{"class":135,"line":537},[133,1778,165],{"emptyLinePlaceholder":164},[133,1780,1781],{"class":135,"line":566},[133,1782,165],{"emptyLinePlaceholder":164},[133,1784,1785,1787,1790],{"class":135,"line":580},[133,1786,216],{"class":146},[133,1788,1789],{"class":219}," test_newline_output_refuses_ambiguous_names",[133,1791,1562],{"class":157},[133,1793,1794,1797,1800,1803,1805,1808,1810,1813],{"class":135,"line":595},[133,1795,1796],{"class":146},"    with",[133,1798,1799],{"class":157}," pytest.raises(",[133,1801,1802],{"class":150},"ValueError",[133,1804,96],{"class":157},[133,1806,1807],{"class":783},"match",[133,1809,269],{"class":146},[133,1811,1812],{"class":259},"\"use -0\"",[133,1814,1815],{"class":157},"):\n",[133,1817,1818,1821,1823,1825,1827,1830,1832,1834,1836],{"class":135,"line":622},[133,1819,1820],{"class":157},"        write_records([",[133,1822,1520],{"class":259},[133,1824,296],{"class":150},[133,1826,1525],{"class":259},[133,1828,1829],{"class":157},"], io.BytesIO(), ",[133,1831,980],{"class":783},[133,1833,269],{"class":146},[133,1835,1758],{"class":150},[133,1837,493],{"class":157},[133,1839,1840],{"class":135,"line":656},[133,1841,165],{"emptyLinePlaceholder":164},[133,1843,1844],{"class":135,"line":668},[133,1845,165],{"emptyLinePlaceholder":164},[133,1847,1849,1851,1854],{"class":135,"line":1848},37,[133,1850,216],{"class":146},[133,1852,1853],{"class":219}," test_undecodable_bytes_round_trip",[133,1855,1562],{"class":157},[133,1857,1859,1862,1864,1866,1869,1872,1875,1877],{"class":135,"line":1858},38,[133,1860,1861],{"class":157},"    raw ",[133,1863,269],{"class":146},[133,1865,272],{"class":146},[133,1867,1868],{"class":259},"\"bad-",[133,1870,1871],{"class":150},"\\xff",[133,1873,1874],{"class":259},".txt",[133,1876,278],{"class":150},[133,1878,299],{"class":259},[133,1880,1882,1885,1887,1890,1892,1894,1896],{"class":135,"line":1881},39,[133,1883,1884],{"class":157},"    [name] ",[133,1886,269],{"class":146},[133,1888,1889],{"class":157}," read_records(io.BytesIO(raw), ",[133,1891,980],{"class":783},[133,1893,269],{"class":146},[133,1895,1084],{"class":150},[133,1897,493],{"class":157},[133,1899,1901,1904,1906],{"class":135,"line":1900},40,[133,1902,1903],{"class":157},"    out ",[133,1905,269],{"class":146},[133,1907,1572],{"class":157},[133,1909,1911,1914,1916,1918,1920],{"class":135,"line":1910},41,[133,1912,1913],{"class":157},"    write_records([name], out, ",[133,1915,980],{"class":783},[133,1917,269],{"class":146},[133,1919,1084],{"class":150},[133,1921,493],{"class":157},[133,1923,1925,1927,1930,1932],{"class":135,"line":1924},42,[133,1926,1595],{"class":146},[133,1928,1929],{"class":157}," out.getvalue() ",[133,1931,1613],{"class":146},[133,1933,1934],{"class":157}," raw\n",[133,1936,1938],{"class":135,"line":1937},43,[133,1939,165],{"emptyLinePlaceholder":164},[133,1941,1943],{"class":135,"line":1942},44,[133,1944,165],{"emptyLinePlaceholder":164},[133,1946,1948,1951,1953,1956,1959,1962,1964,1967,1969,1971,1974,1977,1980,1982,1985],{"class":135,"line":1947},45,[133,1949,1950],{"class":219},"@pytest.mark.skipif",[133,1952,631],{"class":157},[133,1954,1955],{"class":146},"not",[133,1957,1958],{"class":157}," shutil.which(",[133,1960,1961],{"class":259},"\"find\"",[133,1963,1664],{"class":157},[133,1965,1966],{"class":146},"or",[133,1968,437],{"class":146},[133,1970,1958],{"class":157},[133,1972,1973],{"class":259},"\"xargs\"",[133,1975,1976],{"class":157},"), ",[133,1978,1979],{"class":783},"reason",[133,1981,269],{"class":146},[133,1983,1984],{"class":259},"\"needs find\u002Fxargs\"",[133,1986,493],{"class":157},[133,1988,1990,1992,1995],{"class":135,"line":1989},46,[133,1991,216],{"class":146},[133,1993,1994],{"class":219}," test_find_print0_to_xargs0",[133,1996,1997],{"class":157},"(tmp_path):\n",[133,1999,2001,2003,2005,2007,2009],{"class":135,"line":2000},47,[133,2002,569],{"class":146},[133,2004,1656],{"class":157},[133,2006,365],{"class":146},[133,2008,1661],{"class":150},[133,2010,534],{"class":157},[133,2012,2014,2017,2019,2022,2025],{"class":135,"line":2013},48,[133,2015,2016],{"class":157},"        (tmp_path ",[133,2018,16],{"class":146},[133,2020,2021],{"class":157}," name).write_text(",[133,2023,2024],{"class":259},"\"x\"",[133,2026,493],{"class":157},[133,2028,2030,2033,2035,2038,2041,2043,2045,2048],{"class":135,"line":2029},49,[133,2031,2032],{"class":157},"        os.utime(tmp_path ",[133,2034,16],{"class":146},[133,2036,2037],{"class":157}," name, (",[133,2039,2040],{"class":150},"0",[133,2042,96],{"class":157},[133,2044,2040],{"class":150},[133,2046,2047],{"class":157},"))                        ",[133,2049,2050],{"class":139},"# very old\n",[133,2052,2054,2057,2059,2061,2063,2065,2068,2070],{"class":135,"line":2053},50,[133,2055,2056],{"class":157},"    tool ",[133,2058,269],{"class":146},[133,2060,1122],{"class":146},[133,2062,275],{"class":259},[133,2064,639],{"class":150},[133,2066,2067],{"class":157},"shlex.quote(sys.executable)",[133,2069,648],{"class":150},[133,2071,2072],{"class":259}," -c 'from mytool.cli import app; app()'\"\n",[133,2074,2076,2079,2081,2083,2086,2088,2091,2093,2096,2098,2101,2103,2106,2108],{"class":135,"line":2075},51,[133,2077,2078],{"class":157},"    cmd ",[133,2080,269],{"class":146},[133,2082,1122],{"class":146},[133,2084,2085],{"class":259},"\"find ",[133,2087,639],{"class":150},[133,2089,2090],{"class":157},"shlex.quote(",[133,2092,250],{"class":150},[133,2094,2095],{"class":157},"(tmp_path))",[133,2097,648],{"class":150},[133,2099,2100],{"class":259}," -type f -print0 | ",[133,2102,639],{"class":150},[133,2104,2105],{"class":157},"tool",[133,2107,648],{"class":150},[133,2109,2110],{"class":259}," stale -0 -z | xargs -0 ls -1d\"\n",[133,2112,2114,2116,2118,2121,2124,2126,2128,2130,2133,2135,2137,2139,2142,2144,2146],{"class":135,"line":2113},52,[133,2115,1903],{"class":157},[133,2117,269],{"class":146},[133,2119,2120],{"class":157}," subprocess.run(cmd, ",[133,2122,2123],{"class":783},"shell",[133,2125,269],{"class":146},[133,2127,1084],{"class":150},[133,2129,96],{"class":157},[133,2131,2132],{"class":783},"capture_output",[133,2134,269],{"class":146},[133,2136,1084],{"class":150},[133,2138,96],{"class":157},[133,2140,2141],{"class":783},"check",[133,2143,269],{"class":146},[133,2145,1084],{"class":150},[133,2147,493],{"class":157},[133,2149,2151,2153,2155,2157,2159,2161,2163,2165,2167,2169,2171,2173],{"class":135,"line":2150},53,[133,2152,569],{"class":146},[133,2154,1656],{"class":157},[133,2156,365],{"class":146},[133,2158,1507],{"class":157},[133,2160,1515],{"class":259},[133,2162,96],{"class":157},[133,2164,1530],{"class":259},[133,2166,1533],{"class":150},[133,2168,1536],{"class":259},[133,2170,96],{"class":157},[133,2172,1541],{"class":259},[133,2174,253],{"class":157},[133,2176,2178,2181,2184,2187,2189,2192,2194],{"class":135,"line":2177},54,[133,2179,2180],{"class":146},"        assert",[133,2182,2183],{"class":150}," str",[133,2185,2186],{"class":157},"(tmp_path ",[133,2188,16],{"class":146},[133,2190,2191],{"class":157}," name) ",[133,2193,365],{"class":146},[133,2195,2196],{"class":157}," out.stdout.decode()\n",[10,2198,2199,2200,2202,2203,37,2205,2207],{},"The round-trip test with ",[14,2201,1871],{}," is the one most implementations fail: it proves a name that is not valid UTF-8 comes out exactly as it went in. The pipeline test runs only where ",[14,2204,36],{},[14,2206,40],{}," exist, which covers Linux and macOS CI runners; on Windows, the unit tests still cover the logic.",[49,2209,2211],{"id":2210},"conclusion","Conclusion",[10,2213,2214,2215,2217,2218,2220,2221,2223,2224,2226,2227,16,2229,2231],{},"Paths can contain anything except ",[14,2216,16],{}," and NUL, so NUL is the only safe separator for lists of them. Add ",[14,2219,28],{}," to read NUL-separated input and ",[14,2222,32],{}," to write it, read and write the binary buffers, stream records in chunks while handling records split across chunk boundaries, decode with the file system encoding and ",[14,2225,1237],{}," so every name round-trips, refuse to write newline-containing names in newline mode, and test with awkward names and a real ",[14,2228,36],{},[14,2230,40],{}," pipeline.",[49,2233,2235],{"id":2234},"frequently-asked-questions","Frequently asked questions",[1182,2237,2239],{"id":2238},"does-this-matter-on-windows","Does this matter on Windows?",[10,2241,2242,2243,2245,2246,2248],{},"Less: Windows file names cannot contain newlines, and ",[14,2244,40],{}," is not part of the system. Supporting ",[14,2247,28],{}," costs nothing there, and users of Git Bash, WSL and MSYS2 pipelines benefit from it.",[1182,2250,2252],{"id":2251},"how-do-i-pass-nul-separated-names-to-a-subprocess-from-python","How do I pass NUL-separated names to a subprocess from Python?",[10,2254,2255,2256,2259,2260,47],{},"You do not need a separator at all — pass the list as arguments: ",[14,2257,2258],{},"subprocess.run([\"rm\", \"--\", *paths])",". Arguments are separate strings, so no name can be split. Mind the operating system’s argument length limit for very long lists, as discussed in ",[43,2261,2263],{"href":2262},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002F","calling external commands safely with subprocess",[1182,2265,2267],{"id":2266},"can-click-or-typer-read-nul-separated-stdin-directly","Can Click or Typer read NUL-separated stdin directly?",[10,2269,2270,2271,37,2274,2276,2277,2279],{},"They provide ",[14,2272,2273],{},"click.File(\"rb\")",[14,2275,959],{}," for stdin, but not record splitting. Read ",[14,2278,1194],{}," (or the opened binary file) with a reader like the one above.",[1182,2281,2283],{"id":2282},"what-about-nul-inside-data-not-file-names","What about NUL inside data, not file names?",[10,2285,2286],{},"If your records are arbitrary binary data that may contain NUL, no separator is safe; use a length-prefixed format or a structured encoding such as JSON lines with escaping.",[1182,2288,2290,2291,2293],{"id":2289},"is-surrogateescape-safe-to-print-to-a-terminal","Is ",[14,2292,1237],{}," safe to print to a terminal?",[10,2295,2296,2297,2300,2301,2304],{},"Printing a surrogate-escaped name through a text stream raises ",[14,2298,2299],{},"UnicodeEncodeError",". Write such names through the binary buffer, as the recipe does, or show them with ",[14,2302,2303],{},"repr()"," in messages meant for people.",[49,2306,2308],{"id":2307},"related","Related",[54,2310,2311,2317,2322,2327,2332],{},[57,2312,2313,2314],{},"Up: ",[43,2315,2316],{"href":45},"Working with stdin, stdout and pipes",[57,2318,2319],{},[43,2320,2321],{"href":62},"Reading piped input in Python CLIs",[57,2323,2324],{},[43,2325,2326],{"href":1375},"Handling broken pipe and SIGPIPE",[57,2328,2329],{},[43,2330,2331],{"href":1398},"Processing large files and NDJSON streams",[57,2333,2334],{},[43,2335,2337],{"href":2336},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002F","Cross-platform paths with pathlib",[2339,2340,2341],"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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":129,"searchDepth":143,"depth":143,"links":2343},[2344,2345,2346,2351,2352,2353,2354,2355,2363],{"id":51,"depth":143,"text":52},{"id":80,"depth":143,"text":81},{"id":121,"depth":143,"text":122,"children":2347},[2348,2349,2350],{"id":1184,"depth":161,"text":1185},{"id":1208,"depth":161,"text":1209},{"id":1248,"depth":161,"text":1249},{"id":1272,"depth":143,"text":1273},{"id":1329,"depth":143,"text":1330},{"id":1405,"depth":143,"text":1406},{"id":2210,"depth":143,"text":2211},{"id":2234,"depth":143,"text":2235,"children":2356},[2357,2358,2359,2360,2361],{"id":2238,"depth":161,"text":2239},{"id":2251,"depth":161,"text":2252},{"id":2266,"depth":161,"text":2267},{"id":2282,"depth":161,"text":2283},{"id":2289,"depth":161,"text":2362},"Is surrogateescape safe to print to a terminal?",{"id":2307,"depth":143,"text":2308},"2026-10-02","Make a Python CLI safe in find -print0 and xargs -0 pipelines: -0 and -z flags, streaming NUL-separated records, surrogateescape for odd file names, and tests.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility",{"title":5,"description":2365},"advanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility\u002Findex",[2374,40,36,2375,2376],"pipes","stdin","file-names","K0ejv7SibaZjXc4rv8UFFdbLZy5qcW0GFZuU6-F_FnE",[2379,2382,2385,2388,2391,2394,2397,2400,2403,2406,2409,2412,2415,2418,2421,2424,2427,2430,2433,2436,2439,2442,2445,2448,2451,2454,2457,2460,2463,2466,2469,2472,2475,2478,2481,2484,2487,2490,2493,2496,2499,2502,2505,2508,2511,2514,2517,2520,2523,2526,2529,2532,2535,2538,2541,2544,2547,2550,2553,2556,2559,2562,2565,2568,2571,2574,2577,2580,2583,2586,2589,2592,2595,2598,2601,2604,2607,2610,2613,2614,2617,2620,2623,2626,2629,2632,2635,2638,2641,2644,2647,2650,2653,2656,2659,2662,2665,2668,2671,2674,2677,2680,2683,2686,2689,2692,2695,2698,2701,2704,2707,2710,2713,2716,2719,2722,2725,2728,2731,2734,2737,2740,2743,2746,2749,2752,2755,2758,2761,2764,2767,2770,2773,2776,2779,2782,2785,2788,2791,2794,2796,2799,2802,2805,2808,2811,2814,2817,2820,2823,2826,2829,2832,2835,2838,2841,2844,2847,2850,2853,2856,2859,2862,2865,2868,2871,2874,2877,2880,2883,2886,2889,2892,2895,2898,2901,2904,2907,2910,2913,2916,2919,2922,2925,2928,2931,2934,2937,2940,2943,2946,2949,2952,2955,2958,2961,2964,2967,2970,2973,2976,2979,2982,2985,2988,2991,2994,2997,3000,3003,3006,3009,3012,3015,3018,3021,3024,3027,3030,3033,3036,3039,3042,3045,3048,3051,3054,3057,3060,3063,3066,3069,3072,3075,3078,3081,3084,3087,3090,3093,3096,3099,3102,3105,3108,3111,3114,3117,3120,3123,3126,3129,3132,3135,3138,3141,3144,3147,3150,3153,3156,3159,3162,3165,3168,3171,3174,3177,3180,3183,3186,3189,3192,3195,3198,3201,3204,3207,3210,3213,3216,3219],{"path":2380,"title":2381},"\u002Fabout","About Python CLI Toolcraft",{"path":2383,"title":2384},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2386,"title":2387},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2389,"title":2390},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":2392,"title":2393},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2395,"title":2396},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2398,"title":2399},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2401,"title":2402},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2404,"title":2405},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2407,"title":2408},"\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":2410,"title":2411},"\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":2413,"title":2414},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2416,"title":2417},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":2419,"title":2420},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2422,"title":2423},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2425,"title":2426},"\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":2428,"title":2429},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2431,"title":2432},"\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":2434,"title":2435},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2437,"title":2438},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":2440,"title":2441},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2443,"title":2444},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2446,"title":2447},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2449,"title":2450},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2452,"title":2453},"\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":2455,"title":2456},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2458,"title":2459},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2461,"title":2462},"\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":2464,"title":2465},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":2467,"title":2468},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2470,"title":2471},"\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":2473,"title":2474},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":2476,"title":2477},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2479,"title":2480},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2482,"title":2483},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2485,"title":2486},"\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":2488,"title":2489},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2491,"title":2492},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2494,"title":2495},"\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":2497,"title":2498},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2500,"title":2501},"\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":2503,"title":2504},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2506,"title":2507},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2509,"title":2510},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":2512,"title":2513},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":2515,"title":2516},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2518,"title":2519},"\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":2521,"title":2522},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2524,"title":2525},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":2527,"title":2528},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2530,"title":2531},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2533,"title":2534},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":2536,"title":2537},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2539,"title":2540},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2542,"title":2543},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":2545,"title":2546},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":2548,"title":2549},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":2551,"title":2552},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2554,"title":2555},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":2557,"title":2558},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":2560,"title":2561},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2563,"title":2564},"\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":2566,"title":2567},"\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":2569,"title":2570},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2572,"title":2573},"\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":2575,"title":2576},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2578,"title":2579},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2581,"title":2582},"\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":2584,"title":2585},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2587,"title":2588},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2590,"title":2591},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2593,"title":2594},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":2596,"title":2597},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2599,"title":2600},"\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":2602,"title":2603},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2605,"title":2606},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2608,"title":2609},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2611,"title":2612},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2370,"title":5},{"path":2615,"title":2616},"\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":2618,"title":2619},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2621,"title":2622},"\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":2624,"title":2625},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2627,"title":2628},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2630,"title":2631},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":2633,"title":2634},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2636,"title":2637},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2639,"title":2640},"\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":2642,"title":2643},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":2645,"title":2646},"\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":2648,"title":2649},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2651,"title":2652},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2654,"title":2655},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2657,"title":2658},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2660,"title":2661},"\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":2663,"title":2664},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":2666,"title":2667},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2669,"title":2670},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2672,"title":2673},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2675,"title":2676},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":2678,"title":2679},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2681,"title":2682},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2684,"title":2685},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2687,"title":2688},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":2690,"title":2691},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2693,"title":2694},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2696,"title":2697},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":2699,"title":2700},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2702,"title":2703},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":2705,"title":2706},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":2708,"title":2709},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":2711,"title":2712},"\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":2714,"title":2715},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2717,"title":2718},"\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":2720,"title":2721},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2723,"title":2724},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2726,"title":2727},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":2729,"title":2730},"\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":2732,"title":2733},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":2735,"title":2736},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2738,"title":2739},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2741,"title":2742},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2744,"title":2745},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2747,"title":2748},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":2750,"title":2751},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2753,"title":2754},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2756,"title":2757},"\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":2759,"title":2760},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2762,"title":2763},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2765,"title":2766},"\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":2768,"title":2769},"\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":2771,"title":2772},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2774,"title":2775},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2777,"title":2778},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2780,"title":2781},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":2783,"title":2784},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2786,"title":2787},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":2789,"title":2790},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":2792,"title":2793},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":16,"title":2795},"Python CLI Toolcraft",{"path":2797,"title":2798},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2800,"title":2801},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2803,"title":2804},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2806,"title":2807},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2809,"title":2810},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":2812,"title":2813},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":2815,"title":2816},"\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":2818,"title":2819},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2821,"title":2822},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2824,"title":2825},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2827,"title":2828},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2830,"title":2831},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2833,"title":2834},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2836,"title":2837},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2839,"title":2840},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":2842,"title":2843},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2845,"title":2846},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2848,"title":2849},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2851,"title":2852},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":2854,"title":2855},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2857,"title":2858},"\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":2860,"title":2861},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2863,"title":2864},"\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":2866,"title":2867},"\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":2869,"title":2870},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2872,"title":2873},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2875,"title":2876},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2878,"title":2879},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2881,"title":2882},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2884,"title":2885},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2887,"title":2888},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2890,"title":2891},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2893,"title":2894},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2896,"title":2897},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2899,"title":2900},"\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":2902,"title":2903},"\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":2905,"title":2906},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2908,"title":2909},"\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":2911,"title":2912},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2914,"title":2915},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":2917,"title":2918},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2920,"title":2921},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2923,"title":2924},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2926,"title":2927},"\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":2929,"title":2930},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2932,"title":2933},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2935,"title":2936},"\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":2938,"title":2939},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2941,"title":2942},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2944,"title":2945},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2947,"title":2948},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2950,"title":2951},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2953,"title":2954},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2956,"title":2957},"\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":2959,"title":2960},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2962,"title":2963},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":2965,"title":2966},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":2968,"title":2969},"\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":2971,"title":2972},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2974,"title":2975},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":2977,"title":2978},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2980,"title":2981},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2983,"title":2984},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2986,"title":2987},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2989,"title":2990},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2992,"title":2993},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":2995,"title":2996},"\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":2998,"title":2999},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":3001,"title":3002},"\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":3004,"title":3005},"\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":3007,"title":3008},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3010,"title":3011},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3013,"title":3014},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3016,"title":3017},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3019,"title":3020},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3022,"title":3023},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3025,"title":3026},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3028,"title":3029},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3031,"title":3032},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3034,"title":3035},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3037,"title":3038},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3040,"title":3041},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3043,"title":3044},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3046,"title":3047},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3049,"title":3050},"\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":3052,"title":3053},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3055,"title":3056},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3058,"title":3059},"\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":3061,"title":3062},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":3064,"title":3065},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3067,"title":3068},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":3070,"title":3071},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":3073,"title":3074},"\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":3076,"title":3077},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3079,"title":3080},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3082,"title":3083},"\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":3085,"title":3086},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3088,"title":3089},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3091,"title":3092},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3094,"title":3095},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":3097,"title":3098},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3100,"title":3101},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3103,"title":3104},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":3106,"title":3107},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3109,"title":3110},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3112,"title":3113},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3115,"title":3116},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3118,"title":3119},"\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":3121,"title":3122},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3124,"title":3125},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3127,"title":3128},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3130,"title":3131},"\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":3133,"title":3134},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3136,"title":3137},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3139,"title":3140},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3142,"title":3143},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3145,"title":3146},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":3148,"title":3149},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":3151,"title":3152},"\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":3154,"title":3155},"\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":3157,"title":3158},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":3160,"title":3161},"\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":3163,"title":3164},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":3166,"title":3167},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":3169,"title":3170},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3172,"title":3173},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3175,"title":3176},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3178,"title":3179},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":3181,"title":3182},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3184,"title":3185},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":3187,"title":3188},"\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":3190,"title":3191},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":3193,"title":3194},"\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":3196,"title":3197},"\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":3199,"title":3200},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3202,"title":3203},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":3205,"title":3206},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":3208,"title":3209},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3211,"title":3212},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3214,"title":3215},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3217,"title":3218},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3220,"title":3221},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967537242]