Chapters
Python114 chapters

Beyond the basicsChapter 97 of 114

Command-line Arguments

Turn a script into a proper tool with argparse.

sys.argv is the raw version

Everything typed after the script name, as strings:

Python
# greet.py
import sys

print(sys.argv)

Output, from a real run elsewhere

['greet.py', 'Ada', '--loud']

It works, and it leaves you to do everything: check the count, convert types, handle flags in any order, and write the help text. argparse does all of that.

A parser in four lines

Python
import argparse

parser = argparse.ArgumentParser(description="Greet someone.")
parser.add_argument("name", help="who to greet")

args = parser.parse_args(["Ada"])
print(args.name)

Output

Ada

Passing a list to parse_args is how you test a parser, and how these examples run. In a real script you call parser.parse_args() with nothing and it reads sys.argv.

Positional and optional

A name without dashes is required and positional. With dashes it is optional:

Python
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("name")
parser.add_argument("--greeting", default="Hello")
parser.add_argument("--times", type=int, default=1)

args = parser.parse_args(["Ada", "--times", "2"])
for _ in range(args.times):
    print(f"{args.greeting}, {args.name}")

Output

Hello, Ada
Hello, Ada

type=int converts for you, and rejects anything that will not convert — so the rest of your program never sees a bad value.

Flags

action="store_true" makes an option that takes no value:

Python
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--loud", action="store_true")
parser.add_argument("-n", "--dry-run", action="store_true")

args = parser.parse_args(["--loud"])
print(args.loud, args.dry_run)

args = parser.parse_args(["-n"])
print(args.loud, args.dry_run)

Output

True False
False True

Note --dry-run becomes args.dry_run. Dashes turn into underscores.

Choices, counts and lists

Python
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["fast", "safe"], default="safe")
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument("--tag", action="append", default=[])
parser.add_argument("files", nargs="*")

args = parser.parse_args(["--mode", "fast", "-vv", "--tag", "a", "--tag", "b", "x.txt", "y.txt"])
print(args.mode, args.verbose, args.tag, args.files)

Output

fast 2 ['a', 'b'] ['x.txt', 'y.txt']

choices validates for you, count gives the familiar -v/-vv verbosity, append collects repeats, and nargs="*" takes whatever is left.

It validates, and it explains

A bad value produces a message and a non-zero exit, without you writing any of it:

Python
import argparse
import io
from contextlib import redirect_stderr

parser = argparse.ArgumentParser(prog="tool")
parser.add_argument("--mode", choices=["fast", "safe"])

complaint = io.StringIO()
try:
    with redirect_stderr(complaint):
        parser.parse_args(["--mode", "sideways"])
except SystemExit as problem:
    print("exited with", problem.code)

message = complaint.getvalue()
print("invalid choice" in message, "sideways" in message)

Output

exited with 2
True True

The exact wording shifts between Python versions, so the example checks the message rather than quoting it. What matters is that you wrote no error handling and still got a precise complaint plus a non-zero exit.

argparse calls sys.exit(2) on a usage error, which is the conventional code. The message goes to stderr.

Help comes free

Python
import argparse

parser = argparse.ArgumentParser(prog="greet", description="Greet someone.")
parser.add_argument("name", help="who to greet")
parser.add_argument("--times", type=int, default=1, help="how many times")

print(parser.format_help().strip())

Output

usage: greet [-h] [--times TIMES] name

Greet someone.

positional arguments:
  name           who to greet

options:
  -h, --help     show this help message and exit
  --times TIMES  how many times

-h was added for you, and the text is generated from the help= strings. This is the main reason to use argparse over hand-rolling.

Subcommands

For a tool with several verbs, like git commit and git push:

Python
import argparse

parser = argparse.ArgumentParser(prog="tool")
subs = parser.add_subparsers(dest="command", required=True)

add = subs.add_parser("add", help="add an item")
add.add_argument("item")

remove = subs.add_parser("remove", help="remove an item")
remove.add_argument("item")
remove.add_argument("--force", action="store_true")

args = parser.parse_args(["remove", "notes.txt", "--force"])
print(args.command, args.item, args.force)

Output

remove notes.txt True

Each subcommand gets its own arguments and its own help.

The usual shape

Python
import argparse

def main():
    parser = argparse.ArgumentParser(description="What this tool does.")
    parser.add_argument("path")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()

    if args.dry_run:
        print("would process", args.path)
        return 0

    print("processing", args.path)
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

Putting the work in main() and returning an exit code keeps the script importable and testable, and raise SystemExit(main()) passes the code to the shell.

Test yourself

2 questions

What does action="store_true" give you?

Show the answer

A flag that takes no value — And --dry-run becomes args.dry_run, because dashes turn into underscores.

What does argparse do on a usage error?

Show the answer

Prints a message to stderr and exits with code 2 — You wrote no error handling and still got a precise complaint plus the conventional exit code.

Next chapter

Debugging

Read a traceback, then find the problem with a debugger instead of guessing.