[{"data":1,"prerenderedAt":1238},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison\u002F":3,"content-directory":991},{"id":4,"title":5,"body":6,"date":977,"description":978,"difficulty":979,"draft":980,"extension":981,"meta":982,"navigation":733,"path":983,"seo":984,"stem":985,"tags":986,"updated":977,"__hash__":990},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison\u002Findex.md","argparse vs Click vs Typer Compared",{"type":7,"value":8,"toc":955},"minimark",[9,18,23,52,56,59,63,67,225,377,472,475,479,482,485,502,518,535,548,552,555,561,567,577,580,584,590,601,607,623,627,630,643,656,673,677,680,787,793,797,803,807,812,815,819,828,832,839,843,862,866,880,884,887,891,897,901,911,915,951],[10,11,12,13,17],"p",{},"Three libraries, one job. They all turn ",[14,15,16],"code",{},"sys.argv"," into typed values and dispatch to a function,\nand they all produce the same usage errors and the same exit code 2. What differs is how much you\nwrite yourself, and how much arrives for free.",[19,20,22],"h2",{"id":21},"tldr","TL;DR",[24,25,26,34,40,46,49],"ul",{},[27,28,29,33],"li",{},[30,31,32],"strong",{},"argparse"," — stdlib, no dependency, everything beyond parsing is yours to build.",[27,35,36,39],{},[30,37,38],{},"Click"," — decorators, nested groups, completion, a test runner, explicit parameter objects.",[27,41,42,45],{},[30,43,44],{},"Typer"," — Click underneath, declared through type hints instead of decorators.",[27,47,48],{},"The deciding question is whether a dependency is acceptable. Everything else is style.",[27,50,51],{},"Typer and Click share a runtime, so moving between them is a local refactor, not a rewrite.",[19,53,55],{"id":54},"prerequisites","Prerequisites",[10,57,58],{},"Familiarity with writing a small CLI in any one of the three. Everything here targets current\nversions on Python 3.11 or newer.",[19,60,62],{"id":61},"the-same-command-three-times","The same command, three times",[64,65],"inline-diagram",{"name":66},"three-way-same-command",[68,69,74],"pre",{"className":70,"code":71,"language":72,"meta":73,"style":73},"language-python shiki shiki-themes github-light github-dark","# argparse\nparser = argparse.ArgumentParser(prog=\"mytool\")\nparser.add_argument(\"source\", type=Path)\nparser.add_argument(\"--retries\", type=int, default=3, help=\"Attempts per file.\")\nparser.add_argument(\"--dry-run\", action=\"store_true\")\nargs = parser.parse_args()\nsync(args.source, retries=args.retries, dry_run=args.dry_run)\n","python","",[14,75,76,85,112,132,172,192,203],{"__ignoreMap":73},[77,78,81],"span",{"class":79,"line":80},"line",1,[77,82,84],{"class":83},"sJ8bj","# argparse\n",[77,86,88,92,96,99,103,105,109],{"class":79,"line":87},2,[77,89,91],{"class":90},"sVt8B","parser ",[77,93,95],{"class":94},"szBVR","=",[77,97,98],{"class":90}," argparse.ArgumentParser(",[77,100,102],{"class":101},"s4XuR","prog",[77,104,95],{"class":94},[77,106,108],{"class":107},"sZZnC","\"mytool\"",[77,110,111],{"class":90},")\n",[77,113,115,118,121,124,127,129],{"class":79,"line":114},3,[77,116,117],{"class":90},"parser.add_argument(",[77,119,120],{"class":107},"\"source\"",[77,122,123],{"class":90},", ",[77,125,126],{"class":101},"type",[77,128,95],{"class":94},[77,130,131],{"class":90},"Path)\n",[77,133,135,137,140,142,144,146,150,152,155,157,160,162,165,167,170],{"class":79,"line":134},4,[77,136,117],{"class":90},[77,138,139],{"class":107},"\"--retries\"",[77,141,123],{"class":90},[77,143,126],{"class":101},[77,145,95],{"class":94},[77,147,149],{"class":148},"sj4cs","int",[77,151,123],{"class":90},[77,153,154],{"class":101},"default",[77,156,95],{"class":94},[77,158,159],{"class":148},"3",[77,161,123],{"class":90},[77,163,164],{"class":101},"help",[77,166,95],{"class":94},[77,168,169],{"class":107},"\"Attempts per file.\"",[77,171,111],{"class":90},[77,173,175,177,180,182,185,187,190],{"class":79,"line":174},5,[77,176,117],{"class":90},[77,178,179],{"class":107},"\"--dry-run\"",[77,181,123],{"class":90},[77,183,184],{"class":101},"action",[77,186,95],{"class":94},[77,188,189],{"class":107},"\"store_true\"",[77,191,111],{"class":90},[77,193,195,198,200],{"class":79,"line":194},6,[77,196,197],{"class":90},"args ",[77,199,95],{"class":94},[77,201,202],{"class":90}," parser.parse_args()\n",[77,204,206,209,212,214,217,220,222],{"class":79,"line":205},7,[77,207,208],{"class":90},"sync(args.source, ",[77,210,211],{"class":101},"retries",[77,213,95],{"class":94},[77,215,216],{"class":90},"args.retries, ",[77,218,219],{"class":101},"dry_run",[77,221,95],{"class":94},[77,223,224],{"class":90},"args.dry_run)\n",[68,226,228],{"className":70,"code":227,"language":72,"meta":73,"style":73},"# Click\n@click.command()\n@click.argument(\"source\", type=click.Path(exists=True, path_type=Path))\n@click.option(\"--retries\", type=int, default=3, show_default=True, help=\"Attempts per file.\")\n@click.option(\"--dry-run\", is_flag=True)\ndef sync(source: Path, retries: int, dry_run: bool) -> None:\n    \"\"\"Sync SOURCE to the bucket.\"\"\"\n",[14,229,230,235,244,281,325,344,372],{"__ignoreMap":73},[77,231,232],{"class":79,"line":80},[77,233,234],{"class":83},"# Click\n",[77,236,237,241],{"class":79,"line":87},[77,238,240],{"class":239},"sScJk","@click.command",[77,242,243],{"class":90},"()\n",[77,245,246,249,252,254,256,258,260,263,266,268,271,273,276,278],{"class":79,"line":114},[77,247,248],{"class":239},"@click.argument",[77,250,251],{"class":90},"(",[77,253,120],{"class":107},[77,255,123],{"class":90},[77,257,126],{"class":101},[77,259,95],{"class":94},[77,261,262],{"class":90},"click.Path(",[77,264,265],{"class":101},"exists",[77,267,95],{"class":94},[77,269,270],{"class":148},"True",[77,272,123],{"class":90},[77,274,275],{"class":101},"path_type",[77,277,95],{"class":94},[77,279,280],{"class":90},"Path))\n",[77,282,283,286,288,290,292,294,296,298,300,302,304,306,308,311,313,315,317,319,321,323],{"class":79,"line":134},[77,284,285],{"class":239},"@click.option",[77,287,251],{"class":90},[77,289,139],{"class":107},[77,291,123],{"class":90},[77,293,126],{"class":101},[77,295,95],{"class":94},[77,297,149],{"class":148},[77,299,123],{"class":90},[77,301,154],{"class":101},[77,303,95],{"class":94},[77,305,159],{"class":148},[77,307,123],{"class":90},[77,309,310],{"class":101},"show_default",[77,312,95],{"class":94},[77,314,270],{"class":148},[77,316,123],{"class":90},[77,318,164],{"class":101},[77,320,95],{"class":94},[77,322,169],{"class":107},[77,324,111],{"class":90},[77,326,327,329,331,333,335,338,340,342],{"class":79,"line":174},[77,328,285],{"class":239},[77,330,251],{"class":90},[77,332,179],{"class":107},[77,334,123],{"class":90},[77,336,337],{"class":101},"is_flag",[77,339,95],{"class":94},[77,341,270],{"class":148},[77,343,111],{"class":90},[77,345,346,349,352,355,357,360,363,366,369],{"class":79,"line":194},[77,347,348],{"class":94},"def",[77,350,351],{"class":239}," sync",[77,353,354],{"class":90},"(source: Path, retries: ",[77,356,149],{"class":148},[77,358,359],{"class":90},", dry_run: ",[77,361,362],{"class":148},"bool",[77,364,365],{"class":90},") -> ",[77,367,368],{"class":148},"None",[77,370,371],{"class":90},":\n",[77,373,374],{"class":79,"line":205},[77,375,376],{"class":107},"    \"\"\"Sync SOURCE to the bucket.\"\"\"\n",[68,378,380],{"className":70,"code":379,"language":72,"meta":73,"style":73},"# Typer\n@app.command()\ndef sync(\n    source: Annotated[Path, typer.Argument(exists=True)],\n    retries: Annotated[int, typer.Option(help=\"Attempts per file.\")] = 3,\n    dry_run: bool = False,\n) -> None:\n    \"\"\"Sync SOURCE to the bucket.\"\"\"\n",[14,381,382,387,394,403,417,444,459,467],{"__ignoreMap":73},[77,383,384],{"class":79,"line":80},[77,385,386],{"class":83},"# Typer\n",[77,388,389,392],{"class":79,"line":87},[77,390,391],{"class":239},"@app.command",[77,393,243],{"class":90},[77,395,396,398,400],{"class":79,"line":114},[77,397,348],{"class":94},[77,399,351],{"class":239},[77,401,402],{"class":90},"(\n",[77,404,405,408,410,412,414],{"class":79,"line":134},[77,406,407],{"class":90},"    source: Annotated[Path, typer.Argument(",[77,409,265],{"class":101},[77,411,95],{"class":94},[77,413,270],{"class":148},[77,415,416],{"class":90},")],\n",[77,418,419,422,424,427,429,431,433,436,438,441],{"class":79,"line":174},[77,420,421],{"class":90},"    retries: Annotated[",[77,423,149],{"class":148},[77,425,426],{"class":90},", typer.Option(",[77,428,164],{"class":101},[77,430,95],{"class":94},[77,432,169],{"class":107},[77,434,435],{"class":90},")] ",[77,437,95],{"class":94},[77,439,440],{"class":148}," 3",[77,442,443],{"class":90},",\n",[77,445,446,449,451,454,457],{"class":79,"line":194},[77,447,448],{"class":90},"    dry_run: ",[77,450,362],{"class":148},[77,452,453],{"class":94}," =",[77,455,456],{"class":148}," False",[77,458,443],{"class":90},[77,460,461,463,465],{"class":79,"line":205},[77,462,365],{"class":90},[77,464,368],{"class":148},[77,466,371],{"class":90},[77,468,470],{"class":79,"line":469},8,[77,471,376],{"class":107},[10,473,474],{},"Three dialects, identical behaviour. The trend is visible: each step moves information out of\nfunction calls and into the place Python already keeps it — the signature.",[19,476,478],{"id":477},"feature-by-feature","Feature by feature",[64,480],{"name":481},"three-way-feature-matrix",[10,483,484],{},"Four differences matter in practice.",[10,486,487,490,491,493,494,497,498,501],{},[30,488,489],{},"Nested commands."," ",[14,492,32],{}," has subparsers, and they work, but every level needs its own\n",[14,495,496],{},"dest",", dispatch is a ",[14,499,500],{},"set_defaults(func=…)"," convention you implement, and there is no equivalent\nof a group callback that runs before each subcommand. Click and Typer treat groups as first-class.",[10,503,504,507,508,510,511,514,515,517],{},[30,505,506],{},"Shell completion."," Free in Click and one command away in Typer. In ",[14,509,32],{}," it needs\n",[14,512,513],{},"argcomplete"," — a third-party dependency, which undercuts the main reason to be on ",[14,516,32],{}," in\nthe first place.",[10,519,520,523,524,527,528,530,531,534],{},[30,521,522],{},"Testing."," Click's ",[14,525,526],{},"CliRunner"," invokes commands in-process with captured streams; Typer\nre-exports it. Testing ",[14,529,32],{}," means calling ",[14,532,533],{},"parse_args"," on a list and testing your dispatch\nseparately, which works but exercises less of the real path.",[10,536,537,540,541,543,544,547],{},[30,538,539],{},"Shared state."," Click's context carries an object down the tree, so global options load\nconfiguration once. In ",[14,542,32],{}," shared state travels in the ",[14,545,546],{},"Namespace"," or in variables you\nthread through by hand.",[19,549,551],{"id":550},"where-each-one-is-the-right-answer","Where each one is the right answer",[64,553],{"name":554},"parser-choice-decision",[10,556,557,560],{},[30,558,559],{},"Choose argparse"," when a dependency is genuinely unacceptable: an installer, a bootstrap script,\na tool that runs inside a locked-down image, or something that ships in an environment where you\ncannot add packages. It is stable, documented and present in every Python.",[10,562,563,566],{},[30,564,565],{},"Choose Typer"," for most new tools with more than one command. The type hints you would write\nanyway become the interface, completion and groups are free, and the runtime is Click's.",[10,568,569,572,573,576],{},[30,570,571],{},"Choose Click"," when your commands are built rather than declared — a plugin system that\nconstructs commands in a loop, options derived from a schema at run time, or a custom ",[14,574,575],{},"Command","\nsubclass. Click's object model is designed for that; Typer's signature-driven model assumes the\nparameters are known at import time.",[10,578,579],{},"What should not drive the choice is performance. Typer's construction cost is single-digit\nmilliseconds against a 20 ms interpreter floor — an order of magnitude less than one heavy import.",[19,581,583],{"id":582},"what-no-dependencies-is-really-worth","What \"no dependencies\" is really worth",[10,585,586,587,589],{},"The argument for ",[14,588,32],{}," is a real one, and it is worth being precise about its size.",[10,591,592,593,596,597,600],{},"A wheel with ",[14,594,595],{},"typer"," and ",[14,598,599],{},"rich"," adds roughly two megabytes and about 50 ms of import time. In\nexchange you stop maintaining: subcommand dispatch, completion scripts, parameter type conversion\nwith consistent error messages, a testing runner, and context propagation. That is a few hundred\nlines you own forever, and they are lines that get subtly wrong edge cases — the kind that produce\na confusing message six months later.",[10,602,603,604,606],{},"Where the trade genuinely favours ",[14,605,32],{},":",[24,608,609,617,620],{},[27,610,611,612,616],{},"The tool ",[613,614,615],"em",{},"is"," the bootstrap step, so it cannot depend on anything installable.",[27,618,619],{},"It ships into an audited environment where every dependency is a review.",[27,621,622],{},"It is one command with three flags and will stay that way.",[19,624,626],{"id":625},"ux-considerations","UX considerations",[10,628,629],{},"The frameworks differ in what a user sees by default, which is worth knowing before you pick.",[10,631,632,635,636,638,639,642],{},[30,633,634],{},"Help rendering."," Typer renders through Rich — colour, panels, aligned columns. Click and\n",[14,637,32],{}," produce plain text. Most people prefer the Rich output; if you do not,\n",[14,640,641],{},"rich_markup_mode=None"," turns it off.",[10,644,645,648,649,652,653,655],{},[30,646,647],{},"Error messages."," All three exit 2 on a usage error. Click and Typer name the offending option\nand suggest ",[14,650,651],{},"--help","; ",[14,654,32],{}," prints the usage line and a shorter message. The difference is\nsmall and consistently in Click's favour.",[10,657,658,661,662,665,666,668,669,672],{},[30,659,660],{},"Defaults in help."," Typer shows them automatically, Click needs ",[14,663,664],{},"show_default=True",", and\n",[14,667,32],{}," needs ",[14,670,671],{},"ArgumentDefaultsHelpFormatter",". Turn it on in all three — undocumented defaults\nare the most common gap in CLI help.",[19,674,676],{"id":675},"testing-the-behaviour","Testing the behaviour",[10,678,679],{},"If you are evaluating rather than reading, the fastest comparison is to write the same two-command\ntool three times and test it each way. The tests tell you more than the implementations:",[68,681,683],{"className":70,"code":682,"language":72,"meta":73,"style":73},"# Click \u002F Typer\nresult = CliRunner().invoke(app, [\"sync\", \".\u002Fdata\", \"--dry-run\"])\nassert result.exit_code == 0\n\n# argparse\nargs = build_parser().parse_args([\"sync\", \".\u002Fdata\", \"--dry-run\"])\nassert args.dry_run is True\nassert args.func is do_sync            # dispatch tested separately\n",[14,684,685,690,715,729,735,739,760,772],{"__ignoreMap":73},[77,686,687],{"class":79,"line":80},[77,688,689],{"class":83},"# Click \u002F Typer\n",[77,691,692,695,697,700,703,705,708,710,712],{"class":79,"line":87},[77,693,694],{"class":90},"result ",[77,696,95],{"class":94},[77,698,699],{"class":90}," CliRunner().invoke(app, [",[77,701,702],{"class":107},"\"sync\"",[77,704,123],{"class":90},[77,706,707],{"class":107},"\".\u002Fdata\"",[77,709,123],{"class":90},[77,711,179],{"class":107},[77,713,714],{"class":90},"])\n",[77,716,717,720,723,726],{"class":79,"line":114},[77,718,719],{"class":94},"assert",[77,721,722],{"class":90}," result.exit_code ",[77,724,725],{"class":94},"==",[77,727,728],{"class":148}," 0\n",[77,730,731],{"class":79,"line":134},[77,732,734],{"emptyLinePlaceholder":733},true,"\n",[77,736,737],{"class":79,"line":174},[77,738,84],{"class":83},[77,740,741,743,745,748,750,752,754,756,758],{"class":79,"line":194},[77,742,197],{"class":90},[77,744,95],{"class":94},[77,746,747],{"class":90}," build_parser().parse_args([",[77,749,702],{"class":107},[77,751,123],{"class":90},[77,753,707],{"class":107},[77,755,123],{"class":90},[77,757,179],{"class":107},[77,759,714],{"class":90},[77,761,762,764,767,769],{"class":79,"line":205},[77,763,719],{"class":94},[77,765,766],{"class":90}," args.dry_run ",[77,768,615],{"class":94},[77,770,771],{"class":148}," True\n",[77,773,774,776,779,781,784],{"class":79,"line":469},[77,775,719],{"class":94},[77,777,778],{"class":90}," args.func ",[77,780,615],{"class":94},[77,782,783],{"class":90}," do_sync            ",[77,785,786],{"class":83},"# dispatch tested separately\n",[10,788,789,790,792],{},"The framework version tests the whole path — parsing, dispatch, output, exit code — in one call.\nThe ",[14,791,32],{}," version tests parsing, and leaves dispatch and output to separate tests you write\nyourself. For a small tool that is fine; at twenty commands it is a meaningful amount of test\ninfrastructure.",[19,794,796],{"id":795},"conclusion","Conclusion",[10,798,799,800,802],{},"If you cannot take a dependency, use ",[14,801,32],{}," and accept the extra wiring. If you can, use Typer\nunless your commands are constructed dynamically, in which case use Click. Because Typer and Click\nshare a runtime, that last decision is reversible in an afternoon — which is why it deserves far\nless agonising than it usually gets.",[19,804,806],{"id":805},"frequently-asked-questions","Frequently asked questions",[808,809,811],"h3",{"id":810},"is-click-still-maintained-now-that-typer-exists","Is Click still maintained now that Typer exists?",[10,813,814],{},"Yes — Typer is built on it, so Click's maintenance is a prerequisite for Typer's. They are\ncomplementary rather than competing, and a project using Typer is using Click whether it says so or\nnot.",[808,816,818],{"id":817},"which-is-fastest-to-start","Which is fastest to start?",[10,820,821,823,824,827],{},[14,822,32],{},", marginally, because it imports less. The gap is a few milliseconds and irrelevant next\nto a single heavy import in your own code. Measure with ",[14,825,826],{},"-X importtime"," before treating framework\nchoice as a performance decision.",[808,829,831],{"id":830},"can-i-mix-them-in-one-tool","Can I mix them in one tool?",[10,833,834,835,838],{},"Yes. ",[14,836,837],{},"typer.main.get_command(app)"," produces a Click command you can attach to a Click group, and\nTyper can host Click commands. That interop is what makes an incremental migration possible, and it\nis occasionally useful permanently — a dynamically built group in Click alongside declared commands\nin Typer.",[808,840,842],{"id":841},"do-they-all-handle-unicode-and-windows-paths-the-same","Do they all handle Unicode and Windows paths the same?",[10,844,845,846,849,850,853,854,857,858,861],{},"Yes. Arguments arrive as ",[14,847,848],{},"str"," already decoded by Python, and ",[14,851,852],{},"Path"," behaves correctly everywhere.\nCross-platform problems in CLIs are nearly always in your own code — a hard-coded separator, an\nassumption about ",[14,855,856],{},"bin"," versus ",[14,859,860],{},"Scripts"," — not in the parser.",[808,863,865],{"id":864},"what-about-other-libraries-docopt-fire-argh","What about other libraries — docopt, Fire, argh?",[10,867,868,871,872,875,876,879],{},[14,869,870],{},"docopt"," is effectively unmaintained; ",[14,873,874],{},"Fire"," is convenient for exploration and produces an\ninterface that is hard to keep stable; ",[14,877,878],{},"argh"," is a thin argparse wrapper. For a tool other people\ndepend on, the three in this comparison are the ones with the ecosystem and the maintenance to\nmatch.",[808,881,883],{"id":882},"how-much-code-does-the-framework-actually-save","How much code does the framework actually save?",[10,885,886],{},"For a tool with eight commands, typically two to three hundred lines: dispatch, shared option\ndeclarations, parameter conversion with consistent errors, completion, and the test harness around\nthem. That is not a huge amount of code, but it is code that has edge cases — and every line of it\nis something you maintain instead of the library's authors.",[808,888,890],{"id":889},"which-has-the-best-documentation","Which has the best documentation?",[10,892,893,894,896],{},"Click's is the most complete reference; Typer's is the friendliest introduction and assumes you\nwill read Click's for the details; ",[14,895,32],{},"'s is thorough and dense, in the style of the standard\nlibrary. All three are good enough that documentation should not decide the choice.",[808,898,900],{"id":899},"does-the-choice-affect-how-testable-my-logic-is","Does the choice affect how testable my logic is?",[10,902,903,904,907,908,910],{},"Not directly — that depends on whether the logic lives outside the command functions, which is a\ndecision independent of the framework. What the framework affects is how cheaply you can test the\n",[613,905,906],{},"interface",": Click and Typer give you an in-process runner, while ",[14,909,32],{}," leaves you asserting\non a parsed namespace and testing dispatch separately.",[19,912,914],{"id":913},"related","Related",[24,916,917,925,932,938,945],{},[27,918,919,920],{},"Up: ",[921,922,924],"a",{"href":923},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002F","Command-line parsing with argparse",[27,926,927,928],{},"Sideways: ",[921,929,931],{"href":930},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click: when to use each",[27,933,927,934],{},[921,935,937],{"href":936},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer\u002F","Migrating from argparse to Typer",[27,939,940,941],{},"Related: ",[921,942,944],{"href":943},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","Structuring multi-command Python CLIs",[27,946,940,947],{},[921,948,950],{"href":949},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F","Shell completion for Python CLIs",[952,953,954],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":73,"searchDepth":87,"depth":87,"links":956},[957,958,959,960,961,962,963,964,965,966,976],{"id":21,"depth":87,"text":22},{"id":54,"depth":87,"text":55},{"id":61,"depth":87,"text":62},{"id":477,"depth":87,"text":478},{"id":550,"depth":87,"text":551},{"id":582,"depth":87,"text":583},{"id":625,"depth":87,"text":626},{"id":675,"depth":87,"text":676},{"id":795,"depth":87,"text":796},{"id":805,"depth":87,"text":806,"children":967},[968,969,970,971,972,973,974,975],{"id":810,"depth":114,"text":811},{"id":817,"depth":114,"text":818},{"id":830,"depth":114,"text":831},{"id":841,"depth":114,"text":842},{"id":864,"depth":114,"text":865},{"id":882,"depth":114,"text":883},{"id":889,"depth":114,"text":890},{"id":899,"depth":114,"text":900},{"id":913,"depth":87,"text":914},"2026-08-01","A practical comparison of argparse, Click and Typer - dependencies, nested commands, completion, testing and the one question that decides the choice.","beginner",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison",{"title":5,"description":978},"modern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison\u002Findex",[32,987,595,988,989],"click","comparison","frameworks","cBeSdgYZyHkCqnQJDidC4O6jMkUUsM5mDVsBe1_sTf4",[992,995,998,1001,1004,1007,1010,1013,1016,1019,1022,1025,1028,1031,1034,1037,1040,1043,1046,1049,1052,1055,1058,1061,1064,1067,1070,1073,1076,1079,1082,1085,1088,1091,1094,1097,1100,1103,1104,1107,1109,1112,1115,1118,1121,1124,1127,1130,1133,1136,1139,1142,1145,1148,1151,1154,1157,1160,1163,1166,1169,1172,1175,1178,1181,1184,1187,1190,1193,1196,1199,1202,1205,1208,1211,1214,1217,1220,1223,1226,1229,1232,1235],{"path":993,"title":994},"\u002Fabout","About Python CLI Toolcraft",{"path":996,"title":997},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":999,"title":1000},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1002,"title":1003},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1005,"title":1006},"\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":1008,"title":1009},"\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":1011,"title":1012},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1014,"title":1015},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1017,"title":1018},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1020,"title":1021},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1023,"title":1024},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1026,"title":1027},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1029,"title":1030},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1032,"title":1033},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1035,"title":1036},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1038,"title":1039},"\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":1041,"title":1042},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1044,"title":1045},"\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":1047,"title":1048},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1050,"title":1051},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1053,"title":1054},"\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":1056,"title":1057},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1059,"title":1060},"\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":1062,"title":1063},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1065,"title":1066},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1068,"title":1069},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1071,"title":1072},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1074,"title":1075},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1077,"title":1078},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1080,"title":1081},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1083,"title":1084},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1086,"title":1087},"\u002F","Python CLI Toolcraft",{"path":1089,"title":1090},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1092,"title":1093},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1095,"title":1096},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1098,"title":1099},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1101,"title":1102},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":983,"title":5},{"path":1105,"title":1106},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1108,"title":937},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer",{"path":1110,"title":1111},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1113,"title":1114},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1116,"title":1117},"\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":1119,"title":1120},"\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":1122,"title":1123},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1125,"title":1126},"\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":1128,"title":1129},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1131,"title":1132},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1134,"title":1135},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1137,"title":1138},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1140,"title":1141},"\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":1143,"title":1144},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1146,"title":1147},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1149,"title":1150},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1152,"title":1153},"\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":1155,"title":1156},"\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":1158,"title":1159},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1161,"title":1162},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1164,"title":1165},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1167,"title":1168},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1170,"title":1171},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1173,"title":1174},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1176,"title":1177},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1179,"title":1180},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1182,"title":1183},"\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":1185,"title":1186},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1188,"title":1189},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1191,"title":1192},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1194,"title":1195},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1197,"title":1198},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1200,"title":1201},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1203,"title":1204},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1206,"title":1207},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1209,"title":1210},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1212,"title":1213},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1215,"title":1216},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1218,"title":1219},"\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":1221,"title":1222},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1224,"title":1225},"\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":1227,"title":1228},"\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":1230,"title":1231},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1233,"title":1234},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1236,"title":1237},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690031]