How to Parse Command-Line Arguments in Python: argparse
Python's argparse parses arguments, options, types, choices and sub-commands and generates --help. Plus sys.argv, stderr for errors and exit codes.
- Course: Python study plan
- Module: Modules, packages and imports
- Kind: Lesson
- Reading time: 14 min
- Runtime: CPython 3.11
How do you parse command-line arguments in Python?
Use the standard-library argparse module: create an ArgumentParser, declare arguments with add_argument (positionals by name; options with -x or --long; type=int to convert; choices, default, and action="store_true" for flags), then call parse_args(). It generates --help and exits with status 2 on bad usage. For one or two plain arguments, sys.argv[1:] is enough.
Lesson
A script is a program meant to be run from a shell, and a good one follows the conventions every other command-line tool follows: arguments and options parsed properly, --help that explains them, output on stdout and messages on stderr, an exit status that says whether it worked, and a structure that keeps the logic testable apart from the parsing. This lesson covers sys.argv, argparse with its argument kinds and sub-commands, the three streams and why errors go to stderr, exit codes, reading from a file or stdin interchangeably, environment variables, and the main(argv) shape that makes a script importable and testable.
sys.argv
import sys
print(sys.argv) # ['tool.py', 'input.txt', '--verbose']
sys.argv[0] is the script path; the rest are the arguments as strings, split by the shell. For one or two positional arguments, reading sys.argv[1:] directly is fine. Anything with options, defaults or help wants argparse.
argparse
import argparse
def build_parser():
p = argparse.ArgumentParser(prog="wc", description="Count lines, words and characters.")
p.add_argument("files", nargs="*", help="input files (default: stdin)")
p.add_argument("-l", "--lines", action="store_true", help="count lines only")
p.add_argument("-n", "--top", type=int, default=10, metavar="N", help="show the top N words")
p.add_argument("--format", choices=["text", "json"], default="text")
p.add_argument("-v", "--verbose", action="count", default=0)
return p
args = build_parser().parse_args()
args.files, args.lines, args.top, args.format, args.verbose
Positional arguments are named without dashes; options with -x/--long. type=int converts and validates (a bad value gives a clean error and exit status 2); choices restricts; default fills; action="store_true" makes a flag; action="count" makes -vvv a number; nargs="*"/"+"/"?" take several or optional values. --help is generated from the help strings, and a wrong invocation prints usage to stderr and exits with 2. parse_args(argv) takes an explicit list, which is how a parser is tested without a real command line.
Sub-commands (git commit, git push) are subparsers:
sub = p.add_subparsers(dest="command", required=True)
add = sub.add_parser("add", help="add an item")
add.add_argument("name")
add.add_argument("--qty", type=int, default=1)
sub.add_parser("list", help="list items")
args.command says which ran; a set_defaults(func=handler) on each sub-parser lets main call args.func(args).
The three streams
stdin is for data in, stdout for results, stderr for messages — progress, warnings, errors. The separation is what lets tool input.txt > out.json capture results while errors still reach the terminal, and lets tool a | tool b pipe results without messages corrupting them.
print(result) # stdout
print("warning: skipped 3 lines", file=sys.stderr)
sys.exit("error: no input files") # prints to stderr and exits 1
A program that reads either files or stdin uses sys.stdin when no file is named — the - convention — so it works in a pipeline:
import fileinput
for line in fileinput.input(args.files or ("-",)): # each named file in turn, or stdin
...
Exit codes
0 success; 1 a general failure; 2 bad usage (argparse's choice); other small numbers by convention of the program. Shells, make, CI and callers test it: tool && next runs next only on success. sys.exit(n) sets it; an uncaught exception gives 1 plus a traceback, which is fine for bugs and wrong for expected failures — catch those at the top level and exit with a message:
def main(argv=None):
args = build_parser().parse_args(argv)
try:
run(args)
except FileNotFoundError as e:
print(f"error: {e.filename} not found", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
main returns the code and the guard passes it to sys.exit, so the function can be called in a test with a list of arguments and its return value checked.
Environment variables
os.environ["HOME"] reads one (KeyError if unset); os.environ.get("LOG_LEVEL", "INFO") with a default; os.getenv is the same. Configuration that should not be on the command line — credentials, paths that differ per machine — comes from the environment, with the command line overriding it and a default under both. Never print a secret from the environment; never commit a .env file.
Structure
build_parser()builds the parser;main(argv=None)parses and dispatches;run(args)or per-command handlers do the work with plain arguments; the guard is one line.- Logic never calls
sys.exitor readssys.argvdirectly — it takes values and returns or raises, so it can be imported and tested. - Output formatting is a function of the data;
--format jsonis a different formatter, not a different computation. - Progress and diagnostics go to stderr or
logging(Module 15); results go to stdout.
python -m shop.cli runs it in a package (Module 12 lesson 2); a [project.scripts] entry (lesson 4) installs it as a command.
Testing a command-line tool
Because build_parser() and main(argv) take their input as arguments, a test is a plain function call: main(["sum", "1", "2"]) returns 0, and contextlib.redirect_stdout(io.StringIO()) captures what it printed. Usage errors are testable too — construct the parser with exit_on_error=False (3.9) so bad input raises argparse.ArgumentError instead of exiting, or assert that SystemExit is raised with code 2. A tool whose logic lives in functions that never touch sys.argv, sys.exit or the real streams is one that a test suite can exercise completely without spawning a process.
Pitfalls
- Errors printed to stdout, corrupting piped output.
sys.exitfrom deep inside the logic.- Parsing
sys.argvby hand once options appear. - A script that cannot read stdin, so it cannot be used in a pipeline.
- Secrets on the command line (visible in
psand shell history). main()that takes no arguments and cannot be tested.
Key takeaways
sys.argv[1:]for trivial scripts;argparsefor options, types, defaults, choices, flags and sub-commands, with--helpfor free.- Results to stdout, messages to stderr; read stdin when no file is given so the tool works in pipelines.
- Exit
0on success,1on failure,2on usage errors;main(argv=None) -> intundersys.exit(main()). - Configuration from the environment via
os.environ.get, overridable by options; never expose secrets. - Parser,
main, and the logic are separate functions; the logic never touchesargvorexit.
Common questions
What is sys.argv in Python?
sys.argv is the list of command-line arguments as strings, split by the shell: sys.argv[0] is the script path and sys.argv[1:] are the arguments. Reading it directly is fine for one or two positional arguments; once there are options, defaults or help, use argparse.
How do I add sub-commands with argparse?
Call add_subparsers(dest="command", required=True) on the parser, then add_parser("add") for each sub-command and add its arguments to that sub-parser. args.command names the one that ran, and set_defaults(func=handler) on each sub-parser lets main dispatch with args.func(args).
Should error messages go to stdout or stderr?
To stderr. Stdout carries the program's results and stderr its messages, so tool > out.json captures results while errors still reach the terminal, and warnings never corrupt a pipe. print(msg, file=sys.stderr) writes there, and sys.exit("error: ...") prints its message to stderr and exits with status 1.
What exit code should a Python script return?
0 for success, 1 for a general failure and 2 for bad usage, which is what argparse uses. Shells, make and CI test the code, so tool && next runs next only on success. Write main(argv=None) returning the code and call sys.exit(main()) under the main guard.
Exercises
argparse from lines
Build an argparse parser with sub-commands: add name --qty N (qty defaults to 1, must be an int), remove name --force (a flag) and list. Each input line is a command line; split it with shlex.split and parse it with parse_args(argv). Print the parsed namespace as command=add name=bolt qty=3 (attributes in the order command, name, qty, force — only those present). A line the parser rejects prints usage error — construct the parser with exit_on_error=False and catch both argparse.ArgumentError and SystemExit.
Input: command lines. Output: one line per input line.
add bolt --qty 3
remove nut --force
list
add
add nut --qty x
prints
command=add name=bolt qty=3
command=remove name=nut force=True
command=list
usage error
usage errormain returns the exit code
Write main(argv) -> int around an argparse parser with one sub-command sum taking any number of tokens and a --strict flag: without --strict, non-integer tokens are skipped with a message to stderr and the sum is printed to stdout, returning 0; with --strict, the first non-integer token prints error: <token> is not an integer to stderr and returns 1 without printing a sum. A usage error returns 2. Each input line is an argv; call main(shlex.split(line)) and print exit <code>.
Input: argv lines. Output: the sums on stdout and an exit <code> line per argv.
sum 1 2 x 3
sum --strict 1 x
bogus
prints
6
exit 0
exit 1
exit 2In this module: Modules, packages and imports
- Modules and imports — files, namespaces and sys.modules
- Packages — directories, __init__.py, relative imports and project layout
- The standard library map — where to look before you write it
- Virtual environments and packaging — venv, pip and pyproject.toml
- Scripts and the command line — argv, argparse, exit codes and streams (this lesson)
- Checkpoint — Modules, packages and imports
← Virtual environments and packaging — venv, pip and pyproject.toml · Checkpoint — Modules, packages and imports →