[{"data":1,"prerenderedAt":2410},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer\u002F":3,"content-directory":2165},{"id":4,"title":5,"body":6,"date":2150,"description":2151,"difficulty":2152,"draft":2153,"extension":2154,"meta":2155,"navigation":160,"path":2156,"seo":2157,"stem":2158,"tags":2159,"updated":2150,"__hash__":2164},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer\u002Findex.md","Migrating from argparse to Typer",{"type":7,"value":8,"toc":2128},"minimark",[9,26,31,119,123,126,563,567,574,938,985,1016,1020,1023,1027,1260,1263,1296,1301,1324,1369,1373,1382,1429,1433,1449,1565,1575,1579,1582,1632,1643,1647,1650,1653,1699,1702,1706,1713,1716,1966,1973,1977,2056,2060,2064,2067,2071,2074,2078,2081,2085,2088,2092,2095,2099,2124],[10,11,12,13,17,18,25],"p",{},"You have an ",[14,15,16],"code",{},"argparse"," CLI that works, but adding features means more boilerplate, nested\nsubcommands are painful, and you want shell completion for free. ",[19,20,24],"a",{"href":21,"rel":22},"https:\u002F\u002Ftyper.tiangolo.com",[23],"nofollow","Typer","\ngives you all of that by reading your function's type hints instead of making you declare\nevery argument. This guide moves a real CLI across, one construct at a time, so behaviour\nstays identical while the code shrinks.",[27,28,30],"h2",{"id":29},"tldr","TL;DR",[32,33,34,55,94,105,108],"ul",{},[35,36,37,38,41,42,45,46,50,51,54],"li",{},"A Typer app replaces ",[14,39,40],{},"ArgumentParser","; each ",[14,43,44],{},"@app.command()"," function's ",[47,48,49],"strong",{},"parameters","\nreplace ",[14,52,53],{},"add_argument()"," calls.",[35,56,57,60,61,64,65,68,69,64,74,77,78,83,84,87,88,93],{},[14,58,59],{},"type="," becomes a ",[47,62,63],{},"type annotation",", ",[14,66,67],{},"choices="," becomes an ",[47,70,71],{},[14,72,73],{},"Enum",[14,75,76],{},"action=\"store_true\"","\nbecomes a ",[47,79,80],{},[14,81,82],{},"bool",", and ",[14,85,86],{},"add_subparsers()"," becomes multiple ",[47,89,90,92],{},[14,91,44],{},"s",".",[35,95,96,97,101,102,93],{},"Positional parameters become Typer ",[98,99,100],"em",{},"arguments","; parameters with defaults become ",[98,103,104],{},"options",[35,106,107],{},"Migrate incrementally: Typer is built on Click, so you can wrap an existing argparse parser\nbehind a single Typer command and peel it apart command by command.",[35,109,110,111,114,115,118],{},"Lock behaviour with ",[14,112,113],{},"CliRunner"," tests written ",[98,116,117],{},"before"," you change anything.",[27,120,122],{"id":121},"the-starting-point-an-argparse-cli","The starting point: an argparse CLI",[10,124,125],{},"Here is a small but representative tool — it greets people, takes a count, a log level with\nfixed choices, and a verbose flag.",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-python shiki shiki-themes github-light github-dark","# greet_argparse.py\nimport argparse\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        prog=\"greet\", description=\"Greet someone a number of times.\",\n    )\n    parser.add_argument(\"name\", help=\"Who to greet.\")\n    parser.add_argument(\n        \"--count\", type=int, default=1, help=\"How many times (default: 1).\",\n    )\n    parser.add_argument(\n        \"--level\", choices=[\"quiet\", \"normal\", \"loud\"], default=\"normal\",\n        help=\"Greeting volume.\",\n    )\n    parser.add_argument(\"--verbose\", action=\"store_true\", help=\"Explain what is happening.\")\n\n    args = parser.parse_args()\n    if args.verbose:\n        print(f\"[verbose] level={args.level}\")\n    greeting = \"hi\" if args.level == \"quiet\" else \"HELLO\" if args.level == \"loud\" else \"Hello\"\n    for _ in range(args.count):\n        print(f\"{greeting}, {args.name}!\")\n\nif __name__ == \"__main__\":\n    main()\n","python","",[14,134,135,144,155,162,182,194,220,226,248,254,291,296,301,341,354,359,388,393,404,413,442,485,503,535,540,557],{"__ignoreMap":132},[136,137,140],"span",{"class":138,"line":139},"line",1,[136,141,143],{"class":142},"sJ8bj","# greet_argparse.py\n",[136,145,147,151],{"class":138,"line":146},2,[136,148,150],{"class":149},"szBVR","import",[136,152,154],{"class":153},"sVt8B"," argparse\n",[136,156,158],{"class":138,"line":157},3,[136,159,161],{"emptyLinePlaceholder":160},true,"\n",[136,163,165,168,172,175,179],{"class":138,"line":164},4,[136,166,167],{"class":149},"def",[136,169,171],{"class":170},"sScJk"," main",[136,173,174],{"class":153},"() -> ",[136,176,178],{"class":177},"sj4cs","None",[136,180,181],{"class":153},":\n",[136,183,185,188,191],{"class":138,"line":184},5,[136,186,187],{"class":153},"    parser ",[136,189,190],{"class":149},"=",[136,192,193],{"class":153}," argparse.ArgumentParser(\n",[136,195,197,201,203,207,209,212,214,217],{"class":138,"line":196},6,[136,198,200],{"class":199},"s4XuR","        prog",[136,202,190],{"class":149},[136,204,206],{"class":205},"sZZnC","\"greet\"",[136,208,64],{"class":153},[136,210,211],{"class":199},"description",[136,213,190],{"class":149},[136,215,216],{"class":205},"\"Greet someone a number of times.\"",[136,218,219],{"class":153},",\n",[136,221,223],{"class":138,"line":222},7,[136,224,225],{"class":153},"    )\n",[136,227,229,232,235,237,240,242,245],{"class":138,"line":228},8,[136,230,231],{"class":153},"    parser.add_argument(",[136,233,234],{"class":205},"\"name\"",[136,236,64],{"class":153},[136,238,239],{"class":199},"help",[136,241,190],{"class":149},[136,243,244],{"class":205},"\"Who to greet.\"",[136,246,247],{"class":153},")\n",[136,249,251],{"class":138,"line":250},9,[136,252,253],{"class":153},"    parser.add_argument(\n",[136,255,257,260,262,265,267,270,272,275,277,280,282,284,286,289],{"class":138,"line":256},10,[136,258,259],{"class":205},"        \"--count\"",[136,261,64],{"class":153},[136,263,264],{"class":199},"type",[136,266,190],{"class":149},[136,268,269],{"class":177},"int",[136,271,64],{"class":153},[136,273,274],{"class":199},"default",[136,276,190],{"class":149},[136,278,279],{"class":177},"1",[136,281,64],{"class":153},[136,283,239],{"class":199},[136,285,190],{"class":149},[136,287,288],{"class":205},"\"How many times (default: 1).\"",[136,290,219],{"class":153},[136,292,294],{"class":138,"line":293},11,[136,295,225],{"class":153},[136,297,299],{"class":138,"line":298},12,[136,300,253],{"class":153},[136,302,304,307,309,312,314,317,320,322,325,327,330,333,335,337,339],{"class":138,"line":303},13,[136,305,306],{"class":205},"        \"--level\"",[136,308,64],{"class":153},[136,310,311],{"class":199},"choices",[136,313,190],{"class":149},[136,315,316],{"class":153},"[",[136,318,319],{"class":205},"\"quiet\"",[136,321,64],{"class":153},[136,323,324],{"class":205},"\"normal\"",[136,326,64],{"class":153},[136,328,329],{"class":205},"\"loud\"",[136,331,332],{"class":153},"], ",[136,334,274],{"class":199},[136,336,190],{"class":149},[136,338,324],{"class":205},[136,340,219],{"class":153},[136,342,344,347,349,352],{"class":138,"line":343},14,[136,345,346],{"class":199},"        help",[136,348,190],{"class":149},[136,350,351],{"class":205},"\"Greeting volume.\"",[136,353,219],{"class":153},[136,355,357],{"class":138,"line":356},15,[136,358,225],{"class":153},[136,360,362,364,367,369,372,374,377,379,381,383,386],{"class":138,"line":361},16,[136,363,231],{"class":153},[136,365,366],{"class":205},"\"--verbose\"",[136,368,64],{"class":153},[136,370,371],{"class":199},"action",[136,373,190],{"class":149},[136,375,376],{"class":205},"\"store_true\"",[136,378,64],{"class":153},[136,380,239],{"class":199},[136,382,190],{"class":149},[136,384,385],{"class":205},"\"Explain what is happening.\"",[136,387,247],{"class":153},[136,389,391],{"class":138,"line":390},17,[136,392,161],{"emptyLinePlaceholder":160},[136,394,396,399,401],{"class":138,"line":395},18,[136,397,398],{"class":153},"    args ",[136,400,190],{"class":149},[136,402,403],{"class":153}," parser.parse_args()\n",[136,405,407,410],{"class":138,"line":406},19,[136,408,409],{"class":149},"    if",[136,411,412],{"class":153}," args.verbose:\n",[136,414,416,419,422,425,428,431,434,437,440],{"class":138,"line":415},20,[136,417,418],{"class":177},"        print",[136,420,421],{"class":153},"(",[136,423,424],{"class":149},"f",[136,426,427],{"class":205},"\"[verbose] level=",[136,429,430],{"class":177},"{",[136,432,433],{"class":153},"args.level",[136,435,436],{"class":177},"}",[136,438,439],{"class":205},"\"",[136,441,247],{"class":153},[136,443,445,448,450,453,456,459,462,465,468,471,473,475,477,480,482],{"class":138,"line":444},21,[136,446,447],{"class":153},"    greeting ",[136,449,190],{"class":149},[136,451,452],{"class":205}," \"hi\"",[136,454,455],{"class":149}," if",[136,457,458],{"class":153}," args.level ",[136,460,461],{"class":149},"==",[136,463,464],{"class":205}," \"quiet\"",[136,466,467],{"class":149}," else",[136,469,470],{"class":205}," \"HELLO\"",[136,472,455],{"class":149},[136,474,458],{"class":153},[136,476,461],{"class":149},[136,478,479],{"class":205}," \"loud\"",[136,481,467],{"class":149},[136,483,484],{"class":205}," \"Hello\"\n",[136,486,488,491,494,497,500],{"class":138,"line":487},22,[136,489,490],{"class":149},"    for",[136,492,493],{"class":153}," _ ",[136,495,496],{"class":149},"in",[136,498,499],{"class":177}," range",[136,501,502],{"class":153},"(args.count):\n",[136,504,506,508,510,512,514,516,519,521,523,525,528,530,533],{"class":138,"line":505},23,[136,507,418],{"class":177},[136,509,421],{"class":153},[136,511,424],{"class":149},[136,513,439],{"class":205},[136,515,430],{"class":177},[136,517,518],{"class":153},"greeting",[136,520,436],{"class":177},[136,522,64],{"class":205},[136,524,430],{"class":177},[136,526,527],{"class":153},"args.name",[136,529,436],{"class":177},[136,531,532],{"class":205},"!\"",[136,534,247],{"class":153},[136,536,538],{"class":138,"line":537},24,[136,539,161],{"emptyLinePlaceholder":160},[136,541,543,546,549,552,555],{"class":138,"line":542},25,[136,544,545],{"class":149},"if",[136,547,548],{"class":177}," __name__",[136,550,551],{"class":149}," ==",[136,553,554],{"class":205}," \"__main__\"",[136,556,181],{"class":153},[136,558,560],{"class":138,"line":559},26,[136,561,562],{"class":153},"    main()\n",[27,564,566],{"id":565},"the-destination-the-same-cli-in-typer","The destination: the same CLI in Typer",[10,568,569,570,573],{},"The equivalent Typer program produces the same ",[14,571,572],{},"--help",", the same defaults, and the same\noutput, with the parser gone:",[127,575,577],{"className":129,"code":576,"language":131,"meta":132,"style":132},"# greet_typer.py\nfrom enum import Enum\nfrom typing import Annotated\nimport typer\n\nclass Level(str, Enum):\n    quiet = \"quiet\"\n    normal = \"normal\"\n    loud = \"loud\"\n\napp = typer.Typer(help=\"Greet someone a number of times.\")\n\n@app.command()\ndef greet(\n    name: Annotated[str, typer.Argument(help=\"Who to greet.\")],\n    count: Annotated[int, typer.Option(help=\"How many times.\")] = 1,\n    level: Annotated[Level, typer.Option(help=\"Greeting volume.\")] = Level.normal,\n    verbose: Annotated[bool, typer.Option(help=\"Explain what is happening.\")] = False,\n) -> None:\n    if verbose:\n        typer.echo(f\"[verbose] level={level.value}\")\n    greeting = {\"quiet\": \"hi\", \"normal\": \"Hello\", \"loud\": \"HELLO\"}[level.value]\n    for _ in range(count):\n        typer.echo(f\"{greeting}, {name}!\")\n\nif __name__ == \"__main__\":\n    app()\n",[14,578,579,584,597,609,616,620,640,650,660,670,674,692,696,704,714,733,760,778,802,811,818,838,876,889,916,920,932],{"__ignoreMap":132},[136,580,581],{"class":138,"line":139},[136,582,583],{"class":142},"# greet_typer.py\n",[136,585,586,589,592,594],{"class":138,"line":146},[136,587,588],{"class":149},"from",[136,590,591],{"class":153}," enum ",[136,593,150],{"class":149},[136,595,596],{"class":153}," Enum\n",[136,598,599,601,604,606],{"class":138,"line":157},[136,600,588],{"class":149},[136,602,603],{"class":153}," typing ",[136,605,150],{"class":149},[136,607,608],{"class":153}," Annotated\n",[136,610,611,613],{"class":138,"line":164},[136,612,150],{"class":149},[136,614,615],{"class":153}," typer\n",[136,617,618],{"class":138,"line":184},[136,619,161],{"emptyLinePlaceholder":160},[136,621,622,625,628,630,633,635,637],{"class":138,"line":196},[136,623,624],{"class":149},"class",[136,626,627],{"class":170}," Level",[136,629,421],{"class":153},[136,631,632],{"class":177},"str",[136,634,64],{"class":153},[136,636,73],{"class":170},[136,638,639],{"class":153},"):\n",[136,641,642,645,647],{"class":138,"line":222},[136,643,644],{"class":153},"    quiet ",[136,646,190],{"class":149},[136,648,649],{"class":205}," \"quiet\"\n",[136,651,652,655,657],{"class":138,"line":228},[136,653,654],{"class":153},"    normal ",[136,656,190],{"class":149},[136,658,659],{"class":205}," \"normal\"\n",[136,661,662,665,667],{"class":138,"line":250},[136,663,664],{"class":153},"    loud ",[136,666,190],{"class":149},[136,668,669],{"class":205}," \"loud\"\n",[136,671,672],{"class":138,"line":256},[136,673,161],{"emptyLinePlaceholder":160},[136,675,676,679,681,684,686,688,690],{"class":138,"line":293},[136,677,678],{"class":153},"app ",[136,680,190],{"class":149},[136,682,683],{"class":153}," typer.Typer(",[136,685,239],{"class":199},[136,687,190],{"class":149},[136,689,216],{"class":205},[136,691,247],{"class":153},[136,693,694],{"class":138,"line":298},[136,695,161],{"emptyLinePlaceholder":160},[136,697,698,701],{"class":138,"line":303},[136,699,700],{"class":170},"@app.command",[136,702,703],{"class":153},"()\n",[136,705,706,708,711],{"class":138,"line":343},[136,707,167],{"class":149},[136,709,710],{"class":170}," greet",[136,712,713],{"class":153},"(\n",[136,715,716,719,721,724,726,728,730],{"class":138,"line":356},[136,717,718],{"class":153},"    name: Annotated[",[136,720,632],{"class":177},[136,722,723],{"class":153},", typer.Argument(",[136,725,239],{"class":199},[136,727,190],{"class":149},[136,729,244],{"class":205},[136,731,732],{"class":153},")],\n",[136,734,735,738,740,743,745,747,750,753,755,758],{"class":138,"line":361},[136,736,737],{"class":153},"    count: Annotated[",[136,739,269],{"class":177},[136,741,742],{"class":153},", typer.Option(",[136,744,239],{"class":199},[136,746,190],{"class":149},[136,748,749],{"class":205},"\"How many times.\"",[136,751,752],{"class":153},")] ",[136,754,190],{"class":149},[136,756,757],{"class":177}," 1",[136,759,219],{"class":153},[136,761,762,765,767,769,771,773,775],{"class":138,"line":390},[136,763,764],{"class":153},"    level: Annotated[Level, typer.Option(",[136,766,239],{"class":199},[136,768,190],{"class":149},[136,770,351],{"class":205},[136,772,752],{"class":153},[136,774,190],{"class":149},[136,776,777],{"class":153}," Level.normal,\n",[136,779,780,783,785,787,789,791,793,795,797,800],{"class":138,"line":395},[136,781,782],{"class":153},"    verbose: Annotated[",[136,784,82],{"class":177},[136,786,742],{"class":153},[136,788,239],{"class":199},[136,790,190],{"class":149},[136,792,385],{"class":205},[136,794,752],{"class":153},[136,796,190],{"class":149},[136,798,799],{"class":177}," False",[136,801,219],{"class":153},[136,803,804,807,809],{"class":138,"line":406},[136,805,806],{"class":153},") -> ",[136,808,178],{"class":177},[136,810,181],{"class":153},[136,812,813,815],{"class":138,"line":415},[136,814,409],{"class":149},[136,816,817],{"class":153}," verbose:\n",[136,819,820,823,825,827,829,832,834,836],{"class":138,"line":444},[136,821,822],{"class":153},"        typer.echo(",[136,824,424],{"class":149},[136,826,427],{"class":205},[136,828,430],{"class":177},[136,830,831],{"class":153},"level.value",[136,833,436],{"class":177},[136,835,439],{"class":205},[136,837,247],{"class":153},[136,839,840,842,844,847,849,852,855,857,859,861,864,866,868,870,873],{"class":138,"line":487},[136,841,447],{"class":153},[136,843,190],{"class":149},[136,845,846],{"class":153}," {",[136,848,319],{"class":205},[136,850,851],{"class":153},": ",[136,853,854],{"class":205},"\"hi\"",[136,856,64],{"class":153},[136,858,324],{"class":205},[136,860,851],{"class":153},[136,862,863],{"class":205},"\"Hello\"",[136,865,64],{"class":153},[136,867,329],{"class":205},[136,869,851],{"class":153},[136,871,872],{"class":205},"\"HELLO\"",[136,874,875],{"class":153},"}[level.value]\n",[136,877,878,880,882,884,886],{"class":138,"line":505},[136,879,490],{"class":149},[136,881,493],{"class":153},[136,883,496],{"class":149},[136,885,499],{"class":177},[136,887,888],{"class":153},"(count):\n",[136,890,891,893,895,897,899,901,903,905,907,910,912,914],{"class":138,"line":537},[136,892,822],{"class":153},[136,894,424],{"class":149},[136,896,439],{"class":205},[136,898,430],{"class":177},[136,900,518],{"class":153},[136,902,436],{"class":177},[136,904,64],{"class":205},[136,906,430],{"class":177},[136,908,909],{"class":153},"name",[136,911,436],{"class":177},[136,913,532],{"class":205},[136,915,247],{"class":153},[136,917,918],{"class":138,"line":542},[136,919,161],{"emptyLinePlaceholder":160},[136,921,922,924,926,928,930],{"class":138,"line":559},[136,923,545],{"class":149},[136,925,548],{"class":177},[136,927,551],{"class":149},[136,929,554],{"class":205},[136,931,181],{"class":153},[136,933,935],{"class":138,"line":934},27,[136,936,937],{"class":153},"    app()\n",[127,939,943],{"className":940,"code":941,"language":942,"meta":132,"style":132},"language-bash shiki shiki-themes github-light github-dark","$ python greet_typer.py World --count 2 --level loud\nHELLO, World!\nHELLO, World!\n","bash",[14,944,945,971,979],{"__ignoreMap":132},[136,946,947,950,953,956,959,962,965,968],{"class":138,"line":139},[136,948,949],{"class":170},"$",[136,951,952],{"class":205}," python",[136,954,955],{"class":205}," greet_typer.py",[136,957,958],{"class":205}," World",[136,960,961],{"class":177}," --count",[136,963,964],{"class":177}," 2",[136,966,967],{"class":177}," --level",[136,969,970],{"class":205}," loud\n",[136,972,973,976],{"class":138,"line":146},[136,974,975],{"class":170},"HELLO,",[136,977,978],{"class":205}," World!\n",[136,980,981,983],{"class":138,"line":157},[136,982,975],{"class":170},[136,984,978],{"class":205},[10,986,987,988,991,992,994,995,991,997,1000,1001,1003,1004,1006,1007,1009,1010,1012,1013,1015],{},"The annotation ",[98,989,990],{},"is"," the ",[14,993,59],{},". The default value ",[98,996,990],{},[14,998,999],{},"default=",". The ",[14,1002,73],{}," ",[98,1005,990],{}," the\n",[14,1008,67],{},". Typer even validates the enum and lists the options in ",[14,1011,572],{},", exactly like\nargparse's ",[14,1014,311],{},". Roughly a third of the lines are gone, and everything left describes\nintent rather than plumbing.",[27,1017,1019],{"id":1018},"the-mapping-construct-by-construct","The mapping, construct by construct",[10,1021,1022],{},"Keep this table next to you while you translate. Almost every argparse pattern has a direct\nTyper equivalent.",[1024,1025],"inline-diagram",{"name":1026},"argparse-typer-construct-map",[1028,1029,1030,1041],"table",{},[1031,1032,1033],"thead",{},[1034,1035,1036,1039],"tr",{},[1037,1038,16],"th",{},[1037,1040,24],{},[1042,1043,1044,1057,1075,1092,1111,1124,1140,1154,1167,1179,1197,1209,1223,1235,1247],"tbody",{},[1034,1045,1046,1052],{},[1047,1048,1049],"td",{},[14,1050,1051],{},"ArgumentParser(description=...)",[1047,1053,1054],{},[14,1055,1056],{},"typer.Typer(help=...)",[1034,1058,1059,1065],{},[1047,1060,1061,1064],{},[14,1062,1063],{},"add_argument(\"name\")"," (positional)",[1047,1066,1067,1068,1071,1072],{},"function parameter with ",[47,1069,1070],{},"no default"," → ",[14,1073,1074],{},"typer.Argument",[1034,1076,1077,1083],{},[1047,1078,1079,1082],{},[14,1080,1081],{},"add_argument(\"--opt\")"," (optional)",[1047,1084,1085,1086,1071,1089],{},"function parameter ",[47,1087,1088],{},"with a default",[14,1090,1091],{},"typer.Option",[1034,1093,1094,1103],{},[1047,1095,1096,1099,1100],{},[14,1097,1098],{},"type=int"," \u002F ",[14,1101,1102],{},"type=Path",[1047,1104,1105,1106,1099,1108],{},"annotation ",[14,1107,269],{},[14,1109,1110],{},"Path",[1034,1112,1113,1118],{},[1047,1114,1115],{},[14,1116,1117],{},"default=1",[1047,1119,1120,1123],{},[14,1121,1122],{},"= 1"," on the parameter",[1034,1125,1126,1131],{},[1047,1127,1128],{},[14,1129,1130],{},"choices=[...]",[1047,1132,1133,1134,1136,1137,1139],{},"a ",[14,1135,632],{},"-",[14,1138,73],{}," type",[1034,1141,1142,1146],{},[1047,1143,1144],{},[14,1145,76],{},[1047,1147,1148,1150,1151],{},[14,1149,82],{}," parameter defaulting to ",[14,1152,1153],{},"False",[1034,1155,1156,1161],{},[1047,1157,1158],{},[14,1159,1160],{},"nargs=\"+\"",[1047,1162,1163,1166],{},[14,1164,1165],{},"list[str]"," parameter",[1034,1168,1169,1174],{},[1047,1170,1171],{},[14,1172,1173],{},"action=\"append\"",[1047,1175,1176,1178],{},[14,1177,1165],{}," option (repeat the flag)",[1034,1180,1181,1186],{},[1047,1182,1183],{},[14,1184,1185],{},"help=\"...\"",[1047,1187,1188,1191,1192,1194,1195],{},[14,1189,1190],{},"help="," on ",[14,1193,1074],{},"\u002F",[14,1196,1091],{},[1034,1198,1199,1204],{},[1047,1200,1201],{},[14,1202,1203],{},"metavar=\"X\"",[1047,1205,1206,1208],{},[14,1207,1203],{}," on the annotation",[1034,1210,1211,1216],{},[1047,1212,1213,1082],{},[14,1214,1215],{},"required=True",[1047,1217,1218,1219,1222],{},"option with no default, or ",[14,1220,1221],{},"..."," as the default",[1034,1224,1225,1229],{},[1047,1226,1227],{},[14,1228,86],{},[1047,1230,1231,1232,1234],{},"one ",[14,1233,44],{}," per subcommand",[1034,1236,1237,1242],{},[1047,1238,1239],{},[14,1240,1241],{},"parser.error(\"msg\")",[1047,1243,1244],{},[14,1245,1246],{},"raise typer.BadParameter(\"msg\")",[1034,1248,1249,1255],{},[1047,1250,1251,1254],{},[14,1252,1253],{},"parse_args()"," + dispatch",[1047,1256,1257],{},[14,1258,1259],{},"app()",[10,1261,1262],{},"Two rules resolve most confusion:",[1264,1265,1266,1282],"ol",{},[35,1267,1268,1271,1272,1275,1276,1279,1280,93],{},[47,1269,1270],{},"Positional vs option is decided by the default."," A parameter with no default becomes a\nrequired positional argument; a parameter with a default becomes an option. Force the\nissue with ",[14,1273,1274],{},"typer.Argument(...)"," or ",[14,1277,1278],{},"typer.Option(...)"," when you want to override the\nname, help, or make an option required with ",[14,1281,1221],{},[35,1283,1284,1287,1288,1291,1292,1295],{},[47,1285,1286],{},"Hyphenation is automatic."," A parameter ",[14,1289,1290],{},"dest_dir"," becomes the ",[14,1293,1294],{},"--dest-dir"," option, the\nsame normalization argparse did in reverse.",[1297,1298,1300],"h3",{"id":1299},"choices-enum","choices → Enum",[10,1302,1303,1304,1306,1307,1309,1310,1312,1313,1315,1316,1319,1320,1323],{},"argparse ",[14,1305,311],{}," become a ",[14,1308,632],{}," subclass of ",[14,1311,73],{},". Subclassing ",[14,1314,632],{}," matters — it lets\nyou compare ",[14,1317,1318],{},"level == \"loud\""," and serialize cleanly, and Typer uses the ",[98,1321,1322],{},"values"," in help and\ncompletion.",[127,1325,1327],{"className":129,"code":1326,"language":131,"meta":132,"style":132},"class Level(str, Enum):\n    quiet = \"quiet\"\n    normal = \"normal\"\n    loud = \"loud\"\n",[14,1328,1329,1345,1353,1361],{"__ignoreMap":132},[136,1330,1331,1333,1335,1337,1339,1341,1343],{"class":138,"line":139},[136,1332,624],{"class":149},[136,1334,627],{"class":170},[136,1336,421],{"class":153},[136,1338,632],{"class":177},[136,1340,64],{"class":153},[136,1342,73],{"class":170},[136,1344,639],{"class":153},[136,1346,1347,1349,1351],{"class":138,"line":146},[136,1348,644],{"class":153},[136,1350,190],{"class":149},[136,1352,649],{"class":205},[136,1354,1355,1357,1359],{"class":138,"line":157},[136,1356,654],{"class":153},[136,1358,190],{"class":149},[136,1360,659],{"class":205},[136,1362,1363,1365,1367],{"class":138,"line":164},[136,1364,664],{"class":153},[136,1366,190],{"class":149},[136,1368,669],{"class":205},[1297,1370,1372],{"id":1371},"nargs-list","nargs → list",[10,1374,1375,1376,60,1378,1381],{},"A positional ",[14,1377,1160],{},[14,1379,1380],{},"list"," parameter. Typer collects the trailing values into\nthe list just as argparse did:",[127,1383,1385],{"className":129,"code":1384,"language":131,"meta":132,"style":132},"@app.command()\ndef process(paths: list[str]) -> None:\n    for path in paths:\n        typer.echo(path)\n",[14,1386,1387,1393,1412,1424],{"__ignoreMap":132},[136,1388,1389,1391],{"class":138,"line":139},[136,1390,700],{"class":170},[136,1392,703],{"class":153},[136,1394,1395,1397,1400,1403,1405,1408,1410],{"class":138,"line":146},[136,1396,167],{"class":149},[136,1398,1399],{"class":170}," process",[136,1401,1402],{"class":153},"(paths: list[",[136,1404,632],{"class":177},[136,1406,1407],{"class":153},"]) -> ",[136,1409,178],{"class":177},[136,1411,181],{"class":153},[136,1413,1414,1416,1419,1421],{"class":138,"line":157},[136,1415,490],{"class":149},[136,1417,1418],{"class":153}," path ",[136,1420,496],{"class":149},[136,1422,1423],{"class":153}," paths:\n",[136,1425,1426],{"class":138,"line":164},[136,1427,1428],{"class":153},"        typer.echo(path)\n",[1297,1430,1432],{"id":1431},"subparsers-commands","subparsers → commands",[10,1434,1435,1436,1439,1440,1443,1444,1448],{},"This is where Typer wins hardest. Every ",[14,1437,1438],{},"sub.add_parser(\"build\")"," becomes a decorated\nfunction; the dispatch you wrote by hand with ",[14,1441,1442],{},"set_defaults(func=...)"," disappears because\nTyper routes to the function whose name matches the subcommand. If you are moving a\nsubcommand-heavy CLI, read\n",[19,1445,1447],{"href":1446},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands\u002F","argparse subparsers for subcommands","\nfirst to see exactly what you are replacing.",[127,1450,1452],{"className":129,"code":1451,"language":131,"meta":132,"style":132},"app = typer.Typer()\n\n@app.command()\ndef build(release: bool = False) -> None:\n    typer.echo(f\"build release={release}\")\n\n@app.command()\ndef deploy(target: str) -> None:\n    typer.echo(f\"deploy to {target}\")\n",[14,1453,1454,1463,1467,1473,1496,1517,1521,1527,1545],{"__ignoreMap":132},[136,1455,1456,1458,1460],{"class":138,"line":139},[136,1457,678],{"class":153},[136,1459,190],{"class":149},[136,1461,1462],{"class":153}," typer.Typer()\n",[136,1464,1465],{"class":138,"line":146},[136,1466,161],{"emptyLinePlaceholder":160},[136,1468,1469,1471],{"class":138,"line":157},[136,1470,700],{"class":170},[136,1472,703],{"class":153},[136,1474,1475,1477,1480,1483,1485,1488,1490,1492,1494],{"class":138,"line":164},[136,1476,167],{"class":149},[136,1478,1479],{"class":170}," build",[136,1481,1482],{"class":153},"(release: ",[136,1484,82],{"class":177},[136,1486,1487],{"class":149}," =",[136,1489,799],{"class":177},[136,1491,806],{"class":153},[136,1493,178],{"class":177},[136,1495,181],{"class":153},[136,1497,1498,1501,1503,1506,1508,1511,1513,1515],{"class":138,"line":184},[136,1499,1500],{"class":153},"    typer.echo(",[136,1502,424],{"class":149},[136,1504,1505],{"class":205},"\"build release=",[136,1507,430],{"class":177},[136,1509,1510],{"class":153},"release",[136,1512,436],{"class":177},[136,1514,439],{"class":205},[136,1516,247],{"class":153},[136,1518,1519],{"class":138,"line":196},[136,1520,161],{"emptyLinePlaceholder":160},[136,1522,1523,1525],{"class":138,"line":222},[136,1524,700],{"class":170},[136,1526,703],{"class":153},[136,1528,1529,1531,1534,1537,1539,1541,1543],{"class":138,"line":228},[136,1530,167],{"class":149},[136,1532,1533],{"class":170}," deploy",[136,1535,1536],{"class":153},"(target: ",[136,1538,632],{"class":177},[136,1540,806],{"class":153},[136,1542,178],{"class":177},[136,1544,181],{"class":153},[136,1546,1547,1549,1551,1554,1556,1559,1561,1563],{"class":138,"line":250},[136,1548,1500],{"class":153},[136,1550,424],{"class":149},[136,1552,1553],{"class":205},"\"deploy to ",[136,1555,430],{"class":177},[136,1557,1558],{"class":153},"target",[136,1560,436],{"class":177},[136,1562,439],{"class":205},[136,1564,247],{"class":153},[10,1566,1567,1570,1571,1574],{},[14,1568,1569],{},"app build --release"," and ",[14,1572,1573],{},"app deploy prod"," now work with no dispatch code at all.",[27,1576,1578],{"id":1577},"preserving-help-text-and-defaults","Preserving help text and defaults",[10,1580,1581],{},"The most common regression in a migration is silently changed help or defaults. Guard\nagainst it:",[32,1583,1584,1594,1617,1625],{},[35,1585,1586,1587,1589,1590,1194,1592,93],{},"Copy every ",[14,1588,1190],{}," string verbatim into the matching ",[14,1591,1074],{},[14,1593,1091],{},[35,1595,1596,1597,1600,1601,1604,1605,1607,1608,1610,1611,1613,1614,93],{},"Keep the same default ",[98,1598,1599],{},"value and type",". ",[14,1602,1603],{},"--count"," defaulting to ",[14,1606,279],{}," (an ",[14,1609,269],{},") must stay\n",[14,1612,1122],{},", not ",[14,1615,1616],{},"= \"1\"",[35,1618,1619,1620,1622,1623,93],{},"Set the program help from ",[14,1621,1056],{}," (or the callback docstring) to replace\n",[14,1624,1051],{},[35,1626,1627,1628,93],{},"If you relied on argparse's exit code 2 for usage errors, note that Typer also exits\nnon-zero on bad input; align on a scheme deliberately using\n",[19,1629,1631],{"href":1630},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","choosing exit codes for CLI tools",[10,1633,1634,1635,1638,1639,93],{},"For cross-cutting behaviour that used to live in the parser body — a ",[14,1636,1637],{},"--version"," flag,\nglobal setup that ran before every subcommand — use a Typer callback. That mechanism is\ncovered end to end in\n",[19,1640,1642],{"href":1641},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained\u002F","Typer callback functions explained",[27,1644,1646],{"id":1645},"an-incremental-migration-strategy","An incremental migration strategy",[10,1648,1649],{},"You do not have to rewrite everything in one commit. Because Typer sits on Click, you can\nadopt it at the edges and work inward:",[1024,1651],{"name":1652},"argparse-typer-migration-timeline",[1264,1654,1655,1661,1673,1679,1689],{},[35,1656,1657,1660],{},[47,1658,1659],{},"Freeze behaviour with tests first"," (next section). Do not touch the argparse code until\nthe tests pass against it.",[35,1662,1663,1666,1667,1670,1671,93],{},[47,1664,1665],{},"Wrap, then split."," Add Typer as a dependency and expose the new app as the entry point,\nwith your existing ",[14,1668,1669],{},"main()"," behind a single passthrough command. Ship that, then peel one\nsubcommand at a time into a real ",[14,1672,44],{},[35,1674,1675,1678],{},[47,1676,1677],{},"Move shared setup into a callback"," once more than one command needs it.",[35,1680,1681,1684,1685,1688],{},[47,1682,1683],{},"Delete the parser"," only when the last ",[14,1686,1687],{},"add_argument"," is gone.",[35,1690,1691,1694,1695,1698],{},[47,1692,1693],{},"Add completion last."," Typer gives you ",[14,1696,1697],{},"--install-completion"," for free; wire it up once\nthe command tree is stable.",[10,1700,1701],{},"At every step the CLI stays shippable, which matters when other people depend on it.",[27,1703,1705],{"id":1704},"testing-parity","Testing parity",[10,1707,1708,1709,1712],{},"The safety net for the whole migration is a test suite that pins observable behaviour, run\nagainst ",[98,1710,1711],{},"both"," implementations. Typer reuses Click's runner, so the tests barely change:",[1024,1714],{"name":1715},"argparse-typer-parity-test",[127,1717,1719],{"className":129,"code":1718,"language":131,"meta":132,"style":132},"# test_greet.py\nfrom typer.testing import CliRunner\nfrom greet_typer import app\n\nrunner = CliRunner()\n\ndef test_default_greeting() -> None:\n    result = runner.invoke(app, [\"World\"])\n    assert result.exit_code == 0\n    assert result.output == \"Hello, World!\\n\"\n\ndef test_count_and_level() -> None:\n    result = runner.invoke(app, [\"World\", \"--count\", \"2\", \"--level\", \"loud\"])\n    assert result.exit_code == 0\n    assert result.output.count(\"HELLO, World!\") == 2\n\ndef test_invalid_choice_rejected() -> None:\n    result = runner.invoke(app, [\"World\", \"--level\", \"screaming\"])\n    assert result.exit_code != 0\n    assert \"screaming\" in result.output\n",[14,1720,1721,1726,1738,1750,1754,1764,1768,1781,1797,1810,1828,1832,1845,1876,1886,1904,1908,1921,1942,1953],{"__ignoreMap":132},[136,1722,1723],{"class":138,"line":139},[136,1724,1725],{"class":142},"# test_greet.py\n",[136,1727,1728,1730,1733,1735],{"class":138,"line":146},[136,1729,588],{"class":149},[136,1731,1732],{"class":153}," typer.testing ",[136,1734,150],{"class":149},[136,1736,1737],{"class":153}," CliRunner\n",[136,1739,1740,1742,1745,1747],{"class":138,"line":157},[136,1741,588],{"class":149},[136,1743,1744],{"class":153}," greet_typer ",[136,1746,150],{"class":149},[136,1748,1749],{"class":153}," app\n",[136,1751,1752],{"class":138,"line":164},[136,1753,161],{"emptyLinePlaceholder":160},[136,1755,1756,1759,1761],{"class":138,"line":184},[136,1757,1758],{"class":153},"runner ",[136,1760,190],{"class":149},[136,1762,1763],{"class":153}," CliRunner()\n",[136,1765,1766],{"class":138,"line":196},[136,1767,161],{"emptyLinePlaceholder":160},[136,1769,1770,1772,1775,1777,1779],{"class":138,"line":222},[136,1771,167],{"class":149},[136,1773,1774],{"class":170}," test_default_greeting",[136,1776,174],{"class":153},[136,1778,178],{"class":177},[136,1780,181],{"class":153},[136,1782,1783,1786,1788,1791,1794],{"class":138,"line":228},[136,1784,1785],{"class":153},"    result ",[136,1787,190],{"class":149},[136,1789,1790],{"class":153}," runner.invoke(app, [",[136,1792,1793],{"class":205},"\"World\"",[136,1795,1796],{"class":153},"])\n",[136,1798,1799,1802,1805,1807],{"class":138,"line":250},[136,1800,1801],{"class":149},"    assert",[136,1803,1804],{"class":153}," result.exit_code ",[136,1806,461],{"class":149},[136,1808,1809],{"class":177}," 0\n",[136,1811,1812,1814,1817,1819,1822,1825],{"class":138,"line":256},[136,1813,1801],{"class":149},[136,1815,1816],{"class":153}," result.output ",[136,1818,461],{"class":149},[136,1820,1821],{"class":205}," \"Hello, World!",[136,1823,1824],{"class":177},"\\n",[136,1826,1827],{"class":205},"\"\n",[136,1829,1830],{"class":138,"line":293},[136,1831,161],{"emptyLinePlaceholder":160},[136,1833,1834,1836,1839,1841,1843],{"class":138,"line":298},[136,1835,167],{"class":149},[136,1837,1838],{"class":170}," test_count_and_level",[136,1840,174],{"class":153},[136,1842,178],{"class":177},[136,1844,181],{"class":153},[136,1846,1847,1849,1851,1853,1855,1857,1860,1862,1865,1867,1870,1872,1874],{"class":138,"line":303},[136,1848,1785],{"class":153},[136,1850,190],{"class":149},[136,1852,1790],{"class":153},[136,1854,1793],{"class":205},[136,1856,64],{"class":153},[136,1858,1859],{"class":205},"\"--count\"",[136,1861,64],{"class":153},[136,1863,1864],{"class":205},"\"2\"",[136,1866,64],{"class":153},[136,1868,1869],{"class":205},"\"--level\"",[136,1871,64],{"class":153},[136,1873,329],{"class":205},[136,1875,1796],{"class":153},[136,1877,1878,1880,1882,1884],{"class":138,"line":343},[136,1879,1801],{"class":149},[136,1881,1804],{"class":153},[136,1883,461],{"class":149},[136,1885,1809],{"class":177},[136,1887,1888,1890,1893,1896,1899,1901],{"class":138,"line":356},[136,1889,1801],{"class":149},[136,1891,1892],{"class":153}," result.output.count(",[136,1894,1895],{"class":205},"\"HELLO, World!\"",[136,1897,1898],{"class":153},") ",[136,1900,461],{"class":149},[136,1902,1903],{"class":177}," 2\n",[136,1905,1906],{"class":138,"line":361},[136,1907,161],{"emptyLinePlaceholder":160},[136,1909,1910,1912,1915,1917,1919],{"class":138,"line":390},[136,1911,167],{"class":149},[136,1913,1914],{"class":170}," test_invalid_choice_rejected",[136,1916,174],{"class":153},[136,1918,178],{"class":177},[136,1920,181],{"class":153},[136,1922,1923,1925,1927,1929,1931,1933,1935,1937,1940],{"class":138,"line":395},[136,1924,1785],{"class":153},[136,1926,190],{"class":149},[136,1928,1790],{"class":153},[136,1930,1793],{"class":205},[136,1932,64],{"class":153},[136,1934,1869],{"class":205},[136,1936,64],{"class":153},[136,1938,1939],{"class":205},"\"screaming\"",[136,1941,1796],{"class":153},[136,1943,1944,1946,1948,1951],{"class":138,"line":406},[136,1945,1801],{"class":149},[136,1947,1804],{"class":153},[136,1949,1950],{"class":149},"!=",[136,1952,1809],{"class":177},[136,1954,1955,1957,1960,1963],{"class":138,"line":415},[136,1956,1801],{"class":149},[136,1958,1959],{"class":205}," \"screaming\"",[136,1961,1962],{"class":149}," in",[136,1964,1965],{"class":153}," result.output\n",[10,1967,1968,1969,1972],{},"Run the same assertions against the argparse version first (invoke it as a subprocess or\ncall ",[14,1970,1971],{},"parse_args"," directly), confirm they pass, then point the suite at the Typer app. Green\non both means the migration preserved behaviour — which is the entire goal.",[27,1974,1976],{"id":1975},"production-notes","Production notes",[32,1978,1979,2000,2020,2033,2047],{},[35,1980,1981,1987,1988,1991,1992,1995,1996,1999],{},[47,1982,1983,1986],{},[14,1984,1985],{},"Annotated"," is the modern style."," Older Typer code put ",[14,1989,1990],{},"typer.Option()"," in the default\nslot (",[14,1993,1994],{},"count: int = typer.Option(1)","). That still works, but ",[14,1997,1998],{},"Annotated[int, typer.Option()] = 1","\nkeeps the real default in the default position and plays nicely with type checkers and\nnon-Typer callers. Use it in new code.",[35,2001,2002,2005,2006,1136,2008,64,2010,2012,2013,2015,2016,2019],{},[47,2003,2004],{},"Enums serialize by value, not name."," With a ",[14,2007,632],{},[14,2009,73],{},[14,2011,831],{}," is ",[14,2014,329],{},".\nCompare and format against ",[14,2017,2018],{},".value"," to match your old string logic exactly.",[35,2021,2022,2027,2028,2030,2031,93],{},[47,2023,2024,2025,93],{},"Required options use ",[14,2026,1221],{}," To reproduce argparse's ",[14,2029,1215],{}," on an optional\nflag, give it no default or use ",[14,2032,1278],{},[35,2034,2035,2038,2039,2041,2042,2046],{},[47,2036,2037],{},"Keep parsing thin during the move."," If your argparse ",[14,2040,1669],{}," mixed parsing with logic,\nextract the logic into plain functions first — see\n",[19,2043,2045],{"href":2044},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","structuring multi-command Python CLIs",".\nA thin command body makes the framework swap almost mechanical.",[35,2048,2049,2052,2053,2055],{},[47,2050,2051],{},"Pin Typer ≥0.12"," for the ",[14,2054,1985],{},"-first API and current completion support.",[27,2057,2059],{"id":2058},"frequently-asked-questions","Frequently asked questions",[1297,2061,2063],{"id":2062},"can-i-run-argparse-and-typer-side-by-side-during-a-migration","Can I run argparse and Typer side by side during a migration?",[10,2065,2066],{},"Yes, and it is the safest way to do it. Keep the argparse entry point exactly as it is, add a Typer app next to it, and move one command at a time by having the old dispatcher delegate to the new app. Each step ships on its own and each step is revertible, which is not true of a weekend rewrite that lands as one commit.",[1297,2068,2070],{"id":2069},"what-does-not-translate-cleanly-from-argparse","What does not translate cleanly from argparse?",[10,2072,2073],{},"Three things. Custom actions have no direct equivalent — they usually become a parameter callback or a small custom type. Mutually exclusive groups are not a first-class Typer concept, so the rule moves into a validation step in the command body. And argparse's parse_known_args, used to forward unknown flags to another program, needs Click's ignore_unknown_options context setting instead.",[1297,2075,2077],{"id":2076},"how-do-i-prove-the-migration-did-not-change-behaviour","How do I prove the migration did not change behaviour?",[10,2079,2080],{},"Write a parity test before you touch anything: a table of documented invocations, each run through both the old entry point and the new app, asserting the same exit code and the same stdout. Once that table is green on the old CLI, it becomes the acceptance criterion for every migrated command, and the migration stops being a matter of opinion.",[1297,2082,2084],{"id":2083},"will-help-output-look-the-same-afterwards","Will help output look the same afterwards?",[10,2086,2087],{},"No, and you should not try to make it identical. Typer renders help through Rich, so the layout, colours and wrapping differ from argparse's. What matters is that the same options are documented with the same text, which is why parity tests should assert on option names and descriptions rather than on whole help screens.",[1297,2089,2091],{"id":2090},"is-typer-worth-the-dependency-for-a-small-tool","Is Typer worth the dependency for a small tool?",[10,2093,2094],{},"For a single-command script that reads two arguments, argparse is fine and adds nothing to install. The trade tips as soon as you have subcommands, want tab completion, or find yourself writing conversion and validation code by hand — that is exactly the work Typer removes, and it is the work that grows fastest as a tool gets more commands.",[27,2096,2098],{"id":2097},"related","Related",[32,2100,2101,2108,2115,2119],{},[35,2102,2103,2104],{},"Up: ",[19,2105,2107],{"href":2106},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002F","Command-Line Parsing with argparse",[35,2109,2110,2111],{},"Sideways: ",[19,2112,2114],{"href":2113},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click: when to use each",[35,2116,2110,2117],{},[19,2118,1642],{"href":1641},[35,2120,2121,2122],{},"Related: ",[19,2123,1447],{"href":1446},[2125,2126,2127],"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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":132,"searchDepth":146,"depth":146,"links":2129},[2130,2131,2132,2133,2138,2139,2140,2141,2142,2149],{"id":29,"depth":146,"text":30},{"id":121,"depth":146,"text":122},{"id":565,"depth":146,"text":566},{"id":1018,"depth":146,"text":1019,"children":2134},[2135,2136,2137],{"id":1299,"depth":157,"text":1300},{"id":1371,"depth":157,"text":1372},{"id":1431,"depth":157,"text":1432},{"id":1577,"depth":146,"text":1578},{"id":1645,"depth":146,"text":1646},{"id":1704,"depth":146,"text":1705},{"id":1975,"depth":146,"text":1976},{"id":2058,"depth":146,"text":2059,"children":2143},[2144,2145,2146,2147,2148],{"id":2062,"depth":157,"text":2063},{"id":2069,"depth":157,"text":2070},{"id":2076,"depth":157,"text":2077},{"id":2083,"depth":157,"text":2084},{"id":2090,"depth":157,"text":2091},{"id":2097,"depth":146,"text":2098},"2026-07-05","Move a Python CLI from argparse to Typer step by step: map arguments and subparsers to Typer commands, keep behaviour, and cut boilerplate.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer",{"title":5,"description":2151},"modern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer\u002Findex",[16,2160,2161,2162,2163],"typer","cli","migration","structure","pyZIlzYUnuh_a0D8fUbbzBez7lLx3V-tCgdNHRPQDJk",[2166,2169,2172,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2261,2264,2267,2270,2273,2276,2279,2281,2282,2285,2288,2291,2294,2297,2300,2303,2306,2309,2312,2315,2318,2321,2324,2327,2330,2333,2335,2338,2341,2344,2347,2350,2353,2356,2359,2362,2365,2368,2371,2374,2377,2380,2383,2386,2389,2392,2395,2398,2401,2404,2407],{"path":2167,"title":2168},"\u002Fabout","About Python CLI Toolcraft",{"path":2170,"title":2171},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2173,"title":2174},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2176,"title":2177},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2179,"title":2180},"\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":2182,"title":2183},"\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":2185,"title":2186},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2188,"title":2189},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2191,"title":2192},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2194,"title":2195},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2197,"title":2198},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2200,"title":2201},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2203,"title":2204},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2206,"title":2207},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2209,"title":2210},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2212,"title":2213},"\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":2215,"title":2216},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2218,"title":2219},"\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":2221,"title":2222},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2224,"title":2225},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2227,"title":2228},"\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":2230,"title":2231},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2233,"title":2234},"\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":2236,"title":2237},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2239,"title":2240},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2242,"title":2243},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2245,"title":2246},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2248,"title":2249},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2251,"title":2252},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2254,"title":2255},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2257,"title":2258},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1194,"title":2260},"Python CLI Toolcraft",{"path":2262,"title":2263},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2265,"title":2266},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2268,"title":2269},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2271,"title":2272},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2274,"title":2275},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2277,"title":2278},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2280,"title":2107},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse",{"path":2156,"title":5},{"path":2283,"title":2284},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2286,"title":2287},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2289,"title":2290},"\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":2292,"title":2293},"\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":2295,"title":2296},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2298,"title":2299},"\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":2301,"title":2302},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2304,"title":2305},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2307,"title":2308},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2310,"title":2311},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2313,"title":2314},"\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":2316,"title":2317},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2319,"title":2320},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2322,"title":2323},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2325,"title":2326},"\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":2328,"title":2329},"\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":2331,"title":2332},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2334,"title":1642},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained",{"path":2336,"title":2337},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2339,"title":2340},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2342,"title":2343},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2345,"title":2346},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2348,"title":2349},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2351,"title":2352},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2354,"title":2355},"\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":2357,"title":2358},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2360,"title":2361},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2363,"title":2364},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2366,"title":2367},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2369,"title":2370},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2372,"title":2373},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2375,"title":2376},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2378,"title":2379},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2381,"title":2382},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2384,"title":2385},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2387,"title":2388},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2390,"title":2391},"\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":2393,"title":2394},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2396,"title":2397},"\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":2399,"title":2400},"\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":2402,"title":2403},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2405,"title":2406},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2408,"title":2409},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690032]