Custom Action classes

argparse · Expert · 12 min

Some argument behaviours can't be expressed with type and the built-in actions alone.

An action is an object called each time its argument appears on the command line. Subclass argparse.Action and implement __call__:

class KeyValue(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        ...
        setattr(namespace, self.dest, result)

The arguments:

  • parser: the parser, so you can call parser.error(...),
  • namespace: the result object being filled in; read and write self.dest on it,
  • values: the already type-converted value(s), a list when nargs is set,
  • option_string: which flag was used (None for positionals).

__init__ receives the add_argument keywords (option_strings, dest, nargs, default...); override it to validate your options or fix nargs, and call super().__init__.

Important behaviours:

  • Actions run in command-line order, only when the argument appears. Defaults are set beforehand, not through your action.
  • Accumulating actions must copy before mutating: a list default is shared, so items = list(getattr(namespace, self.dest) or []).
  • Since 3.9 an action may define format_usage(); BooleanOptionalAction uses it to show --x | --no-x.

Prefer a type= function when you only convert single values; use an Action when behaviour depends on state across occurrences.

Example

import argparse

class KeyValue(argparse.Action):
    """Collect --set KEY=VALUE pairs into a dict."""
    def __call__(self, parser, namespace, values, option_string=None):
        if "=" not in values:
            parser.error(f"{option_string} expects KEY=VALUE, got {values!r}")
        key, value = values.split("=", 1)
        current = dict(getattr(namespace, self.dest) or {})   # copy, don't mutate default
        current[key] = value
        setattr(namespace, self.dest, current)

p = argparse.ArgumentParser(prog="deploy")
p.add_argument("--set", action=KeyValue, metavar="KEY=VALUE", default={})
a = p.parse_args(["--set", "region=eu", "--set", "replicas=3"])
print(a.set)                            # {'region': 'eu', 'replicas': '3'}
print(p.parse_args([]).set)             # {}
try:
    p.parse_args(["--set", "oops"])
except SystemExit:
    pass
# deploy: error: --set expects KEY=VALUE, got 'oops'

Remember

  • Subclass Action and implement __call__(parser, namespace, values, option_string)
  • Write results with setattr(namespace, self.dest, ...)
  • Copy mutable defaults before changing them

Watch out for

  • Mutating the default list or dict in place, so state leaks between parse_args calls.

Check yourself

  1. When is a custom Action's __call__ invoked?

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#action-classes