Click vs Typer vs argparse for Python CLIs and Which to Pick
Compare Click, Typer and argparse for Python command-line tools: size, type hints, subcommands, dependencies and testing, with the same example in each.

A command-line tool needs the same few things: options and arguments, types, defaults, help text and error messages. Python has one library in the standard library and two popular ones on top. They do the job equally well; they differ in how much code it takes and what you must install. The examples use Python 3.12, Click 8.1 and Typer 0.12; the underlying basics are in argparse in Python and Click and Typer.
argparse
import argparse
def main():
p = argparse.ArgumentParser(description="Greet people")
p.add_argument("name")
p.add_argument("-n", "--times", type=int, default=1, help="repeat count")
p.add_argument("--shout", action="store_true")
args = p.parse_args()
msg = f"Hello {args.name}"
for _ in range(args.times):
print(msg.upper() if args.shout else msg)
if __name__ == "__main__":
main()It is verbose but needs nothing installed, which is the main argument for it: a script that must run on any machine with Python can use it with no pip step. Subcommands use add_subparsers, which is clunky at scale. Help and error output are decent and follow a conventional layout. Because it ships with Python, its behaviour is stable across versions.
Click
import click
@click.command()
@click.argument("name")
@click.option("-n", "--times", default=1, show_default=True, help="repeat count")
@click.option("--shout", is_flag=True)
def greet(name, times, shout):
msg = f"Hello {name}"
for _ in range(times):
click.echo(msg.upper() if shout else msg)
if __name__ == "__main__":
greet()Decorators put each option next to the function that uses it. Click shines for tools with groups of subcommands, @click.group(), with prompts such as click.prompt and click.confirm, file arguments through click.File, environment variable fallbacks with envvar=, and shell completion. click.testing.CliRunner invokes a command in tests and captures its output, which makes CLIs easy to test; see testing a Python CLI with pytest.
Typer
import typer
def greet(name: str, times: int = 1, shout: bool = False):
msg = f"Hello {name}"
for _ in range(times):
print(msg.upper() if shout else msg)
if __name__ == "__main__":
typer.run(greet)Typer reads the function signature: a parameter without a default is an argument, one with a default is an option, and the type hint sets validation. bool parameters become --shout/--no-shout flags. It is built on Click, so it inherits Click's features and adds rich help formatting. The price is a heavier dependency tree and a style that depends on annotations, with Annotated[...] for extra metadata.
Choosing
For a script of under a hundred lines, or one distributed as a single file, use argparse. For a tool with a dozen subcommands, shared options and tests, use Click, or Typer if your code already leans on type hints. Pay attention to startup cost: Click and Typer import more modules and add milliseconds to every invocation, as discussed in why Python starts slower than shell. The options for packaging such a tool are in distributing CLIs with pipx.
What each library gives you for free
Every tool needs help output, error messages for bad input and a non-zero exit status when parsing fails. argparse produces all three, formats usage from the declarations, and exits with status 2 on error, the same convention as the shell tools. Click does the same and adds shell completion, coloured output, a testing runner and a clean way to handle --version. Typer builds on Click and so inherits all of it, then derives the declarations from type hints, and adds rich-formatted errors when the optional extra is installed. The difference is in how much code stands between the declaration and the behaviour you want, and in how much of the library you must understand when something unusual comes up.
Subcommands show the gap most clearly. In argparse they need a subparsers object, one parser per command and a dispatch on the chosen name, usually through set_defaults(func=...). In Click a group is a decorated function and each command is another decorated function. In Typer an app object collects functions with a decorator and sub-apps nest. Shared options, such as --verbose on every command, are a parent parser in argparse and a group-level option or a context object in the other two.
Testing a command-line tool
Whichever library you pick, keep the logic out of the parsing function. A function that takes plain arguments and returns a result can be covered by ordinary unit tests, and the thin command layer is checked by a few tests that invoke it with arguments. Click and Typer ship a CliRunner that runs the command in process and captures output and exit code, which is faster and more reliable than spawning a subprocess. With argparse, pass a list to parse_args(argv) in tests, and catch SystemExit for the error cases. Test the unhappy paths too: a missing argument, a bad type and an unknown option should produce the exit code and message users will see.
Startup time and dependencies
Import time matters for a tool that runs hundreds of times a day. argparse is in the standard library and imports quickly. Click adds a small import, and Typer pulls in Click and, optionally, Rich, which adds noticeable startup time on slow disks. For a command that runs inside a shell loop or a Git hook, measure with python -X importtime or time the command, and choose the lighter option if the extra tenth of a second repeats thousands of times. Packaging matters as well: a single script with argparse can be shared as a file, while the others need an installable package or a zipapp with its dependencies.
Environment variables and configuration
Many tools also read defaults from the environment. Click supports it directly with envvar="APP_TOKEN" on an option, and Typer passes the same argument. In argparse, set the default from os.environ.get when building the parser, and document it in the help text. Flags should win over the environment, and the environment over a config file, as in most Unix tools. Keep secrets out of default values that appear in help output, because --help prints the default and would show a token.
Output formats and exit codes
Command-line tools are used by scripts as well as people, so decide early what the program prints and returns. Send results to standard output and diagnostics to standard error, so that a pipeline can capture the first without the second. Offer a --json option for structured output when other programs will read it, and keep the human format free to change. Return zero on success, one for a runtime failure and two for a usage error, the same convention argparse follows, and document any other codes. Handle KeyboardInterrupt and a closed pipe, which raises BrokenPipeError when output goes to head, with a quiet exit and not a traceback. These small details separate a script from a tool that others can build on.
Where this does not apply
If the script takes one or two positional arguments, sys.argv is enough, and a library only adds weight. For very large tools consider the structure and plugin systems of Click, or alternatives such as cyclopts and fire, though their behaviour is less conventional. The reference documentation is the argparse documentation, Click documentation and Typer documentation.


