[{"data":1,"prerenderedAt":1608},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002F":3,"content-directory":1360},{"id":4,"title":5,"body":6,"date":1344,"description":1345,"difficulty":1346,"draft":1347,"extension":1348,"meta":1349,"navigation":510,"path":1350,"seo":1351,"stem":1352,"tags":1353,"updated":1344,"__hash__":1359},"content\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002Findex.md","Versioning and Deprecating CLI Flags",{"type":7,"value":8,"toc":1323},"minimark",[9,18,23,64,68,77,81,85,88,98,104,117,121,124,127,133,139,149,155,165,169,172,175,412,415,634,648,651,805,808,812,815,891,894,898,908,917,923,929,933,936,1203,1206,1210,1213,1217,1222,1225,1229,1232,1236,1239,1243,1254,1258,1261,1265,1268,1272,1281,1285,1319],[10,11,12,13,17],"p",{},"A flag name is a public API. Somewhere there is a ",[14,15,16],"code",{},"Makefile",", a cron entry or a CI step that types\nit, and the person who wrote it is not reading your release notes. Retiring a flag safely is a\nsequence rather than an edit: alias, warn, hide, remove.",[19,20,22],"h2",{"id":21},"tldr","TL;DR",[24,25,26,35,46,53,61],"ul",{},[27,28,29,30,34],"li",{},"Renaming a flag, changing its default, or changing what it means are all ",[31,32,33],"strong",{},"breaking changes",".",[27,36,37,38,41,42,45],{},"Keep the old name as a ",[31,39,40],{},"hidden alias",", warn on ",[31,43,44],{},"stderr",", and name both the replacement and\nthe removal version.",[27,47,48,49,52],{},"Leave at least ",[31,50,51],{},"two minor releases"," between the warning and the removal.",[27,54,55,56,60],{},"Changing a flag's ",[57,58,59],"em",{},"meaning"," while keeping its name is the one users cannot detect — never do it.",[27,62,63],{},"Record every step in the changelog; that is where a script owner will look.",[19,65,67],{"id":66},"prerequisites","Prerequisites",[10,69,70,71,76],{},"A released CLI with users, a version scheme (see\n",[72,73,75],"a",{"href":74},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning","),\nand a changelog people can find.",[19,78,80],{"id":79},"what-counts-as-breaking","What counts as breaking",[82,83],"inline-diagram",{"name":84},"flag-change-matrix",[10,86,87],{},"The test is whether an invocation that worked yesterday behaves differently today. By that measure\nsome changes people treat as minor are not:",[10,89,90,93,94,97],{},[31,91,92],{},"Changing a default."," ",[14,95,96],{},"--retries"," moving from 3 to 5 changes behaviour for every user who never\npassed the flag, without a single edit on their side. If the new default is genuinely better, ship\nit in a major release and say so prominently.",[10,99,100,103],{},[31,101,102],{},"Tightening validation."," Rejecting input you previously accepted fixes a bug and breaks a\npipeline that relied on the leniency. It is still the right change; it is a major one.",[10,105,106,93,109,112,113,116],{},[31,107,108],{},"Changing what a flag means.",[14,110,111],{},"--timeout"," moving from milliseconds to seconds while keeping its\nname is the worst case, because nothing looks wrong and the failure is silent. Add\n",[14,114,115],{},"--timeout-seconds"," instead and deprecate the old name — a rename users can see beats a redefinition\nthey cannot.",[19,118,120],{"id":119},"the-deprecation-sequence","The deprecation sequence",[82,122],{"name":123},"deprecation-timeline",[10,125,126],{},"Four steps, spread across releases. Each is small; the spacing is what makes it safe.",[10,128,129,132],{},[31,130,131],{},"1. Add the replacement, accept both."," No warning yet. Users who read release notes migrate\nearly; nobody is nagged.",[10,134,135,138],{},[31,136,137],{},"2. Warn."," The old name still works and produces one line on stderr naming the replacement and the\nremoval version.",[10,140,141,144,145,148],{},[31,142,143],{},"3. Hide it."," Remove it from ",[14,146,147],{},"--help"," so new users never learn it, while existing scripts keep\nworking.",[10,150,151,154],{},[31,152,153],{},"4. Remove it."," At a major version, with a changelog entry, and ideally with an error message that\nstill recognises the old name and says what replaced it.",[10,156,157,158,161,162,164],{},"That last touch is worth the five lines. A user whose script breaks gets \"unknown option\n",[14,159,160],{},"--retry-count","; it was renamed to ",[14,163,96],{}," in 2.0\" instead of a generic usage error.",[19,166,168],{"id":167},"implementing-the-alias","Implementing the alias",[82,170],{"name":171},"alias-resolution-flow",[10,173,174],{},"In Click, a parameter can carry several names, and the extra one can be hidden:",[176,177,182],"pre",{"className":178,"code":179,"language":180,"meta":181,"style":181},"language-python shiki shiki-themes github-light github-dark","@click.option(\"--retries\", \"retries\", type=int, default=3, show_default=True,\n              help=\"Attempts per file before giving up.\")\n@click.option(\"--retry-count\", \"retry_count\", type=int, default=None, hidden=True)\ndef sync(retries: int, retry_count: int | None) -> None:\n    if retry_count is not None:\n        click.secho(\n            \"--retry-count is deprecated and will be removed in 2.0; use --retries\",\n            fg=\"yellow\", err=True,\n        )\n        retries = retry_count\n    ...\n","python","",[14,183,184,244,258,301,334,353,359,367,389,395,406],{"__ignoreMap":181},[185,186,189,193,197,201,204,207,209,213,217,221,223,226,228,231,233,236,238,241],"span",{"class":187,"line":188},"line",1,[185,190,192],{"class":191},"sScJk","@click.option",[185,194,196],{"class":195},"sVt8B","(",[185,198,200],{"class":199},"sZZnC","\"--retries\"",[185,202,203],{"class":195},", ",[185,205,206],{"class":199},"\"retries\"",[185,208,203],{"class":195},[185,210,212],{"class":211},"s4XuR","type",[185,214,216],{"class":215},"szBVR","=",[185,218,220],{"class":219},"sj4cs","int",[185,222,203],{"class":195},[185,224,225],{"class":211},"default",[185,227,216],{"class":215},[185,229,230],{"class":219},"3",[185,232,203],{"class":195},[185,234,235],{"class":211},"show_default",[185,237,216],{"class":215},[185,239,240],{"class":219},"True",[185,242,243],{"class":195},",\n",[185,245,247,250,252,255],{"class":187,"line":246},2,[185,248,249],{"class":211},"              help",[185,251,216],{"class":215},[185,253,254],{"class":199},"\"Attempts per file before giving up.\"",[185,256,257],{"class":195},")\n",[185,259,261,263,265,268,270,273,275,277,279,281,283,285,287,290,292,295,297,299],{"class":187,"line":260},3,[185,262,192],{"class":191},[185,264,196],{"class":195},[185,266,267],{"class":199},"\"--retry-count\"",[185,269,203],{"class":195},[185,271,272],{"class":199},"\"retry_count\"",[185,274,203],{"class":195},[185,276,212],{"class":211},[185,278,216],{"class":215},[185,280,220],{"class":219},[185,282,203],{"class":195},[185,284,225],{"class":211},[185,286,216],{"class":215},[185,288,289],{"class":219},"None",[185,291,203],{"class":195},[185,293,294],{"class":211},"hidden",[185,296,216],{"class":215},[185,298,240],{"class":219},[185,300,257],{"class":195},[185,302,304,307,310,313,315,318,320,323,326,329,331],{"class":187,"line":303},4,[185,305,306],{"class":215},"def",[185,308,309],{"class":191}," sync",[185,311,312],{"class":195},"(retries: ",[185,314,220],{"class":219},[185,316,317],{"class":195},", retry_count: ",[185,319,220],{"class":219},[185,321,322],{"class":215}," |",[185,324,325],{"class":219}," None",[185,327,328],{"class":195},") -> ",[185,330,289],{"class":219},[185,332,333],{"class":195},":\n",[185,335,337,340,343,346,349,351],{"class":187,"line":336},5,[185,338,339],{"class":215},"    if",[185,341,342],{"class":195}," retry_count ",[185,344,345],{"class":215},"is",[185,347,348],{"class":215}," not",[185,350,325],{"class":219},[185,352,333],{"class":195},[185,354,356],{"class":187,"line":355},6,[185,357,358],{"class":195},"        click.secho(\n",[185,360,362,365],{"class":187,"line":361},7,[185,363,364],{"class":199},"            \"--retry-count is deprecated and will be removed in 2.0; use --retries\"",[185,366,243],{"class":195},[185,368,370,373,375,378,380,383,385,387],{"class":187,"line":369},8,[185,371,372],{"class":211},"            fg",[185,374,216],{"class":215},[185,376,377],{"class":199},"\"yellow\"",[185,379,203],{"class":195},[185,381,382],{"class":211},"err",[185,384,216],{"class":215},[185,386,240],{"class":219},[185,388,243],{"class":195},[185,390,392],{"class":187,"line":391},9,[185,393,394],{"class":195},"        )\n",[185,396,398,401,403],{"class":187,"line":397},10,[185,399,400],{"class":195},"        retries ",[185,402,216],{"class":215},[185,404,405],{"class":195}," retry_count\n",[185,407,409],{"class":187,"line":408},11,[185,410,411],{"class":219},"    ...\n",[10,413,414],{},"In Typer, a parameter callback keeps the deprecation out of the command body:",[176,416,418],{"className":178,"code":417,"language":180,"meta":181,"style":181},"def deprecated_retry_count(value: int | None) -> int | None:\n    if value is not None:\n        typer.secho(\n            \"--retry-count is deprecated and will be removed in 2.0; use --retries\",\n            fg=typer.colors.YELLOW, err=True,\n        )\n    return value\n\n@app.command()\ndef sync(\n    retries: Annotated[int, typer.Option(help=\"Attempts per file.\")] = 3,\n    retry_count: Annotated[int | None, typer.Option(hidden=True, callback=deprecated_retry_count)] = None,\n) -> None:\n    retries = retry_count if retry_count is not None else retries\n    ...\n",[14,419,420,446,461,466,472,494,498,506,512,520,529,557,593,602,629],{"__ignoreMap":181},[185,421,422,424,427,430,432,434,436,438,440,442,444],{"class":187,"line":188},[185,423,306],{"class":215},[185,425,426],{"class":191}," deprecated_retry_count",[185,428,429],{"class":195},"(value: ",[185,431,220],{"class":219},[185,433,322],{"class":215},[185,435,325],{"class":219},[185,437,328],{"class":195},[185,439,220],{"class":219},[185,441,322],{"class":215},[185,443,325],{"class":219},[185,445,333],{"class":195},[185,447,448,450,453,455,457,459],{"class":187,"line":246},[185,449,339],{"class":215},[185,451,452],{"class":195}," value ",[185,454,345],{"class":215},[185,456,348],{"class":215},[185,458,325],{"class":219},[185,460,333],{"class":195},[185,462,463],{"class":187,"line":260},[185,464,465],{"class":195},"        typer.secho(\n",[185,467,468,470],{"class":187,"line":303},[185,469,364],{"class":199},[185,471,243],{"class":195},[185,473,474,476,478,481,484,486,488,490,492],{"class":187,"line":336},[185,475,372],{"class":211},[185,477,216],{"class":215},[185,479,480],{"class":195},"typer.colors.",[185,482,483],{"class":219},"YELLOW",[185,485,203],{"class":195},[185,487,382],{"class":211},[185,489,216],{"class":215},[185,491,240],{"class":219},[185,493,243],{"class":195},[185,495,496],{"class":187,"line":355},[185,497,394],{"class":195},[185,499,500,503],{"class":187,"line":361},[185,501,502],{"class":215},"    return",[185,504,505],{"class":195}," value\n",[185,507,508],{"class":187,"line":369},[185,509,511],{"emptyLinePlaceholder":510},true,"\n",[185,513,514,517],{"class":187,"line":391},[185,515,516],{"class":191},"@app.command",[185,518,519],{"class":195},"()\n",[185,521,522,524,526],{"class":187,"line":397},[185,523,306],{"class":215},[185,525,309],{"class":191},[185,527,528],{"class":195},"(\n",[185,530,531,534,536,539,542,544,547,550,552,555],{"class":187,"line":408},[185,532,533],{"class":195},"    retries: Annotated[",[185,535,220],{"class":219},[185,537,538],{"class":195},", typer.Option(",[185,540,541],{"class":211},"help",[185,543,216],{"class":215},[185,545,546],{"class":199},"\"Attempts per file.\"",[185,548,549],{"class":195},")] ",[185,551,216],{"class":215},[185,553,554],{"class":219}," 3",[185,556,243],{"class":195},[185,558,560,563,565,567,569,571,573,575,577,579,582,584,587,589,591],{"class":187,"line":559},12,[185,561,562],{"class":195},"    retry_count: Annotated[",[185,564,220],{"class":219},[185,566,322],{"class":215},[185,568,325],{"class":219},[185,570,538],{"class":195},[185,572,294],{"class":211},[185,574,216],{"class":215},[185,576,240],{"class":219},[185,578,203],{"class":195},[185,580,581],{"class":211},"callback",[185,583,216],{"class":215},[185,585,586],{"class":195},"deprecated_retry_count)] ",[185,588,216],{"class":215},[185,590,325],{"class":219},[185,592,243],{"class":195},[185,594,596,598,600],{"class":187,"line":595},13,[185,597,328],{"class":195},[185,599,289],{"class":219},[185,601,333],{"class":195},[185,603,605,608,610,612,615,617,619,621,623,626],{"class":187,"line":604},14,[185,606,607],{"class":195},"    retries ",[185,609,216],{"class":215},[185,611,342],{"class":195},[185,613,614],{"class":215},"if",[185,616,342],{"class":195},[185,618,345],{"class":215},[185,620,348],{"class":215},[185,622,325],{"class":219},[185,624,625],{"class":215}," else",[185,627,628],{"class":195}," retries\n",[185,630,632],{"class":187,"line":631},15,[185,633,411],{"class":219},[10,635,636,637,639,640,643,644,647],{},"Three properties matter in both versions. The warning goes to ",[31,638,44],{},", so it cannot contaminate\npiped output. It fires ",[31,641,642],{},"once",", not per use. And the new name ",[31,645,646],{},"wins"," if somebody passes both, so\na half-migrated script behaves predictably.",[10,649,650],{},"For more than one or two deprecations, a small helper keeps the pattern in one place:",[176,652,654],{"className":178,"code":653,"language":180,"meta":181,"style":181},"DEPRECATED = {\"--retry-count\": (\"--retries\", \"2.0\"), \"--dir\": (\"--source\", \"2.0\")}\n\ndef warn_deprecated(flag: str) -> None:\n    replacement, removal = DEPRECATED[flag]\n    typer.secho(\n        f\"{flag} is deprecated and will be removed in {removal}; use {replacement}\",\n        fg=typer.colors.YELLOW, err=True,\n    )\n",[14,655,656,697,701,720,733,738,779,800],{"__ignoreMap":181},[185,657,658,661,664,667,669,672,674,676,679,682,685,687,690,692,694],{"class":187,"line":188},[185,659,660],{"class":219},"DEPRECATED",[185,662,663],{"class":215}," =",[185,665,666],{"class":195}," {",[185,668,267],{"class":199},[185,670,671],{"class":195},": (",[185,673,200],{"class":199},[185,675,203],{"class":195},[185,677,678],{"class":199},"\"2.0\"",[185,680,681],{"class":195},"), ",[185,683,684],{"class":199},"\"--dir\"",[185,686,671],{"class":195},[185,688,689],{"class":199},"\"--source\"",[185,691,203],{"class":195},[185,693,678],{"class":199},[185,695,696],{"class":195},")}\n",[185,698,699],{"class":187,"line":246},[185,700,511],{"emptyLinePlaceholder":510},[185,702,703,705,708,711,714,716,718],{"class":187,"line":260},[185,704,306],{"class":215},[185,706,707],{"class":191}," warn_deprecated",[185,709,710],{"class":195},"(flag: ",[185,712,713],{"class":219},"str",[185,715,328],{"class":195},[185,717,289],{"class":219},[185,719,333],{"class":195},[185,721,722,725,727,730],{"class":187,"line":303},[185,723,724],{"class":195},"    replacement, removal ",[185,726,216],{"class":215},[185,728,729],{"class":219}," DEPRECATED",[185,731,732],{"class":195},"[flag]\n",[185,734,735],{"class":187,"line":336},[185,736,737],{"class":195},"    typer.secho(\n",[185,739,740,743,746,749,752,755,758,760,763,765,768,770,773,775,777],{"class":187,"line":355},[185,741,742],{"class":215},"        f",[185,744,745],{"class":199},"\"",[185,747,748],{"class":219},"{",[185,750,751],{"class":195},"flag",[185,753,754],{"class":219},"}",[185,756,757],{"class":199}," is deprecated and will be removed in ",[185,759,748],{"class":219},[185,761,762],{"class":195},"removal",[185,764,754],{"class":219},[185,766,767],{"class":199},"; use ",[185,769,748],{"class":219},[185,771,772],{"class":195},"replacement",[185,774,754],{"class":219},[185,776,745],{"class":199},[185,778,243],{"class":195},[185,780,781,784,786,788,790,792,794,796,798],{"class":187,"line":361},[185,782,783],{"class":211},"        fg",[185,785,216],{"class":215},[185,787,480],{"class":195},[185,789,483],{"class":219},[185,791,203],{"class":195},[185,793,382],{"class":211},[185,795,216],{"class":215},[185,797,240],{"class":219},[185,799,243],{"class":195},[185,801,802],{"class":187,"line":369},[185,803,804],{"class":195},"    )\n",[10,806,807],{},"That dictionary then feeds the warning, the changelog generation and the test suite, which is what\nkeeps the three consistent.",[19,809,811],{"id":810},"deprecating-a-whole-command","Deprecating a whole command",[10,813,814],{},"The same sequence works, with one addition: a deprecated command should say so in its own help text\nas well as at run time.",[176,816,818],{"className":178,"code":817,"language":180,"meta":181,"style":181},"@app.command(help=\"[DEPRECATED] Use 'mytool sync' instead. Removed in 2.0.\", hidden=True)\ndef upload(source: Path) -> None:\n    typer.secho(\"'upload' is deprecated; use 'sync' (removed in 2.0)\", fg=typer.colors.YELLOW, err=True)\n    sync(source)\n",[14,819,820,843,857,886],{"__ignoreMap":181},[185,821,822,824,826,828,830,833,835,837,839,841],{"class":187,"line":188},[185,823,516],{"class":191},[185,825,196],{"class":195},[185,827,541],{"class":211},[185,829,216],{"class":215},[185,831,832],{"class":199},"\"[DEPRECATED] Use 'mytool sync' instead. Removed in 2.0.\"",[185,834,203],{"class":195},[185,836,294],{"class":211},[185,838,216],{"class":215},[185,840,240],{"class":219},[185,842,257],{"class":195},[185,844,845,847,850,853,855],{"class":187,"line":246},[185,846,306],{"class":215},[185,848,849],{"class":191}," upload",[185,851,852],{"class":195},"(source: Path) -> ",[185,854,289],{"class":219},[185,856,333],{"class":195},[185,858,859,862,865,867,870,872,874,876,878,880,882,884],{"class":187,"line":260},[185,860,861],{"class":195},"    typer.secho(",[185,863,864],{"class":199},"\"'upload' is deprecated; use 'sync' (removed in 2.0)\"",[185,866,203],{"class":195},[185,868,869],{"class":211},"fg",[185,871,216],{"class":215},[185,873,480],{"class":195},[185,875,483],{"class":219},[185,877,203],{"class":195},[185,879,382],{"class":211},[185,881,216],{"class":215},[185,883,240],{"class":219},[185,885,257],{"class":195},[185,887,888],{"class":187,"line":303},[185,889,890],{"class":195},"    sync(source)\n",[10,892,893],{},"Delegating to the replacement rather than duplicating the implementation means the deprecated path\ncannot rot separately — and when the removal comes, deleting the function is the entire change.",[19,895,897],{"id":896},"ux-considerations","UX considerations",[10,899,900,903,904,907],{},[31,901,902],{},"Warn once per run, not per file."," A loop that warns on every iteration produces thousands of\nlines and trains people to redirect stderr to ",[14,905,906],{},"\u002Fdev\u002Fnull",", which is exactly the stream your errors\nuse.",[10,909,910,93,913,916],{},[31,911,912],{},"Give a suppression escape hatch.",[14,914,915],{},"MYTOOL_NO_DEPRECATION_WARNINGS=1"," is one line and it matters\nfor teams who have seen the warning, planned the migration, and need a clean CI log until then.",[10,918,919,922],{},[31,920,921],{},"Say the version."," \"Deprecated\" is ignorable; \"removed in 2.0\" is a deadline someone can put in a\nticket.",[10,924,925,928],{},[31,926,927],{},"Do not warn for something the user cannot fix."," If a deprecated flag is being passed by a tool\nyou also ship, fix that tool first — a warning nobody can act on is noise.",[19,930,932],{"id":931},"testing-the-behaviour","Testing the behaviour",[10,934,935],{},"Deprecation logic is easy to break by accident and cheap to pin:",[176,937,939],{"className":178,"code":938,"language":180,"meta":181,"style":181},"def test_old_flag_still_works_and_warns(cli):\n    result = cli.invoke(app, [\"sync\", \"data\", \"--retry-count\", \"7\"])\n\n    assert result.exit_code == 0\n    assert \"deprecated\" in result.stderr\n    assert \"--retries\" in result.stderr          # the message names the replacement\n    assert \"2.0\" in result.stderr                # and the removal version\n\ndef test_new_flag_wins_when_both_are_given(cli, monkeypatch):\n    seen = {}\n    monkeypatch.setattr(\"mytool.commands.sync.core.sync_directory\",\n                        lambda source, **kw: seen.update(kw) or SyncResult(0, 0))\n\n    cli.invoke(app, [\"sync\", \"data\", \"--retries\", \"3\", \"--retry-count\", \"9\"])\n\n    assert seen[\"retries\"] == 3\n\ndef test_deprecated_flag_is_hidden_from_help(cli):\n    assert \"--retry-count\" not in cli.invoke(app, [\"sync\", \"--help\"]).stdout\n",[14,940,941,951,981,985,999,1012,1028,1043,1047,1057,1067,1077,1107,1111,1142,1146,1164,1169,1179],{"__ignoreMap":181},[185,942,943,945,948],{"class":187,"line":188},[185,944,306],{"class":215},[185,946,947],{"class":191}," test_old_flag_still_works_and_warns",[185,949,950],{"class":195},"(cli):\n",[185,952,953,956,958,961,964,966,969,971,973,975,978],{"class":187,"line":246},[185,954,955],{"class":195},"    result ",[185,957,216],{"class":215},[185,959,960],{"class":195}," cli.invoke(app, [",[185,962,963],{"class":199},"\"sync\"",[185,965,203],{"class":195},[185,967,968],{"class":199},"\"data\"",[185,970,203],{"class":195},[185,972,267],{"class":199},[185,974,203],{"class":195},[185,976,977],{"class":199},"\"7\"",[185,979,980],{"class":195},"])\n",[185,982,983],{"class":187,"line":260},[185,984,511],{"emptyLinePlaceholder":510},[185,986,987,990,993,996],{"class":187,"line":303},[185,988,989],{"class":215},"    assert",[185,991,992],{"class":195}," result.exit_code ",[185,994,995],{"class":215},"==",[185,997,998],{"class":219}," 0\n",[185,1000,1001,1003,1006,1009],{"class":187,"line":336},[185,1002,989],{"class":215},[185,1004,1005],{"class":199}," \"deprecated\"",[185,1007,1008],{"class":215}," in",[185,1010,1011],{"class":195}," result.stderr\n",[185,1013,1014,1016,1019,1021,1024],{"class":187,"line":355},[185,1015,989],{"class":215},[185,1017,1018],{"class":199}," \"--retries\"",[185,1020,1008],{"class":215},[185,1022,1023],{"class":195}," result.stderr          ",[185,1025,1027],{"class":1026},"sJ8bj","# the message names the replacement\n",[185,1029,1030,1032,1035,1037,1040],{"class":187,"line":361},[185,1031,989],{"class":215},[185,1033,1034],{"class":199}," \"2.0\"",[185,1036,1008],{"class":215},[185,1038,1039],{"class":195}," result.stderr                ",[185,1041,1042],{"class":1026},"# and the removal version\n",[185,1044,1045],{"class":187,"line":369},[185,1046,511],{"emptyLinePlaceholder":510},[185,1048,1049,1051,1054],{"class":187,"line":391},[185,1050,306],{"class":215},[185,1052,1053],{"class":191}," test_new_flag_wins_when_both_are_given",[185,1055,1056],{"class":195},"(cli, monkeypatch):\n",[185,1058,1059,1062,1064],{"class":187,"line":397},[185,1060,1061],{"class":195},"    seen ",[185,1063,216],{"class":215},[185,1065,1066],{"class":195}," {}\n",[185,1068,1069,1072,1075],{"class":187,"line":408},[185,1070,1071],{"class":195},"    monkeypatch.setattr(",[185,1073,1074],{"class":199},"\"mytool.commands.sync.core.sync_directory\"",[185,1076,243],{"class":195},[185,1078,1079,1082,1085,1088,1091,1094,1097,1100,1102,1104],{"class":187,"line":559},[185,1080,1081],{"class":215},"                        lambda",[185,1083,1084],{"class":195}," source, ",[185,1086,1087],{"class":215},"**",[185,1089,1090],{"class":195},"kw: seen.update(kw) ",[185,1092,1093],{"class":215},"or",[185,1095,1096],{"class":195}," SyncResult(",[185,1098,1099],{"class":219},"0",[185,1101,203],{"class":195},[185,1103,1099],{"class":219},[185,1105,1106],{"class":195},"))\n",[185,1108,1109],{"class":187,"line":595},[185,1110,511],{"emptyLinePlaceholder":510},[185,1112,1113,1116,1118,1120,1122,1124,1126,1128,1131,1133,1135,1137,1140],{"class":187,"line":604},[185,1114,1115],{"class":195},"    cli.invoke(app, [",[185,1117,963],{"class":199},[185,1119,203],{"class":195},[185,1121,968],{"class":199},[185,1123,203],{"class":195},[185,1125,200],{"class":199},[185,1127,203],{"class":195},[185,1129,1130],{"class":199},"\"3\"",[185,1132,203],{"class":195},[185,1134,267],{"class":199},[185,1136,203],{"class":195},[185,1138,1139],{"class":199},"\"9\"",[185,1141,980],{"class":195},[185,1143,1144],{"class":187,"line":631},[185,1145,511],{"emptyLinePlaceholder":510},[185,1147,1149,1151,1154,1156,1159,1161],{"class":187,"line":1148},16,[185,1150,989],{"class":215},[185,1152,1153],{"class":195}," seen[",[185,1155,206],{"class":199},[185,1157,1158],{"class":195},"] ",[185,1160,995],{"class":215},[185,1162,1163],{"class":219}," 3\n",[185,1165,1167],{"class":187,"line":1166},17,[185,1168,511],{"emptyLinePlaceholder":510},[185,1170,1172,1174,1177],{"class":187,"line":1171},18,[185,1173,306],{"class":215},[185,1175,1176],{"class":191}," test_deprecated_flag_is_hidden_from_help",[185,1178,950],{"class":195},[185,1180,1182,1184,1187,1189,1191,1193,1195,1197,1200],{"class":187,"line":1181},19,[185,1183,989],{"class":215},[185,1185,1186],{"class":199}," \"--retry-count\"",[185,1188,348],{"class":215},[185,1190,1008],{"class":215},[185,1192,960],{"class":195},[185,1194,963],{"class":199},[185,1196,203],{"class":195},[185,1198,1199],{"class":199},"\"--help\"",[185,1201,1202],{"class":195},"]).stdout\n",[10,1204,1205],{},"The middle test is the one that catches real mistakes — precedence between an old and new flag is\neasy to get backwards, and the symptom is a script that silently keeps its old behaviour after\nmigrating.",[19,1207,1209],{"id":1208},"conclusion","Conclusion",[10,1211,1212],{},"Deprecation is a schedule, not a decision. Add the replacement, accept both, warn with the\nreplacement name and the removal version, hide the old name, then remove it at a major release.\nEvery step is a few lines; the spacing between them is what turns a breaking change into one nobody\nnotices until they have already migrated.",[19,1214,1216],{"id":1215},"frequently-asked-questions","Frequently asked questions",[1218,1219,1221],"h3",{"id":1220},"how-long-should-a-deprecation-period-be","How long should a deprecation period be?",[10,1223,1224],{},"At least two minor releases between the first warning and the removal, and longer for a tool used\nin automation, where nobody is watching output day to day. Time matters less than release count:\nsomeone pinned to 1.4 experiences your deprecation only when they upgrade.",[1218,1226,1228],{"id":1227},"should-a-deprecated-flag-warn-on-stdout-or-stderr","Should a deprecated flag warn on stdout or stderr?",[10,1230,1231],{},"Stderr, always. A warning on stdout corrupts piped output, which turns a courtesy into a breaking\nchange of its own. This is the same rule that governs every non-result message your tool produces.",[1218,1233,1235],{"id":1234},"what-if-i-need-to-change-a-default-rather-than-a-name","What if I need to change a default rather than a name?",[10,1237,1238],{},"Announce it a release ahead, warn when the user is relying on the old default (they passed nothing\nand the value is about to change), and make the change at a major version. If the difference is\ndangerous — a default that starts deleting things — consider a new flag instead and leave the\ndefault alone.",[1218,1240,1242],{"id":1241},"how-do-i-deprecate-a-positional-argument","How do I deprecate a positional argument?",[10,1244,1245,1246,1249,1250,1253],{},"The same way, with one difference: positional arguments cannot be aliased, so accept the old\narity and warn. For example, if ",[14,1247,1248],{},"mytool sync SOURCE DEST"," becomes ",[14,1251,1252],{},"mytool sync SOURCE --dest DEST",",\naccept the trailing positional as optional, warn when it is used, and remove it at the major\nversion.",[1218,1255,1257],{"id":1256},"do-i-need-a-deprecation-policy-in-the-documentation","Do I need a deprecation policy in the documentation?",[10,1259,1260],{},"A short one helps: \"flags are deprecated for at least two minor releases before removal; removals\nhappen only in major versions\". It sets an expectation, and it saves the argument about whether a\nparticular removal was too fast.",[1218,1262,1264],{"id":1263},"can-i-detect-who-is-still-using-a-deprecated-flag","Can I detect who is still using a deprecated flag?",[10,1266,1267],{},"Not remotely, and you should not try — a CLI phoning home about usage is a much bigger imposition\nthan the deprecation. What you can do is make the warning specific enough that whoever sees it can\nfind the script, which means naming the flag, the replacement and the removal version in one line.",[1218,1269,1271],{"id":1270},"what-belongs-in-the-changelog-for-a-deprecation","What belongs in the changelog for a deprecation?",[10,1273,1274,1275,1277,1278,1280],{},"Three entries across three releases: the release that adds the replacement (\"added ",[14,1276,96],{},",\nwhich replaces ",[14,1279,160],{},"\"), the release that starts warning, and the major release that\nremoves it. Each names both flags. A reader upgrading across several versions can then reconstruct\nwhat happened without reading the source.",[19,1282,1284],{"id":1283},"related","Related",[24,1286,1287,1294,1301,1307,1313],{},[27,1288,1289,1290],{},"Up: ",[72,1291,1293],{"href":1292},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002F","CLI help output and documentation",[27,1295,1296,1297],{},"Sideways: ",[72,1298,1300],{"href":1299},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli\u002F","Generating man pages and docs from a CLI",[27,1302,1296,1303],{},[72,1304,1306],{"href":1305},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read\u002F","Writing help text users actually read",[27,1308,1309,1310],{},"Related: ",[72,1311,1312],{"href":74},"Managing CLI versioning and changelogs",[27,1314,1309,1315],{},[72,1316,1318],{"href":1317},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits\u002F","Automating changelogs with Conventional Commits",[1320,1321,1322],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":181,"searchDepth":246,"depth":246,"links":1324},[1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1343],{"id":21,"depth":246,"text":22},{"id":66,"depth":246,"text":67},{"id":79,"depth":246,"text":80},{"id":119,"depth":246,"text":120},{"id":167,"depth":246,"text":168},{"id":810,"depth":246,"text":811},{"id":896,"depth":246,"text":897},{"id":931,"depth":246,"text":932},{"id":1208,"depth":246,"text":1209},{"id":1215,"depth":246,"text":1216,"children":1335},[1336,1337,1338,1339,1340,1341,1342],{"id":1220,"depth":260,"text":1221},{"id":1227,"depth":260,"text":1228},{"id":1234,"depth":260,"text":1235},{"id":1241,"depth":260,"text":1242},{"id":1256,"depth":260,"text":1257},{"id":1263,"depth":260,"text":1264},{"id":1270,"depth":260,"text":1271},{"id":1283,"depth":246,"text":1284},"2026-08-01","Rename or retire a CLI flag without breaking scripts - hidden aliases, stderr warnings, a removal timeline, and the changelog entries that go with it.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags",{"title":5,"description":1345},"advanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002Findex",[1354,1355,1356,1357,1358],"deprecation","versioning","flags","compatibility","ux","Y47XokblBYPbj1LbpP3D73mZgJMENc5WohX3I2BV8co",[1361,1364,1367,1370,1373,1376,1379,1382,1383,1386,1389,1392,1395,1398,1401,1404,1407,1410,1413,1416,1419,1422,1425,1428,1431,1434,1437,1440,1443,1446,1449,1452,1455,1458,1461,1464,1467,1470,1473,1476,1479,1482,1485,1488,1491,1494,1497,1500,1503,1506,1509,1512,1515,1518,1521,1524,1527,1530,1533,1536,1539,1542,1545,1548,1551,1554,1557,1560,1563,1566,1569,1572,1575,1578,1581,1584,1587,1590,1593,1596,1599,1602,1605],{"path":1362,"title":1363},"\u002Fabout","About Python CLI Toolcraft",{"path":1365,"title":1366},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1368,"title":1369},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1371,"title":1372},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1374,"title":1375},"\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":1377,"title":1378},"\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":1380,"title":1381},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1350,"title":5},{"path":1384,"title":1385},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1387,"title":1388},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1390,"title":1391},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1393,"title":1394},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1396,"title":1397},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1399,"title":1400},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1402,"title":1403},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1405,"title":1406},"\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":1408,"title":1409},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1411,"title":1412},"\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":1414,"title":1415},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1417,"title":1418},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1420,"title":1421},"\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":1423,"title":1424},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1426,"title":1427},"\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":1429,"title":1430},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1432,"title":1433},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1435,"title":1436},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1438,"title":1439},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1441,"title":1442},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1444,"title":1445},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1447,"title":1448},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1450,"title":1451},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1453,"title":1454},"\u002F","Python CLI Toolcraft",{"path":1456,"title":1457},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1459,"title":1460},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1462,"title":1463},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1465,"title":1466},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1468,"title":1469},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1471,"title":1472},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1474,"title":1475},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1477,"title":1478},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1480,"title":1481},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1483,"title":1484},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1486,"title":1487},"\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":1489,"title":1490},"\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":1492,"title":1493},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1495,"title":1496},"\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":1498,"title":1499},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1501,"title":1502},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1504,"title":1505},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1507,"title":1508},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1510,"title":1511},"\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":1513,"title":1514},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1516,"title":1517},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1519,"title":1520},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1522,"title":1523},"\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":1525,"title":1526},"\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":1528,"title":1529},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1531,"title":1532},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1534,"title":1535},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1537,"title":1538},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1540,"title":1541},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1543,"title":1544},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1546,"title":1547},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1549,"title":1550},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1552,"title":1553},"\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":1555,"title":1556},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1558,"title":1559},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1561,"title":1562},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1564,"title":1565},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1567,"title":1568},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1570,"title":1571},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1573,"title":1574},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1576,"title":1577},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1579,"title":1580},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1582,"title":1583},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1585,"title":1586},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1588,"title":1589},"\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":1591,"title":1592},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1594,"title":1595},"\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":1597,"title":1598},"\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":1600,"title":1601},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1603,"title":1604},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1606,"title":1607},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690027]