skip to content

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

level: middleimportance: should knowfreq 55%

answer

  1. The shell hands over text only
  2. One keyword does the conversion
  3. It is a callable, not a type name
  4. Defaults follow a special rule
  5. choices are checked after conversion

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.

solid answer

~40 s

argparse receives strings, and `type=` is the hook that converts them. It takes **any single-argument callable** — `int`, `float`, `pathlib.Path`, `str.lower`, or a function of your own — and argparse calls it once per supplied value, so with `nargs` you get a converted list. Two details matter. First, `type=` is also applied to a `default` **when the default is a string**; a non-string default is stored untouched, which is why `default=6800` and `default="6800"` both end up as the integer with `type=int`. Second, `choices=` is checked *after* conversion, so the members of `choices` must be of the converted type. `action="store_true"` consumes no value, so `type=` is meaningless there — and `type=bool` is the classic trap, because `bool("False")` is `True`. Use `store_true` or `argparse.BooleanOptionalAction`.

code

python · 9 lines
python
import argparse

parser = argparse.ArgumentParser(prog="report")
parser.add_argument("--rows", type=int, default="6800")
parser.add_argument("--ids", nargs="*", type=int, default=[])
parser.add_argument("--mode", type=str.lower, choices=["fast", "full"], default="fast")

print(parser.parse_args([]))
print(parser.parse_args(["--rows", "500", "--ids", "1", "2", "--mode", "FULL"]))

go deeper

for a junior

Remember that everything off the command line is a str until you pass type=. Reach for type=int on counts and type=float on thresholds, and use action="store_true" rather than trying to parse a boolean word yourself.

for a middle

Explain the mechanics: type= is any one-argument callable, it runs per value so nargs yields a converted list, it also converts a string default, and choices is compared after conversion. Know why type=bool misbehaves.

for a senior

Show that validation belongs in the converter, so bad input produces the same usage-plus-message-plus-exit-2 shape as every other argparse error rather than a bespoke check further down the program.

for a principal

Own the boundary: which invariants belong in argument parsing and which belong in the domain code, so a CLI, a scheduled job and a library entry point validate the same inputs once instead of three times with different messages.

### Everything arrives as text The operating system hands a process its command line as a list of strings. argparse does not guess: with no `type=`, a value is stored exactly as typed, so `--retries 3` gives you `'3'`, and `'3' * 2` is `'33'` rather than `6`. That silent string is behind a large share of the bugs in hand-rolled scripts, because `if args.retries > 0` raises `TypeError` in Python 3 rather than quietly comparing. ### type= is a callable, not a type name The `type=` keyword of `add_argument()` accepts any callable taking one string and returning the value you want stored. `type=int` and `type=float` are the common cases, but `type=pathlib.Path` gives you a path object, `type=str.lower` normalises case, and a function of your own can parse a date, a duration or a key=value pair. Because it is an ordinary callable, argparse calls it once **per value**, which composes with `nargs`: `add_argument("--ids", nargs="*", type=int)` yields a list of integers, not a list of strings and not one integer. ### The rule about defaults, stated precisely argparse applies `type=` to a `default` only when that default is a string. If the argument is missing from the command line and the stored default is still the string you wrote, argparse runs it through the converter, so `default="6800"` with `type=int` produces `6800`. A default that is already a non-string — `6800`, `None`, `[]`, a `Path` — is stored as-is, untouched. The practical guidance is to write the default in its final type (`default=6800`) so no conversion has to happen at all, and to remember that `None` is the default default: without `default=`, an absent optional lands as `None`, and code that does arithmetic on it fails at the first use rather than at parse time. ### choices, and the order of operations `choices=` is a membership test performed **after** the type conversion, on the converted value. So `add_argument("--n", type=int, choices=[1, 2, 6800])` must list integers, not strings; listing `["1"]` would never match. That ordering is also what makes `type=str.lower` with `choices=["fast", "full"]` accept `--mode FULL`: the value is lowered first, then checked. A rejected choice becomes a parse error naming the allowed set, printed with the usage line to standard error, exit status 2. ### Where type= does not apply Actions that consume no value never call the converter. `action="store_true"` and `"store_false"` are the obvious cases; `action="count"` is another. Passing `type=` alongside them is dead configuration at best. The most common mistake in this area is reaching for `type=bool` to build a switch: `bool` is a perfectly valid callable, so argparse accepts it — and then `--force False` stores `True`, because `bool` on a non-empty string is `True` for every string except the empty one. There are two correct answers: `action="store_true"` for a plain on-switch, and `argparse.BooleanOptionalAction` (added in Python 3.9) when you want an explicit off-switch too, which generates `--cache` and `--no-cache` from a single declaration and honours `default=True`. ### Errors raised inside the converter When the callable raises `ValueError` or `TypeError` — as `int("many")` does — argparse turns that into a parse error rather than a traceback, printing `error: argument --retries: invalid int value: 'many'`. That is a deliberate design choice: a user typo should produce a usage message, not a stack trace. It also means the converter's own error text is discarded unless you raise `argparse.ArgumentTypeError`, whose message argparse prints verbatim. ### A checklist for a converter you write yourself Make it a named module-level function so the generated error message reads well (argparse uses the callable's `__name__` in `invalid <name> value`). Make it total: given any string, either return a value or raise. Keep it free of side effects — no file writes, no network — because argparse may call it while building an error message path, and because a converter that mutates state is untestable. Do validate cheap invariants there (a positive count, an ISO date, an existing directory) rather than after `parse_args()`: failing inside the converter gives you argparse's uniform error format and exit status for free, while failing afterwards means you write your own message, your own exit code, and eventually two inconsistent styles in the same tool.

  • If an argparse argument has default="6800" and type=int, what is on the Namespace when the user omits it?
    The integer `6800`. argparse runs `type=` over a default that is still a string when the argument was not supplied, so a string default is converted exactly like a typed value. A non-string default — `6800`, `None`, `[]` — is stored untouched. Writing the default in its final type avoids the question entirely.
  • How do you validate an argument's range or format without writing checks after parse_args()?
    Do it in the `type=` callable. A function that returns the converted value or raises `argparse.ArgumentTypeError("batch size must be positive")` gets argparse's uniform treatment: usage line, one-line message on standard error, exit status 2. Validating after `parse_args()` means hand-rolling the message and the exit code, and the two styles drift apart as the tool grows.

saying these in an interview costs you the question

  • Assumes argparse infers int or float from the value
  • Uses type=bool to build an on/off switch
  • Thinks type= converts non-string defaults too
  • Puts strings in choices alongside type=int
  • Believes type= runs once for the whole nargs list
  • Calls int() on every Namespace value after parsing

context