Topic 15.7
Command-Line Tools: argparse and Typer
In one line
A command-line tool reads its settings from the words typed after its name. Python's argparse module turns sys.argv into a clean object, builds --help for free, rejects bad input with a usage message, and lets the program report success or failure through its exit code.
Think of it like this
Ordering at a coffee counter with a short, fixed script. "One latte, large, no sugar." The barista knows the order of the words: the drink first, then the options. If you say "large latte purple", they do not guess; they tell you what they can make. A command-line tool works the same way: you type its name, then the thing to work on, then options, and it either does the job or says exactly how you should have asked.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Command line
- The text line you type in a terminal to run a program.
- Argument
- A word you type after the program name to tell it what to do.
- sys.argv
- The raw list of command-line words, as strings. The first is the script name.
- Positional argument
- A required argument identified by its position.
- Option / flag
- An optional argument that starts with dashes. A flag has no value; it is on or off.
- Subcommand
- A named action inside a tool, such as git commit.
- Exit code
- The number a program returns when it ends. 0 means success, anything else means failure.
- stdout / stderr
- The normal output stream and the error stream. They can be redirected separately.
- Typer
- A library that builds a command-line tool from a function's type hints.
Step by step
01Start with sys.argv
Save a file and run it with extra words after its name. Everything arrives as strings in a list. This is what argparse starts from.
import sys
print(sys.argv)02Declare what the tool accepts
Each add_argument call is one entry on the menu. A name without dashes is positional and required. A name with dashes is an option. store_true makes a flag whose value is False unless it appears.
import argparse
parser = argparse.ArgumentParser(description="Greet someone.")
parser.add_argument("name", help="who to greet")
parser.add_argument("--times", type=int, default=1, help="how many greetings")
parser.add_argument("--shout", action="store_true", help="use capital letters")03Parse, and read the result
parse_args() with no argument reads sys.argv[1:]. In a test (and on this page) you can pass a list instead: parse_args(["Asha", "--times", "2"]). The result is a Namespace, an object whose attributes are the argument names (a dash in --dry-run becomes dry_run).
args = parser.parse_args(["Asha", "--times", "2", "--shout"])
print(args.name, args.times, args.shout) # Asha 2 True04Help and errors come free
argparse reads your declarations to write the --help page. On a bad command line it prints the usage plus one clear sentence and exits with code 2. The second runnable example below catches that SystemExit so you can see the code.
$ python main.py
usage: main.py [-h] [--times TIMES] [--shout] name
main.py: error: the following arguments are required: name
$ echo $?
205Subcommands
add_subparsers(dest="command", required=True) creates the choice of actions. Each add_parser returns a parser for that action's own arguments. Afterwards args.command says which one the user picked.
06main() returns the exit code
Put the work in a function that takes the argument list and returns an integer. Then the real entry point is raise SystemExit(main()). Tests call main(["add", "milk"]) directly and check the return value, with no subprocess needed.
def main(argv=None) -> int:
args = build_parser().parse_args(argv)
...
return 0
if __name__ == "__main__":
raise SystemExit(main())07The Typer way
In Typer you write one function; the type hints define the interface: a plain parameter is a positional argument, a parameter with a default becomes an option, and a bool becomes a --flag/--no-flag pair. The block in the code section shows it with a real run.
Try it yourself
- 1
Add a flag
In the first example add
parser.add_argument("--polite", action="store_true")and print "Please" before the greeting when it is set. Parse a list that includes"--polite". - 2
Cause a usage error
In the second example change the last call to
main(["add"])(no title) and read the message argparse prints. Then try--priority mediumand notice that the error lists the allowed choices. - 3
Return a failure code
Make
mainreturn1when a task id is negative, and printexit code:for it. The code is yours to define: choose 0 for success and non-zero for problems, and document it.
Code & diagrams
The prog name in the usage line is main.py. parse_args receives a fixed list so the example is repeatable.
Expected output
Namespace(name='Asha', times=2, shout=True)
HELLO, ASHA!
HELLO, ASHA!
Namespace(name='Ben', times=1, shout=False)
usage: main.py [-h] [--times TIMES] [--shout] name
Greet someone.
positional arguments:
name who to greet
options:
-h, --help show this help message and exit
--times TIMES how many greetings
--shout use capital lettersThe bad call (task id 'three') makes argparse print to the error stream and raise SystemExit(2).
Expected output
added 'buy milk' with priority high
exit code: 0
finished task 3
exit code: 0
usage: todo done [-h] task_id
todo done: error: argument task_id: invalid int value: 'three'
SystemExit code: 2Not runnable here: Typer is not in the browser. Verified on a real machine (Python 3.12, a recent Typer). The --help page is drawn with boxes and varies with the terminal width, so only the working calls are shown.
# pip install typer
import typer
app = typer.Typer()
@app.command()
def greet(name: str, times: int = 1, shout: bool = False):
"""Greet NAME."""
for _ in range(times):
text = f"Hello, {name}!"
typer.echo(text.upper() if shout else text)
if __name__ == "__main__":
app()Output
$ python greet.py Asha --times 2 --shout
HELLO, ASHA!
HELLO, ASHA!
$ python greet.py --times 2
(prints a usage error: Missing argument 'name'.)
$ echo $?
2Not runnable here. These are standard uv commands; the tool names are examples, so check each tool's own page before using it.
# run a published command-line tool in a temporary isolated environment
uvx ruff --version
# install a tool permanently, available on your PATH
uv tool install ruffBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Leave out a required argument
Call parse_args with an empty list when a positional argument is required.
import argparse
parser = argparse.ArgumentParser(description="Greet someone.")
parser.add_argument("name")
parser.add_argument("--times", type=int, default=1)
args = parser.parse_args([])
print(args)Myth vs fact
Myth
I can read sys.argv myself, argparse is overkill.
Fact
For one value it is fine. As soon as you have options, defaults, types and help, hand parsing is longer and worse than argparse, which also gives consistent errors and --help.
Myth
Printing an error message is enough to report failure.
Fact
Other programs and CI check the exit code, not your text. A tool that prints "failed" but exits with 0 looks successful to the shell.
Myth
Typer replaces argparse.
Fact
Typer builds on Click, not on argparse. Both are good. argparse needs no installation, which matters for scripts you share.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Keep parsing separate from work:
build_parser()and amain(argv)that returns an int. This makes the tool easy to test (Phase 11) without launching a process. - ▸
Declare the tool in
pyproject.tomlunder[project.scripts](for exampletodo = "mytool.cli:main"; Topic 6.7 coverspyproject.toml) and installing the package creates a realtodocommand. That is howruff,pytestand many AI tools ship. - ▸
Respect Unix conventions: results to stdout, diagnostics and progress to stderr,
-for stdin, exit code 0/1/2 as documented, and--version. Keep--helpaccurate: it is the manual users actually read. - ▸
Do not accept secrets as arguments (they appear in process lists and shell history, Topic 15.3); read them from the environment or a file.
- ▸
argparse behaviours that surprise people: it accepts any unambiguous prefix of a long option (
--tim 3works for--times), which can silently break scripts when you later add--timeout; passallow_abbrev=Falsefor tools others will script against.type=is called on a string, sotype=intaccepts"-3"and you still need your own range check (or a custom type function that raisesargparse.ArgumentTypeError). For booleans,action=argparse.BooleanOptionalAction(3.9) gives a--cache/--no-cachepair, whereastype=boolis a trap becausebool("False")isTrue.--ends option parsing, so a value that starts with a dash can still be a positional argument. - ▸
sys.exit("message")prints the message to stderr and exits with code 1,sys.exit(0)orsys.exit()exits with 0, andSystemExitis not caught byexcept Exception(it derives fromBaseException). Conventions worth following: 0 success, 1 general failure, 2 usage error (argparse's choice), and 130 when the user pressed Ctrl+C. CatchKeyboardInterruptat the top ofmainfor a clean message, not a traceback.
Remember this
- 1
A command-line interface (CLI) is a program you run from a terminal by typing a line such as
python tool.py report.csv --top 5. The words after the program name are arguments. Python gives them to you insys.argv, a plain list of strings whose first item is the script's name. You could parse that list by hand, but you would end up rewriting whatargparsealready does. - 2
**
argparse** is in the standard library. You create anArgumentParser, declare what the tool accepts withadd_argument, and callparse_args(). A positional argument (name) is required and identified by its position. An option starts with dashes (--times 3, or a short form-t 3), is optional, and can have a default. A flag is an option with no value (--shout), declared withaction="store_true". - 3
type=intconverts the text to a number and rejectsthreewith a clear message.choices=["low", "high"]limits the allowed words.default=sets the value when the option is missing.help=text appears in the automatic--helppage, which argparse builds for you, together with the one-line usage summary. - 4
When the command line is wrong, argparse prints a usage message and an error to the *error stream* (
stderr) and ends the program with exit code2by raisingSystemExit.--helpprints the help to the normal output and exits with code0. This is why, inside a test or a notebook, you may needtry: ... except SystemExitaroundparse_args(Topic 7.1 explainstryandexcept). - 5
An exit code is a small number a program hands to the shell when it ends. 0 means success; anything else means failure. Scripts, CI systems and
&&chains depend on this. Makemain()return an integer and finish withraise SystemExit(main())(orsys.exit(main())). Print normal results to stdout and errors to stderr, so output can be piped to other programs without error text mixed in. - 6
Tools often have several actions, such as
git addandgit commit. In argparse these are subcommands, created withadd_subparsers; each has its own arguments. For friendlier tools, Typer (built on type hints, so a function's parameters become the arguments and--helpis generated) and Click (the library under Typer) are popular, and Rich adds coloured tables and progress bars. Install a ready-made tool without touching your project withuvx tool-name(it runs the tool in a temporary isolated environment). Many AI utilities are CLIs, such as small scripts that run an evaluation or index a folder.
Explain it without notes
What is the difference between a positional argument, an option and a flag in argparse?
What are exit codes, and what does argparse do on bad input?
How do you build a tool with subcommands, and how do you test it without a terminal?
Why do many tools read secrets from the environment rather than a --api-key option?
Practice
Write a parser for a tool repeat with a positional word and an option --count (integer, default 2), and print the word that many times on one line.
Add a --upper flag to it.
Write main(argv) that returns 0 on success and 1 if --count is below 1 (print an error to sys.stderr).
Trade-offs
- ↔
argparse versus Typer or Click. argparse is built in and fine for scripts you share. Typer and Click give nicer code and help for larger tools, at the cost of a dependency.
- ↔
Many small options versus a config file. Options are easy to discover and script; too many make the command unreadable. Move stable settings to a config file or environment variables (Topic 15.3) and keep options for what changes per run.
Done when you can
Done when you can explain what
sys.argvholds.Done when you can write an argparse parser with a positional argument, an option with a type and default, and a flag.
Done when you can add subcommands and read
args.command.Done when
main()returns an exit code and you know 0 means success.Done when you can say what Typer adds and how
uvxruns a tool.