Why does argparse replace an exception raised inside a custom type= callable with 'invalid value'?
answer
- Bad input should not print a traceback
- Three exception classes are absorbed
- One exception type keeps your wording
- Failure leaves through a status, not a return
- It is not an Exception subclass
basics
~20 sargparse 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.
solid answer
~50 sargparse deliberately turns bad input into a usage message rather than a traceback. When the `type=` callable raises `ValueError` or `TypeError`, argparse discards the exception and prints `error: argument --since: invalid <callable name> value: '...'`, then exits with status 2 through `SystemExit`. The message you wanted is only preserved if you raise `argparse.ArgumentTypeError`, whose text argparse prints verbatim. Any other exception class propagates normally, with a full traceback — which is the right signal for a bug in your converter as opposed to bad user input. If you need to handle the failure in code instead, build the parser with `exit_on_error=False` and argument-level failures raise `argparse.ArgumentError` instead of exiting. And because `SystemExit` derives from `BaseException`, a broad `except Exception` around your entry point will not swallow it — but a bare `except:` will, turning a rejected command line into a silent success.
code
python · 17 linesimport argparse
import datetime
def iso_date(text):
try:
return datetime.date.fromisoformat(text)
except ValueError:
raise argparse.ArgumentTypeError(f"{text!r} is not an ISO date (YYYY-MM-DD)")
parser = argparse.ArgumentParser(prog="report")
parser.add_argument("--since", type=iso_date)
parser.add_argument("--rows", type=int, default=6800)
try:
parser.parse_args(["--since", "2026-13-01"])
except SystemExit as exc:
print("exit status:", exc.code)go deeper
Know that argparse answers bad input with a usage message and a non-zero exit rather than a stack trace, and that -h ends the program too. Reading the error line and fixing the command is the skill expected here.
Explain the mechanics: which exception classes argparse absorbs from a type= callable, that ArgumentTypeError's text is printed verbatim while a ValueError message is discarded, and that failures exit with status 2.
Demonstrate operational judgement: reserve exit 2 for usage errors, never wrap an entry point in a bare except that swallows SystemExit, and decide whether a given check belongs in the converter or after parsing.
Own the failure contract across the team's tools — which statuses mean what, where errors are written, and how a scheduled run distinguishes a bad invocation from a genuine failure, so alerting and retries can be built on top of it.
### What argparse does around the converter When an argument declares `type=some_callable`, argparse invokes that callable on each supplied string inside a guarded block. Three exception classes are treated as *user error*: `argparse.ArgumentTypeError`, `TypeError` and `ValueError`. Any of them is converted into a parse error, which means `parser.error()` runs: the usage line goes to standard error, followed by one line of the form `prog: error: argument --since: invalid iso_date value: '2026-13-01'`, and the process exits with status 2. Note where the text came from — the callable's `__name__` and the offending string, not the exception's own message, which is thrown away. That explains the symptom people report as *the exception disappeared*. Consider a nightly report generator whose `--since` option converts an ISO date and whose `--rows` option defaults to a 6,800-row batch. A mistyped date raises `ValueError` inside `datetime.date.fromisoformat`, and the operator sees only `invalid since value`, with nothing about months running to twelve. The information was not lost by accident; argparse discarded it on purpose, because a traceback is the wrong output for a typo. ### Getting your message back Raise `argparse.ArgumentTypeError` with the text you want, and argparse prints it verbatim after the argument name. A converter that catches the low-level `ValueError` and re-raises `ArgumentTypeError(f"{text!r} is not an ISO date (YYYY-MM-DD)")` gives operators the format they need while keeping argparse's uniform shape: usage line, one message, exit 2. That uniformity is worth protecting — every failure in the tool looks the same to a human and to a scheduler. ### The exceptions argparse does not catch Only those three classes are absorbed. If your converter raises `KeyError`, `OSError` or anything else, it propagates out of `parse_args()` with a full traceback. This is a useful split rather than an inconsistency: `ValueError` from a parse attempt means the *input* was bad, while an unexpected class almost always means the *converter* is broken, and a bug deserves a traceback. It also means a converter that touches the filesystem or the network can fail in ways that bypass the tidy error path entirely — one more reason to keep converters pure. ### Handling errors instead of exiting `ArgumentParser(exit_on_error=False)` makes argument-level failures raise `argparse.ArgumentError` instead of calling `parser.error()`, which is what you want when the parser is embedded — a REPL command, a chat bot, a test that asserts on the message rather than on an exit status. Treat it as covering the argument-level paths rather than every possible parse failure, and keep a fallback for `SystemExit` if the calling context truly must never exit. ### The swallowed-exit trap `parse_args()` signals both failure and `--help` by raising `SystemExit` — status 2 for a parse error, status 0 after printing help. `SystemExit` inherits from `BaseException`, **not** `Exception`, and that asymmetry decides who catches it. A wrapper written as `except Exception:` around your entry point leaves it alone, which is correct. A wrapper written as a bare `except:` — or `except BaseException:` — swallows both: `--help` prints and then execution continues past it, and a rejected command line is converted into a zero exit status. In a nightly job that is genuinely dangerous, because the scheduler reads exit 0 as success and no report is produced; the failure surfaces days later as missing data rather than as a red run. If you must wrap `main()` for logging, catch `Exception`, and let `SystemExit` through — or catch it explicitly and re-raise after logging `exc.code`. ### Exit-status discipline Argparse's own convention is: 0 for success including `--help`, 2 for a usage error. Keep your own failures off 2 so callers can tell a bad command line from a failed run, and pick a small stable set — 1 for a general failure is the common choice. Where you validate matters here too: validation done inside a `type=` callable inherits the exit-2 usage path automatically, while validation done after `parse_args()` is yours to number, message and document. Deciding that boundary once, for the whole tool, is what stops a CLI from accumulating three different failure styles.
- Which exit status does argparse use for a rejected command line, and why does that number matter?Status 2, with the usage line and one message on standard error; `--help` exits 0. Keeping 2 reserved for usage errors lets a scheduler or wrapper script distinguish a malformed invocation from a run that started and failed, so pick a different code — 1 is conventional — for your own runtime failures.
- A wrapper of the form try: main() except: log_and_return() hides both --help and parse errors. Why?Both leave `parse_args()` as `SystemExit`, which inherits from `BaseException` rather than `Exception`. A bare `except:` catches `BaseException`, so help output is printed and then execution continues, and a rejected command line ends with exit status 0 — a scheduled job reports success while producing nothing. Catch `Exception`, or re-raise `SystemExit` after logging its code.
- When would you build the parser with exit_on_error=False?When the parser is embedded in something that must not exit: an interactive command loop, a long-lived process parsing user-supplied lines, or a test that asserts on the error message. Argument-level failures then raise `argparse.ArgumentError`, which you catch and render yourself. Keep a `SystemExit` guard as well if the host truly can never terminate.
saying these in an interview costs you the question
- Expects a traceback from the converter to reach the user
- Thinks except Exception catches an argparse parse failure
- Raises ValueError expecting its message to be shown
- Believes a parse error exits with status 1
- Wraps main in a bare except and reports success
- Assumes every exception from a converter is absorbed