How argparse parses edge cases

argparse · Expert · 12 min

Odd inputs (values starting with '-', abbreviations, greedy nargs) cause the most confusing CLI bugs.

argparse first classifies each word as an option (O) or argument (A), then matches patterns of these against your arguments using regular expressions over the O/A string. Several rules follow from that:

  • Prefix matching: --verb is accepted for --verbose if unambiguous. That can break scripts when you later add --verbatim. Disable with allow_abbrev=False.
  • Negative numbers: -5 is treated as a value, not an option, unless the parser has options that look like negative numbers (such as -1). Then users must write --offset=-5.
  • The `--` separator: everything after -- is positional, even if it starts with a dash, so rm -- -weird-file works.
  • Values starting with `-`: --pattern -x fails because -x looks like an option; use --pattern=-x.
  • Greedy nargs: positionals are matched greedily left to right, so nargs="*" followed by another positional can steal values.
  • Intermixing: by default, positionals with nargs must be contiguous; parse_intermixed_args() allows options in between.

When in doubt, print parser.parse_args(argv) for a list of tricky inputs and add them to your tests.

Example

import argparse

p = argparse.ArgumentParser(prog="calc")
p.add_argument("--verbose", action="store_true")
p.add_argument("--offset", type=int, default=0)
p.add_argument("--pattern")
p.add_argument("files", nargs="*")

print(p.parse_args(["--verb", "--offset", "-5"]))       # prefix match; -5 is a value
# Namespace(verbose=True, offset=-5, pattern=None, files=[])
print(p.parse_args(["--pattern=-x", "--", "-file.txt"]).files)   # ['-file.txt']

strict = argparse.ArgumentParser(prog="strict", allow_abbrev=False)
strict.add_argument("--verbose", action="store_true")
try:
    strict.parse_args(["--verb"])
except SystemExit:
    pass
# strict: error: unrecognized arguments: --verb

q = argparse.ArgumentParser()
q.add_argument("cmd"); q.add_argument("--n"); q.add_argument("rest", nargs="*")
print(q.parse_intermixed_args(["go", "a", "--n", "1", "b"]))
# Namespace(cmd='go', n='1', rest=['a', 'b'])

Remember

  • Long options can be abbreviated unless allow_abbrev=False
  • Use --opt=-value or -- for values that start with a dash
  • parse_intermixed_args allows options between positional values

Watch out for

  • Shipping a CLI with prefix matching on, then breaking users' abbreviations by adding a new option.

Check yourself

  1. How can a user pass the value '-x' to --pattern?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/argparse.html#allow-abbrev

Builds on

Unlocks

Nothing builds on this yet.