skip to content

Command-Line Interfaces with argparse

argparse turns sys.argv into a validated, self-documenting interface with types, defaults, flags and generated help. Interviewers ask for it whenever a take-home is a script.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

In argparse, what distinguishes a positional argument from an optional one?

level: juniorimportance: must knowfreq 70%

answer

  1. Look at the first character
  2. Order versus name
  3. One of them is required by default
  4. Where the parsed values land
  5. nargs="?" softens a positional

basics

~20 s

The leading dashes decide. add_argument("dataset") defines a positional argument: required, matched by its place on the command line. add_argument("--dataset") defines an optional argument: matched by name, and when absent it takes its default. Both land on the Namespace.

solid answer

~40 s

`ArgumentParser.add_argument()` branches on the first string you give it. No leading dash means a **positional** argument: it is matched by order, it is required, and the name you gave becomes the attribute name. A leading dash means an **optional** argument: it is matched by name anywhere on the line, it is not required unless you pass `required=True`, and when it is missing argparse stores its `default` (`None` if you gave none). `action="store_true"` builds a valueless flag whose default is `False`. `parse_args()` returns one `argparse.Namespace`; read it with attribute access, or `vars(args)` for a plain dict. A positional can be softened with `nargs="?"` or `nargs="*"`, and the generated `-h` lists the two kinds in separate groups, `positional arguments:` and `options:`.

code

python · 11 lines
python
import argparse

parser = argparse.ArgumentParser(prog="report")
parser.add_argument("dataset")                          # positional: required
parser.add_argument("--rows", type=int, default=6800)   # optional: has a default
parser.add_argument("--verbose", action="store_true")   # flag: default False

args = parser.parse_args(["sales", "--verbose"])
print(args)
print(args.dataset, args.rows, args.verbose)
print(vars(args))

go deeper

for a junior

Be ready to write a five-line parser from memory: one positional, one --option with a default, one store_true flag, then read them off the object parse_args() returns. Knowing that dashes make the difference is the whole answer here.

for a middle

Explain the mechanics: how the dest is derived for each kind, why a positional is required unless nargs relaxes it, and what argparse actually stores when an optional is absent. Mention that vars() turns the Namespace into a dict.

for a senior

Show judgement about the interface users will type. Positionals for the subject, options for modifiers, sane defaults so the common invocation is short, and help text that survives being read by someone at 3am debugging a failed job.

for a principal

Own the consistency story across a team's tools: which shape is a positional everywhere, whether abbreviations are allowed, and how flag names stay stable once scripts and schedulers depend on them. Renaming an option is a breaking change to every caller.

### The switch is the dash, not a keyword `argparse.ArgumentParser.add_argument()` inspects the first string you hand it. If that string begins with one of the parser's prefix characters — `-`, unless you changed `prefix_chars` — argparse builds an *optional* argument. If it does not, argparse builds a *positional* argument. Nothing else selects between the two: there is no `positional=True` keyword, and passing `required=` to a positional raises `TypeError: 'required' is an invalid argument for positionals`. Everything else about the two kinds follows from that one branch. ### What each kind implies A positional is matched by **place**. The parser consumes the non-flag words of the command line in the order the positionals were declared, and a missing one is a parse error: argparse prints the usage line plus `error: the following arguments are required: dataset` to standard error and exits with status 2. A positional is therefore *required by default*, and a `default=` on one is only reachable if you also relax `nargs`. An optional is matched by **name**, and may appear anywhere on the line, in any order relative to other optionals. Long options accept both `--rows 500` and `--rows=500`; single-letter options accept `-r500`, `-r 500` and bundling such as `-ab`. An optional is *not required by default*; when the user omits it, argparse stores whatever you passed as `default`, and `None` when you passed nothing. `required=True` is available but reads as a contradiction in the help text, where the argument still appears in the `options:` group. ### Where the value lands `parse_args()` returns a single `argparse.Namespace` — a plain attribute bag holding one entry per declared argument. You read `args.rows`, and `vars(args)` converts it to a dict, which is the idiomatic way to log the whole parse or to splat the values into a function call. The attribute name (argparse calls it the `dest`) is derived differently for the two kinds. For an optional, argparse takes the first long option string, strips the leading dashes and replaces internal hyphens with underscores, so `--dry-run` becomes `args.dry_run`. For a positional, the name you wrote **is** the dest verbatim: `add_argument("input-file")` produces a Namespace key `'input-file'`, which attribute syntax cannot reach at all. Name positionals with underscores, or pass an explicit `dest=`. ### Flags are just optionals with an action `action="store_true"` declares an optional that takes no value: present means `True`, absent means `False`, and you never write `default=False` yourself. Its mirror is `action="store_false"` (absent means `True`). `action="count"` gives `-vvv` semantics, `action="append"` collects repeats into a list, and `argparse.BooleanOptionalAction` (added in 3.9) generates the `--cache` / `--no-cache` pair from one declaration. ### Softening a positional `nargs` is how a positional stops being mandatory. `nargs="?"` accepts zero or one value and falls back to `default`; `nargs="*"` accepts any number and yields a list; `nargs="+"` accepts one or more and still errors on none. Those are the supported ways to make a positional omissible — `required=False` is rejected outright. ### Choosing between them when you design a CLI The convention users already know is: the positional is the *thing the command acts on*, and optionals are *modifiers of how it acts*. A report generator takes the dataset as a positional and `--rows`, `--since` and `--verbose` as optionals. Two or three positionals is usually the limit before order becomes a memory test; past that, name them. ### Generated help and two pitfalls Unless you pass `add_help=False`, argparse installs `-h/--help` for you and groups the two kinds under their own headings. The heading for optionals was renamed from `optional arguments:` to `options:` in Python 3.10, which is the difference you will notice comparing help output across versions. Two behaviours surprise people. First, `allow_abbrev` defaults to `True`, so any unambiguous prefix of a long option is accepted: `--verb` works when `--verbose` is the only match. Pass `allow_abbrev=False` if you want strict names. Second, a value that itself starts with a dash confuses the matcher — `--rows -1` looks like two options — so write `--rows=-1`. When you need the leftovers rather than an error on unknown words, `parse_known_args()` returns a `(namespace, remaining_list)` tuple instead of failing.

  • How do you make a positional argument that the user may omit?
    Relax `nargs`, not `required`. `nargs="?"` accepts zero or one value and falls back to `default`; `nargs="*"` accepts any number and yields a list, so `default=[]` is the sane pairing. Passing `required=False` to a positional is not supported — argparse raises `TypeError: 'required' is an invalid argument for positionals`.
  • Where does the Namespace attribute name for --dry-run come from, and when does that rule not apply?
    For an optional, argparse takes the first long option string, strips the leading dashes and replaces internal hyphens with underscores, so `--dry-run` becomes `args.dry_run`. Positionals are different: the name you passed is the dest verbatim, so `add_argument("input-file")` yields the key `'input-file'`, unreachable by attribute access. Name positionals with underscores or pass `dest=`.
  • When is required=True on an optional argument a design smell?
    When the argument is really the subject of the command. `report --dataset sales` with `required=True` forces users to type a name that carries no choice, and `-h` still shows it under `options:`, which reads as untrue. Make it a positional instead. `required=True` earns its place only for a genuinely modal input, such as an output target that has no safe default.

saying these in an interview costs you the question

  • Says a required= keyword is what makes an argument positional
  • Thinks a positional can never be omitted, ignoring nargs
  • Expects args.dry-run to work as attribute access
  • Believes a store_true flag defaults to None
  • Claims optional arguments must be given in declaration order
  • Reads sys.argv by hand after calling parse_args

context

open as a page

How do you give an argparse CLI git-style subcommands and dispatch to the right handler?

level: middleimportance: should knowfreq 45%

basics

~10 s

Call parser.add_subparsers(dest="command", required=True), then add_parser("build") on the object it returns for each subcommand, giving each its own arguments. Attach a handler with set_defaults(func=build) so parse_args() leaves args.func ready to call.

open as a page

Why does argparse store the value of --retries 3 as a string, and what changes it?

level: middleimportance: should knowfreq 55%

basics

~20 s

A command line is text and argparse converts nothing unless asked. Adding type=int to add_argument("--retries") makes argparse call int() on the supplied value, so parse_args() hands back 3 instead of "3". Without type=, every parsed value stays a str.

open as a page

Why does argparse replace an exception raised inside a custom type= callable with 'invalid value'?

level: seniorimportance: should knowfreq 30%

basics

~20 s

argparse calls the converter inside a try block: ValueError, TypeError and ArgumentTypeError become a parse error, printing the usage line and one message to standard error and exiting with status 2. Raise argparse.ArgumentTypeError to control that message.

open as a page