[{"data":1,"prerenderedAt":1562},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F":3,"content-directory":1314},{"id":4,"title":5,"body":6,"date":1298,"description":1299,"difficulty":1300,"draft":1301,"extension":1302,"meta":1303,"navigation":180,"path":1304,"seo":1305,"stem":1306,"tags":1307,"updated":1298,"__hash__":1313},"content\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002Findex.md","Emitting JSON Output for Scripting",{"type":7,"value":8,"toc":1276},"minimark",[9,22,27,75,79,88,92,96,111,366,467,489,493,496,499,618,621,628,631,635,638,644,650,656,662,679,686,690,693,699,734,745,760,832,836,850,868,877,883,887,894,1144,1147,1151,1157,1161,1166,1169,1173,1180,1184,1191,1195,1198,1202,1205,1212,1223,1227,1234,1238,1272],[10,11,12,13,17,18,21],"p",{},"The moment someone wants to automate your tool, they parse whatever it prints. A ",[14,15,16],"code",{},"--json"," flag\nturns that from a fragile exercise in ",[14,19,20],{},"awk"," into a documented interface — and moves your\nhuman-readable output back into the category of things you are free to improve.",[23,24,26],"h2",{"id":25},"tldr","TL;DR",[28,29,30,39,46,53,64],"ul",{},[31,32,33,34,38],"li",{},"One ",[35,36,37],"strong",{},"envelope shape"," across every command, not a shape per command.",[31,40,41,42,45],{},"Include a ",[35,43,44],{},"version"," field so consumers can branch when the shape changes.",[31,47,48,49,52],{},"Use ",[35,50,51],{},"NDJSON"," (one object per line) for streams; one document for bounded results.",[31,54,55,56,59,60,63],{},"JSON goes to ",[35,57,58],{},"stdout","; warnings, progress and errors stay on ",[35,61,62],{},"stderr",".",[31,65,66,67,70,71,74],{},"Signal failure with the ",[35,68,69],{},"exit code",", not a ",[14,72,73],{},"\"status\": \"error\""," field.",[23,76,78],{"id":77},"prerequisites","Prerequisites",[10,80,81,82,87],{},"A command that produces structured results, and the stream discipline from\n",[83,84,86],"a",{"href":85},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","working with stdin, stdout and pipes",":\nresults on stdout, everything else on stderr.",[23,89,91],{"id":90},"one-envelope-every-command","One envelope, every command",[93,94],"inline-diagram",{"name":95},"json-envelope-anatomy",[10,97,98,99,102,103,106,107,110],{},"A tool where ",[14,100,101],{},"status --json"," returns a bare list and ",[14,104,105],{},"list --json"," returns ",[14,108,109],{},"{\"items\": [...]}","\nforces every consumer to write a parser per command. One shape removes that:",[112,113,118],"pre",{"className":114,"code":115,"language":116,"meta":117,"style":117},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Foutput.py\nimport json\nimport sys\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nSCHEMA_VERSION = 1\n\n@dataclass\nclass Envelope:\n    data: Any\n    warnings: list[str] = field(default_factory=list)\n\n    def emit(self) -> None:\n        json.dump(\n            {\"version\": SCHEMA_VERSION, \"data\": self.data, \"warnings\": self.warnings},\n            sys.stdout,\n            default=str,          # dates and paths serialise predictably\n        )\n        sys.stdout.write(\"\\n\")\n","python","",[14,119,120,129,140,148,162,175,182,195,200,207,219,225,255,260,277,283,322,328,344,350],{"__ignoreMap":117},[121,122,125],"span",{"class":123,"line":124},"line",1,[121,126,128],{"class":127},"sJ8bj","# src\u002Fmytool\u002Foutput.py\n",[121,130,132,136],{"class":123,"line":131},2,[121,133,135],{"class":134},"szBVR","import",[121,137,139],{"class":138},"sVt8B"," json\n",[121,141,143,145],{"class":123,"line":142},3,[121,144,135],{"class":134},[121,146,147],{"class":138}," sys\n",[121,149,151,154,157,159],{"class":123,"line":150},4,[121,152,153],{"class":134},"from",[121,155,156],{"class":138}," dataclasses ",[121,158,135],{"class":134},[121,160,161],{"class":138}," dataclass, field\n",[121,163,165,167,170,172],{"class":123,"line":164},5,[121,166,153],{"class":134},[121,168,169],{"class":138}," typing ",[121,171,135],{"class":134},[121,173,174],{"class":138}," Any\n",[121,176,178],{"class":123,"line":177},6,[121,179,181],{"emptyLinePlaceholder":180},true,"\n",[121,183,185,189,192],{"class":123,"line":184},7,[121,186,188],{"class":187},"sj4cs","SCHEMA_VERSION",[121,190,191],{"class":134}," =",[121,193,194],{"class":187}," 1\n",[121,196,198],{"class":123,"line":197},8,[121,199,181],{"emptyLinePlaceholder":180},[121,201,203],{"class":123,"line":202},9,[121,204,206],{"class":205},"sScJk","@dataclass\n",[121,208,210,213,216],{"class":123,"line":209},10,[121,211,212],{"class":134},"class",[121,214,215],{"class":205}," Envelope",[121,217,218],{"class":138},":\n",[121,220,222],{"class":123,"line":221},11,[121,223,224],{"class":138},"    data: Any\n",[121,226,228,231,234,237,240,243,247,249,252],{"class":123,"line":227},12,[121,229,230],{"class":138},"    warnings: list[",[121,232,233],{"class":187},"str",[121,235,236],{"class":138},"] ",[121,238,239],{"class":134},"=",[121,241,242],{"class":138}," field(",[121,244,246],{"class":245},"s4XuR","default_factory",[121,248,239],{"class":134},[121,250,251],{"class":187},"list",[121,253,254],{"class":138},")\n",[121,256,258],{"class":123,"line":257},13,[121,259,181],{"emptyLinePlaceholder":180},[121,261,263,266,269,272,275],{"class":123,"line":262},14,[121,264,265],{"class":134},"    def",[121,267,268],{"class":205}," emit",[121,270,271],{"class":138},"(self) -> ",[121,273,274],{"class":187},"None",[121,276,218],{"class":138},[121,278,280],{"class":123,"line":279},15,[121,281,282],{"class":138},"        json.dump(\n",[121,284,286,289,293,296,298,301,304,306,309,312,315,317,319],{"class":123,"line":285},16,[121,287,288],{"class":138},"            {",[121,290,292],{"class":291},"sZZnC","\"version\"",[121,294,295],{"class":138},": ",[121,297,188],{"class":187},[121,299,300],{"class":138},", ",[121,302,303],{"class":291},"\"data\"",[121,305,295],{"class":138},[121,307,308],{"class":187},"self",[121,310,311],{"class":138},".data, ",[121,313,314],{"class":291},"\"warnings\"",[121,316,295],{"class":138},[121,318,308],{"class":187},[121,320,321],{"class":138},".warnings},\n",[121,323,325],{"class":123,"line":324},17,[121,326,327],{"class":138},"            sys.stdout,\n",[121,329,331,334,336,338,341],{"class":123,"line":330},18,[121,332,333],{"class":245},"            default",[121,335,239],{"class":134},[121,337,233],{"class":187},[121,339,340],{"class":138},",          ",[121,342,343],{"class":127},"# dates and paths serialise predictably\n",[121,345,347],{"class":123,"line":346},19,[121,348,349],{"class":138},"        )\n",[121,351,353,356,359,362,364],{"class":123,"line":352},20,[121,354,355],{"class":138},"        sys.stdout.write(",[121,357,358],{"class":291},"\"",[121,360,361],{"class":187},"\\n",[121,363,358],{"class":291},[121,365,254],{"class":138},[112,367,369],{"className":114,"code":368,"language":116,"meta":117,"style":117},"@app.command()\ndef status(json_output: Annotated[bool, typer.Option(\"--json\")] = False) -> None:\n    rows = core.collect_status()\n    if json_output:\n        Envelope(data=[row.as_dict() for row in rows]).emit()\n        return\n    render_table(rows)\n",[14,370,371,379,414,424,432,457,462],{"__ignoreMap":117},[121,372,373,376],{"class":123,"line":124},[121,374,375],{"class":205},"@app.command",[121,377,378],{"class":138},"()\n",[121,380,381,384,387,390,393,396,399,402,404,407,410,412],{"class":123,"line":131},[121,382,383],{"class":134},"def",[121,385,386],{"class":205}," status",[121,388,389],{"class":138},"(json_output: Annotated[",[121,391,392],{"class":187},"bool",[121,394,395],{"class":138},", typer.Option(",[121,397,398],{"class":291},"\"--json\"",[121,400,401],{"class":138},")] ",[121,403,239],{"class":134},[121,405,406],{"class":187}," False",[121,408,409],{"class":138},") -> ",[121,411,274],{"class":187},[121,413,218],{"class":138},[121,415,416,419,421],{"class":123,"line":142},[121,417,418],{"class":138},"    rows ",[121,420,239],{"class":134},[121,422,423],{"class":138}," core.collect_status()\n",[121,425,426,429],{"class":123,"line":150},[121,427,428],{"class":134},"    if",[121,430,431],{"class":138}," json_output:\n",[121,433,434,437,440,442,445,448,451,454],{"class":123,"line":164},[121,435,436],{"class":138},"        Envelope(",[121,438,439],{"class":245},"data",[121,441,239],{"class":134},[121,443,444],{"class":138},"[row.as_dict() ",[121,446,447],{"class":134},"for",[121,449,450],{"class":138}," row ",[121,452,453],{"class":134},"in",[121,455,456],{"class":138}," rows]).emit()\n",[121,458,459],{"class":123,"line":177},[121,460,461],{"class":134},"        return\n",[121,463,464],{"class":123,"line":184},[121,465,466],{"class":138},"    render_table(rows)\n",[10,468,469,470,472,473,476,477,480,481,484,485,488],{},"The ",[14,471,44],{}," field costs nothing now and is the only thing that lets you evolve the shape later\nwithout breaking every consumer at once. ",[14,474,475],{},"default=str"," is a small mercy: without it, a ",[14,478,479],{},"datetime"," or\na ",[14,482,483],{},"Path"," in your data raises ",[14,486,487],{},"TypeError"," at the worst possible moment.",[23,490,492],{"id":491},"streams-ndjson","Streams: NDJSON",[93,494],{"name":495},"ndjson-vs-array",[10,497,498],{},"For a bounded result — the status of four environments — one document is right, and it can be\npretty-printed when a human is reading it. For a stream of many records, newline-delimited JSON is\nbetter: one object per line, no enclosing array, no closing bracket to wait for.",[112,500,502],{"className":114,"code":501,"language":116,"meta":117,"style":117},"@app.command()\ndef export(\n    period: str,\n    json_output: Annotated[bool, typer.Option(\"--json\")] = False,\n) -> None:\n    for row in core.collect(period):              # a generator\n        if json_output:\n            sys.stdout.write(json.dumps(row.as_dict(), default=str) + \"\\n\")\n        else:\n            out.print(format_row(row))\n",[14,503,504,510,520,530,549,557,572,579,606,613],{"__ignoreMap":117},[121,505,506,508],{"class":123,"line":124},[121,507,375],{"class":205},[121,509,378],{"class":138},[121,511,512,514,517],{"class":123,"line":131},[121,513,383],{"class":134},[121,515,516],{"class":205}," export",[121,518,519],{"class":138},"(\n",[121,521,522,525,527],{"class":123,"line":142},[121,523,524],{"class":138},"    period: ",[121,526,233],{"class":187},[121,528,529],{"class":138},",\n",[121,531,532,535,537,539,541,543,545,547],{"class":123,"line":150},[121,533,534],{"class":138},"    json_output: Annotated[",[121,536,392],{"class":187},[121,538,395],{"class":138},[121,540,398],{"class":291},[121,542,401],{"class":138},[121,544,239],{"class":134},[121,546,406],{"class":187},[121,548,529],{"class":138},[121,550,551,553,555],{"class":123,"line":164},[121,552,409],{"class":138},[121,554,274],{"class":187},[121,556,218],{"class":138},[121,558,559,562,564,566,569],{"class":123,"line":177},[121,560,561],{"class":134},"    for",[121,563,450],{"class":138},[121,565,453],{"class":134},[121,567,568],{"class":138}," core.collect(period):              ",[121,570,571],{"class":127},"# a generator\n",[121,573,574,577],{"class":123,"line":184},[121,575,576],{"class":134},"        if",[121,578,431],{"class":138},[121,580,581,584,587,589,591,594,597,600,602,604],{"class":123,"line":197},[121,582,583],{"class":138},"            sys.stdout.write(json.dumps(row.as_dict(), ",[121,585,586],{"class":245},"default",[121,588,239],{"class":134},[121,590,233],{"class":187},[121,592,593],{"class":138},") ",[121,595,596],{"class":134},"+",[121,598,599],{"class":291}," \"",[121,601,361],{"class":187},[121,603,358],{"class":291},[121,605,254],{"class":138},[121,607,608,611],{"class":123,"line":202},[121,609,610],{"class":134},"        else",[121,612,218],{"class":138},[121,614,615],{"class":123,"line":209},[121,616,617],{"class":138},"            out.print(format_row(row))\n",[93,619],{"name":620},"streaming-memory-bars",[10,622,623,624,627],{},"Two properties follow immediately. Memory stays flat regardless of how many records there are. And\na consumer — ",[14,625,626],{},"jq --unbuffered",", another instance of your tool, a script reading line by line — can\nstart working before your command finishes, which matters when the export takes ten minutes.",[10,629,630],{},"Say which you emit in the help text. \"One JSON object per line\" is a one-line promise that saves a\nconsumer an experiment.",[23,632,634],{"id":633},"versioning-the-shape","Versioning the shape",[10,636,637],{},"The JSON output is an interface with the same rules as your flags. Some changes are safe and some\nare not:",[10,639,640,643],{},[35,641,642],{},"Adding a key"," is safe. Consumers that do not know about it ignore it, which is why documenting\n\"new keys may be added\" up front is worth doing — it makes the addition unambiguously non-breaking.",[10,645,646,649],{},[35,647,648],{},"Renaming or removing a key"," is breaking. Deprecate the way you would a flag: emit both for a\nrelease or two, note it in the changelog, then drop the old one at a major version.",[10,651,652,655],{},[35,653,654],{},"Changing a value's type or units"," is the worst case, exactly as with flags — nothing looks\ndifferent and every consumer silently misreads it. Add a new key instead.",[10,657,658,659,661],{},"When the shape does need to change incompatibly, the ",[14,660,44],{}," field is what lets a consumer cope:",[112,663,665],{"className":114,"code":664,"language":116,"meta":117,"style":117},"SCHEMA_VERSION = 2      # data.items[] gained \"region\"; \"location\" removed\n",[14,666,667],{"__ignoreMap":117},[121,668,669,671,673,676],{"class":123,"line":124},[121,670,188],{"class":187},[121,672,191],{"class":134},[121,674,675],{"class":187}," 2",[121,677,678],{"class":127},"      # data.items[] gained \"region\"; \"location\" removed\n",[10,680,681,682,685],{},"Bump it, document what changed in the changelog, and — if the change is significant — consider\nsupporting ",[14,683,684],{},"--json-version 1"," for a transition period.",[23,687,689],{"id":688},"what-does-not-belong-in-the-json","What does not belong in the JSON",[10,691,692],{},"Two things people put in the payload that belong elsewhere.",[10,694,695,698],{},[35,696,697],{},"Failure status."," A shell script branches on the exit code, not on a field:",[112,700,704],{"className":701,"code":702,"language":703,"meta":117,"style":117},"language-bash shiki shiki-themes github-light github-dark","if mytool status --json > status.json; then …\n","bash",[14,705,706],{"__ignoreMap":117},[121,707,708,711,714,716,719,722,725,728,731],{"class":123,"line":124},[121,709,710],{"class":134},"if",[121,712,713],{"class":205}," mytool",[121,715,386],{"class":291},[121,717,718],{"class":187}," --json",[121,720,721],{"class":134}," >",[121,723,724],{"class":291}," status.json",[121,726,727],{"class":138},"; ",[121,729,730],{"class":134},"then",[121,732,733],{"class":205}," …\n",[10,735,736,737,740,741,744],{},"A command that exits 0 with ",[14,738,739],{},"{\"status\": \"error\"}"," breaks that idiom and every ",[14,742,743],{},"set -e"," script in\nthe world. Use the exit code, and put the message on stderr.",[10,746,747,750,751,755,756,759],{},[35,748,749],{},"Progress and warnings."," Progress belongs on stderr. Warnings ",[752,753,754],"em",{},"may"," appear in the envelope's\n",[14,757,758],{},"warnings"," list — machine-readable and useful — but they should also be printed on stderr for the\nhuman, and neither should ever appear on stdout as loose text.",[112,761,763],{"className":114,"code":762,"language":116,"meta":117,"style":117},"err.print(f\"[yellow]{len(skipped)} records skipped[\u002F]\")            # for the human\nEnvelope(data=rows, warnings=[f\"{len(skipped)} records skipped\"]).emit()   # for the script\n",[14,764,765,794],{"__ignoreMap":117},[121,766,767,770,773,776,779,782,785,788,791],{"class":123,"line":124},[121,768,769],{"class":138},"err.print(",[121,771,772],{"class":134},"f",[121,774,775],{"class":291},"\"[yellow]",[121,777,778],{"class":187},"{len",[121,780,781],{"class":138},"(skipped)",[121,783,784],{"class":187},"}",[121,786,787],{"class":291}," records skipped[\u002F]\"",[121,789,790],{"class":138},")            ",[121,792,793],{"class":127},"# for the human\n",[121,795,796,799,801,803,806,808,810,813,815,817,819,821,823,826,829],{"class":123,"line":131},[121,797,798],{"class":138},"Envelope(",[121,800,439],{"class":245},[121,802,239],{"class":134},[121,804,805],{"class":138},"rows, ",[121,807,758],{"class":245},[121,809,239],{"class":134},[121,811,812],{"class":138},"[",[121,814,772],{"class":134},[121,816,358],{"class":291},[121,818,778],{"class":187},[121,820,781],{"class":138},[121,822,784],{"class":187},[121,824,825],{"class":291}," records skipped\"",[121,827,828],{"class":138},"]).emit()   ",[121,830,831],{"class":127},"# for the script\n",[23,833,835],{"id":834},"ux-considerations","UX considerations",[10,837,838,841,842,845,846,849],{},[35,839,840],{},"Pretty-print for humans, compact for pipes."," When stdout is a terminal, indented JSON with\ncolour is much easier to read; when it is piped, compact output is smaller and faster to parse.\nRich's ",[14,843,844],{},"print_json"," handles the terminal case, and the decision follows the same ",[14,847,848],{},"isatty"," check as\neverything else.",[10,851,852,855,856,859,860,863,864,867],{},[35,853,854],{},"Keep key names stable and boring."," ",[14,857,858],{},"created_at"," rather than ",[14,861,862],{},"createdAt"," or ",[14,865,866],{},"ctime","; the same\nname for the same concept in every command. Consumers write code against these strings, and\nconsistency is worth more than elegance.",[10,869,870,855,873,876],{},[35,871,872],{},"Sort keys when the output might be diffed.",[14,874,875],{},"json.dumps(..., sort_keys=True)"," makes two runs\ncomparable, which matters when people use your export to detect drift between environments.",[10,878,879,882],{},[35,880,881],{},"Document one example."," A single realistic document in the help epilog or the reference tells a\nconsumer more than a schema description of the same size.",[23,884,886],{"id":885},"testing-the-behaviour","Testing the behaviour",[10,888,889,890,893],{},"The valuable assertion is that stdout parses on its own — because that is what breaks when\nsomebody adds a stray ",[14,891,892],{},"print",":",[112,895,897],{"className":114,"code":896,"language":116,"meta":117,"style":117},"def test_json_output_is_parseable_and_stdout_is_clean(cli):\n    result = cli.invoke(app, [\"status\", \"--json\"])\n\n    assert result.exit_code == 0\n    payload = json.loads(result.stdout)          # fails loudly on any non-JSON line\n    assert payload[\"version\"] == 1\n    assert isinstance(payload[\"data\"], list)\n\ndef test_progress_does_not_contaminate_stdout(cli):\n    result = cli.invoke(app, [\"export\", \"7d\", \"--json\"])\n\n    for line in result.stdout.splitlines():\n        json.loads(line)                          # NDJSON: every line stands alone\n    assert \"exported\" in result.stderr\n\ndef test_failure_uses_the_exit_code_not_a_field(cli):\n    result = cli.invoke(app, [\"status\", \"--json\"], env={\"MYTOOL_ENDPOINT\": \"http:\u002F\u002F127.0.0.1:1\"})\n\n    assert result.exit_code == 69\n    assert result.stdout == \"\"                    # no half-written payload\n",[14,898,899,909,929,933,947,960,975,994,998,1007,1029,1033,1045,1053,1066,1070,1079,1114,1118,1129],{"__ignoreMap":117},[121,900,901,903,906],{"class":123,"line":124},[121,902,383],{"class":134},[121,904,905],{"class":205}," test_json_output_is_parseable_and_stdout_is_clean",[121,907,908],{"class":138},"(cli):\n",[121,910,911,914,916,919,922,924,926],{"class":123,"line":131},[121,912,913],{"class":138},"    result ",[121,915,239],{"class":134},[121,917,918],{"class":138}," cli.invoke(app, [",[121,920,921],{"class":291},"\"status\"",[121,923,300],{"class":138},[121,925,398],{"class":291},[121,927,928],{"class":138},"])\n",[121,930,931],{"class":123,"line":142},[121,932,181],{"emptyLinePlaceholder":180},[121,934,935,938,941,944],{"class":123,"line":150},[121,936,937],{"class":134},"    assert",[121,939,940],{"class":138}," result.exit_code ",[121,942,943],{"class":134},"==",[121,945,946],{"class":187}," 0\n",[121,948,949,952,954,957],{"class":123,"line":164},[121,950,951],{"class":138},"    payload ",[121,953,239],{"class":134},[121,955,956],{"class":138}," json.loads(result.stdout)          ",[121,958,959],{"class":127},"# fails loudly on any non-JSON line\n",[121,961,962,964,967,969,971,973],{"class":123,"line":177},[121,963,937],{"class":134},[121,965,966],{"class":138}," payload[",[121,968,292],{"class":291},[121,970,236],{"class":138},[121,972,943],{"class":134},[121,974,194],{"class":187},[121,976,977,979,982,985,987,990,992],{"class":123,"line":184},[121,978,937],{"class":134},[121,980,981],{"class":187}," isinstance",[121,983,984],{"class":138},"(payload[",[121,986,303],{"class":291},[121,988,989],{"class":138},"], ",[121,991,251],{"class":187},[121,993,254],{"class":138},[121,995,996],{"class":123,"line":197},[121,997,181],{"emptyLinePlaceholder":180},[121,999,1000,1002,1005],{"class":123,"line":202},[121,1001,383],{"class":134},[121,1003,1004],{"class":205}," test_progress_does_not_contaminate_stdout",[121,1006,908],{"class":138},[121,1008,1009,1011,1013,1015,1018,1020,1023,1025,1027],{"class":123,"line":209},[121,1010,913],{"class":138},[121,1012,239],{"class":134},[121,1014,918],{"class":138},[121,1016,1017],{"class":291},"\"export\"",[121,1019,300],{"class":138},[121,1021,1022],{"class":291},"\"7d\"",[121,1024,300],{"class":138},[121,1026,398],{"class":291},[121,1028,928],{"class":138},[121,1030,1031],{"class":123,"line":221},[121,1032,181],{"emptyLinePlaceholder":180},[121,1034,1035,1037,1040,1042],{"class":123,"line":227},[121,1036,561],{"class":134},[121,1038,1039],{"class":138}," line ",[121,1041,453],{"class":134},[121,1043,1044],{"class":138}," result.stdout.splitlines():\n",[121,1046,1047,1050],{"class":123,"line":257},[121,1048,1049],{"class":138},"        json.loads(line)                          ",[121,1051,1052],{"class":127},"# NDJSON: every line stands alone\n",[121,1054,1055,1057,1060,1063],{"class":123,"line":262},[121,1056,937],{"class":134},[121,1058,1059],{"class":291}," \"exported\"",[121,1061,1062],{"class":134}," in",[121,1064,1065],{"class":138}," result.stderr\n",[121,1067,1068],{"class":123,"line":279},[121,1069,181],{"emptyLinePlaceholder":180},[121,1071,1072,1074,1077],{"class":123,"line":285},[121,1073,383],{"class":134},[121,1075,1076],{"class":205}," test_failure_uses_the_exit_code_not_a_field",[121,1078,908],{"class":138},[121,1080,1081,1083,1085,1087,1089,1091,1093,1095,1098,1100,1103,1106,1108,1111],{"class":123,"line":324},[121,1082,913],{"class":138},[121,1084,239],{"class":134},[121,1086,918],{"class":138},[121,1088,921],{"class":291},[121,1090,300],{"class":138},[121,1092,398],{"class":291},[121,1094,989],{"class":138},[121,1096,1097],{"class":245},"env",[121,1099,239],{"class":134},[121,1101,1102],{"class":138},"{",[121,1104,1105],{"class":291},"\"MYTOOL_ENDPOINT\"",[121,1107,295],{"class":138},[121,1109,1110],{"class":291},"\"http:\u002F\u002F127.0.0.1:1\"",[121,1112,1113],{"class":138},"})\n",[121,1115,1116],{"class":123,"line":330},[121,1117,181],{"emptyLinePlaceholder":180},[121,1119,1120,1122,1124,1126],{"class":123,"line":346},[121,1121,937],{"class":134},[121,1123,940],{"class":138},[121,1125,943],{"class":134},[121,1127,1128],{"class":187}," 69\n",[121,1130,1131,1133,1136,1138,1141],{"class":123,"line":352},[121,1132,937],{"class":134},[121,1134,1135],{"class":138}," result.stdout ",[121,1137,943],{"class":134},[121,1139,1140],{"class":291}," \"\"",[121,1142,1143],{"class":127},"                    # no half-written payload\n",[10,1145,1146],{},"That last test encodes a decision worth making explicitly: on failure, emit nothing on stdout\nrather than a partial document. A consumer that receives half a JSON object cannot tell whether the\ndata was incomplete or the parse failed.",[23,1148,1150],{"id":1149},"conclusion","Conclusion",[10,1152,1153,1154,1156],{},"A ",[14,1155,16],{}," flag is a small feature with a large effect: it gives scripts a stable interface and\ngives you back the freedom to change human-readable output. Keep one envelope, version it, choose\nNDJSON for streams, keep stdout clean, and let the exit code carry failure.",[23,1158,1160],{"id":1159},"frequently-asked-questions","Frequently asked questions",[1162,1163,1165],"h3",{"id":1164},"should-json-be-the-default-output-format","Should JSON be the default output format?",[10,1167,1168],{},"Not for an interactive tool. Default to something a person can read and switch to JSON on an\nexplicit flag or an environment variable that your deployment sets. Defaulting to JSON punishes the\nperson at a terminal to please a script that could just as easily pass the flag.",[1162,1170,1172],{"id":1171},"how-do-i-keep-the-human-and-json-output-in-step","How do I keep the human and JSON output in step?",[10,1174,1175,1176,1179],{},"Render both from the same data structure. If the table is built from a list of dataclasses and the\nJSON is ",[14,1177,1178],{},"[row.as_dict() for row in rows]",", they cannot show different facts. Formatting twice from\ntwo different queries is how a tool ends up reporting two different numbers.",[1162,1181,1183],{"id":1182},"what-about-yaml-or-csv-output","What about YAML or CSV output?",[10,1185,1186,1187,1190],{},"Add them only if your audience asks. CSV is genuinely useful for tabular results people open in a\nspreadsheet; YAML rarely earns its place next to JSON. If you do add a format, use one ",[14,1188,1189],{},"--format","\noption rather than a flag per format, so the choice stays exclusive.",[1162,1192,1194],{"id":1193},"should-the-envelope-include-timing-or-metadata","Should the envelope include timing or metadata?",[10,1196,1197],{},"A little is helpful — a generated timestamp, the tool version — provided it lives in its own key\nrather than being mixed into the data. Be aware that variable fields make output harder to diff and\nharder to snapshot in tests, so keep them out of the payload proper.",[1162,1199,1201],{"id":1200},"how-do-i-handle-very-large-single-documents","How do I handle very large single documents?",[10,1203,1204],{},"Prefer NDJSON, which sidesteps the question. If the result genuinely is one large document, stream\nit with an incremental encoder rather than building the whole string in memory, and consider whether\nthe command should paginate — a hundred-megabyte JSON response is difficult for consumers too.",[1162,1206,1208,1209,1211],{"id":1207},"should-json-suppress-everything-else","Should ",[14,1210,16],{}," suppress everything else?",[10,1213,1214,1215,1218,1219,1222],{},"It should suppress everything else ",[752,1216,1217],{},"on stdout"," — no tables, no summary line, nothing but the\npayload. Progress and warnings on stderr can stay exactly as they were, because they never enter\nthe pipeline. A flag that also silences stderr is a different feature, and ",[14,1220,1221],{},"--quiet"," is its name.",[1162,1224,1226],{"id":1225},"how-do-i-let-consumers-discover-the-shape","How do I let consumers discover the shape?",[10,1228,1229,1230,1233],{},"Ship a ",[14,1231,1232],{},"mytool schema"," command that prints the JSON Schema for your envelope, and mention it in the\nhelp epilog. It is a small amount of work, it gives consumers something to validate against, and it\nturns \"what fields come back\" from a support question into a command. Generating the schema from the\nsame models you serialise keeps the two from drifting.",[23,1235,1237],{"id":1236},"related","Related",[28,1239,1240,1246,1253,1259,1266],{},[31,1241,1242,1243],{},"Up: ",[83,1244,1245],{"href":85},"Working with stdin, stdout and pipes",[31,1247,1248,1249],{},"Sideways: ",[83,1250,1252],{"href":1251},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis\u002F","Reading piped input in Python CLIs",[31,1254,1248,1255],{},[83,1256,1258],{"href":1257},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002F","Detecting a TTY and adapting output",[31,1260,1261,1262],{},"Related: ",[83,1263,1265],{"href":1264},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis\u002F","Structured JSON logging in Python CLIs",[31,1267,1261,1268],{},[83,1269,1271],{"href":1270},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002F","Versioning and deprecating CLI flags",[1273,1274,1275],"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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":117,"searchDepth":131,"depth":131,"links":1277},[1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1297],{"id":25,"depth":131,"text":26},{"id":77,"depth":131,"text":78},{"id":90,"depth":131,"text":91},{"id":491,"depth":131,"text":492},{"id":633,"depth":131,"text":634},{"id":688,"depth":131,"text":689},{"id":834,"depth":131,"text":835},{"id":885,"depth":131,"text":886},{"id":1149,"depth":131,"text":1150},{"id":1159,"depth":131,"text":1160,"children":1288},[1289,1290,1291,1292,1293,1294,1296],{"id":1164,"depth":142,"text":1165},{"id":1171,"depth":142,"text":1172},{"id":1182,"depth":142,"text":1183},{"id":1193,"depth":142,"text":1194},{"id":1200,"depth":142,"text":1201},{"id":1207,"depth":142,"text":1295},"Should --json suppress everything else?",{"id":1225,"depth":142,"text":1226},{"id":1236,"depth":131,"text":1237},"2026-08-01","Give a Python CLI machine-readable output - one stable envelope, NDJSON for streams, versioning the shape, and keeping stdout free of everything else.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting",{"title":5,"description":1299},"advanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002Findex",[1308,1309,1310,1311,1312],"json","output","scripting","ndjson","api","ctOEJjzpc6KgiklV7eMoyF9Z1i0lwvqQoGf9viYiVBs",[1315,1318,1321,1324,1327,1330,1333,1336,1339,1342,1345,1348,1351,1354,1357,1360,1363,1366,1369,1372,1375,1378,1381,1384,1387,1390,1393,1396,1397,1400,1403,1406,1409,1412,1415,1418,1421,1424,1427,1430,1433,1436,1439,1442,1445,1448,1451,1454,1457,1460,1463,1466,1469,1472,1475,1478,1481,1484,1487,1490,1493,1496,1499,1502,1505,1508,1511,1514,1517,1520,1523,1526,1529,1532,1535,1538,1541,1544,1547,1550,1553,1556,1559],{"path":1316,"title":1317},"\u002Fabout","About Python CLI Toolcraft",{"path":1319,"title":1320},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1322,"title":1323},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1325,"title":1326},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1328,"title":1329},"\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":1331,"title":1332},"\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":1334,"title":1335},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1337,"title":1338},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1340,"title":1341},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1343,"title":1344},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1346,"title":1347},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1349,"title":1350},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1352,"title":1353},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1355,"title":1356},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1358,"title":1359},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1361,"title":1362},"\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":1364,"title":1365},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1367,"title":1368},"\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":1370,"title":1371},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1373,"title":1374},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1376,"title":1377},"\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":1379,"title":1380},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1382,"title":1383},"\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":1385,"title":1386},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1388,"title":1389},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1391,"title":1392},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1394,"title":1395},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1304,"title":5},{"path":1398,"title":1399},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1401,"title":1402},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1404,"title":1405},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1407,"title":1408},"\u002F","Python CLI Toolcraft",{"path":1410,"title":1411},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1413,"title":1414},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1416,"title":1417},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1419,"title":1420},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1422,"title":1423},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1425,"title":1426},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1428,"title":1429},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1431,"title":1432},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1434,"title":1435},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1437,"title":1438},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1440,"title":1441},"\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":1443,"title":1444},"\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":1446,"title":1447},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1449,"title":1450},"\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":1452,"title":1453},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1455,"title":1456},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1458,"title":1459},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1461,"title":1462},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1464,"title":1465},"\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":1467,"title":1468},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1470,"title":1471},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1473,"title":1474},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1476,"title":1477},"\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":1479,"title":1480},"\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":1482,"title":1483},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1485,"title":1486},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1488,"title":1489},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1491,"title":1492},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1494,"title":1495},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1497,"title":1498},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1500,"title":1501},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1503,"title":1504},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1506,"title":1507},"\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":1509,"title":1510},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1512,"title":1513},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1515,"title":1516},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1518,"title":1519},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1521,"title":1522},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1524,"title":1525},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1527,"title":1528},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1530,"title":1531},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1533,"title":1534},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1536,"title":1537},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1539,"title":1540},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1542,"title":1543},"\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":1545,"title":1546},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1548,"title":1549},"\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":1551,"title":1552},"\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":1554,"title":1555},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1557,"title":1558},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1560,"title":1561},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690030]