cmd2.annotated
cmd2.annotated
Build argparse parsers from type-annotated function signatures.
Experimental
This module is experimental and its behavior may change in future releases.
The with_annotated decorator inspects a command function's type hints and
default values to build a Cmd2ArgumentParser. Argument and
Option metadata classes give finer per-parameter control via
typing.Annotated.
Parameters without defaults become positional arguments; parameters with defaults
become --option flags; keyword-only parameters (after *) are always options.
A bool option is a flag, not a value: when absent it means False (or None
for bool | None), so it defaults to that and is never required. A *args
parameter becomes a variadic positional accepting zero or more values (nargs='*'),
collected into a tuple. Underscores in a parameter name become dashes in the generated
flag (dry_run -> --dry-run); pass an explicit Option("--my_flag") to opt out.
Positional-only parameters (before /) and **kwargs raise TypeError. The parameter
names dest and subcommand are reserved; cmd2_statement receives the parsed
Statement and (with base_command=True) cmd2_subcommand_func receives the subcommand handler:
class MyApp(cmd2.Cmd):
@cmd2.with_annotated
def do_greet(self, name: str, count: int = 1, loud: bool = False):
for _ in range(count):
msg = f"Hello {name}"
self.poutput(msg.upper() if loud else msg)
Use Annotated with Argument or Option for finer
control over individual parameters:
from typing import Annotated
class MyApp(cmd2.Cmd):
def color_choices(self) -> cmd2.Choices:
return cmd2.Choices.from_values(["red", "green", "blue"])
@cmd2.with_annotated
def do_paint(
self,
item: str,
color: Annotated[str, Option("--color", "-c", choices_provider=color_choices, help_text="Color to use")] = "blue",
):
self.poutput(f"Painting {item} {color}")
How annotations map to argparse settings:
str-- default string argumentint,float-- setstype=booloption ----flag / --no-flagviaBooleanOptionalAction; defaults toFalse(orNoneforbool | None) when omitted, so it is neverrequired- positional
bool-- parsed fromtrue/false,yes/no,on/off,1/0 pathlib.Path-- setstype=Pathenum.Enumsubclass --type=converter,choicesfrom member values- a union of Enums (e.g.
EnumA | EnumB) -- each member keeps its own converter; a token resolves to the first member that accepts it, and the mergedchoicesare the concatenation of each member's choices decimal.Decimal-- setstype=DecimalLiteral[...]--type=converterandchoicesfrom the literal valueslist[T]/set[T]/frozenset[T]/tuple[T, ...]--nargs='+'(or'*'with a default or| None)tuple[T, T](fixed arity, same type) --nargs=Nwithtype=T*args: T-- variadic positional (nargs='*');Tis each value's type, not the collected tuple.Annotated[T, Argument(...)]metadata is honoredT | None(no default) -- positional withnargs='?'(0-or-1 tokens)T | None = None----flagoption withdefault=None
A value option with no default is made required (omitting it would pass None,
violating a non-Optional hint); annotate it T | None or give it a default to make it
omittable.
Explicit Option(action=...) is type-checked so the parsed result matches the
declared type:
store_true/store_false-- require aboolparameter (type=is dropped; argparse supplies theFalse/Truedefault)count-- requires anintparameter; defaults to0(Noneforint | None)append/extend-- require alist[T]parameter and default to[](appendtakes one value per flag;extendtakesnargsvalues per flag)store_const/append_const-- store theOption(const=...)value (type=is dropped). The action is inferred from the type whenaction=is omitted: a scalarOption(const=X)becomesstore_const(present ->const, absent -> the default, which must exist or beT | None); alist[T]Option(const=X)becomesappend_const(each flag appendsconst; defaults to[]). A scalarOption(const=X)given an explicitnargs(e.g.nargs='?') instead keeps thestoreaction for argparse's optional-value idiom (absent -> default, bare flag ->const,flag VALUE-> convertedVALUE); theconstis stored verbatim and must match the declared type.constis validated against the declared type and is rejected on a positionalArgument(argparse ignores it there)- a custom
argparse.Actionsubclass -- passed straight through toadd_argument. The user's class owns storage, so the collection-casting wrapper is dropped and the action-specific type/const/collection-shape constraints are skipped. The type-inferred converter, default, andrequiredare still applied; the action receives them like any hand-builtadd_argumentcall.action='help'andaction='version'are not supported.
The zero-argument actions above (store_true / store_false / count / store_const /
append_const) take no value from the command line, so the value-oriented metadata inferred from
the type is dropped before add_argument is called: the type= converter, the static
choices, and any inferred tab-completer (e.g. the path completer for Path). There is nothing
to complete or convert on a value-less action. A completer that was only inferred from the type is
dropped silently, but a completer / choices_provider you supply explicitly on such an action
is a contradiction and raises TypeError (matching argparse, which rejects it outright). Actions
that do consume values (append / extend on a list[T], or a plain value option) keep the
inferred converter and completer unchanged.
The metadata classes refuse a handful of add_argument kwargs that the decorator derives from the
signature itself, so passing them through Argument(...) / Option(...) raises TypeError:
type (from the annotation), dest (from the parameter name), help (use the help_text
parameter, which maps to it -- a raw help would silently shadow it), and -- on Argument only
-- action / required (which have no meaning on a positional). Every other add_argument
parameter, including those registered via
register_argparse_argument_parameter, passes through unchanged.
A default may be supplied either as the function-signature default (param: T = v) or as
Argument(default=v) / Option(default=v) -- the two forms are equivalent. Specifying both at
once raises TypeError (the value would have two sources of truth), and argparse.SUPPRESS is
rejected as a default from either source because it would remove the keyword argument the function
expects.
Parser-level customization is forwarded to Cmd2ArgumentParser's constructor via PEP
692 **parser_kwargs: Unpack[Cmd2ParserKwargs]. Anything the parser ctor accepts -- description,
epilog, prog, usage, parents, argument_default, prefix_chars,
fromfile_prefix_chars, conflict_handler, add_help, allow_abbrev, exit_on_error,
formatter_class, completer_class, and on Python >= 3.14 suggest_on_error / color --
flows straight through; the Cmd2ParserKwargs TypedDict is the single source of truth
and gives type-checkers/IDEs autocomplete on the decorator's call site. parser_class stays as
its own explicit kwarg because it selects the class itself, not a value passed to it. Two
behaviors layer on top of the raw passthrough: if description is omitted, the first paragraph
of func.__doc__ (up to the first blank line) is used so docstrings double as help text without
leaking :param: directives; and prog is rejected with subcommand_to because cmd2
rewrites it from the parent command's hierarchy. Mutually exclusive groups accept
Group(required=True) to require exactly one member; the same flag on a plain groups= entry
raises ValueError (argparse's add_argument_group has no required). Give a
mutually_exclusive_groups entry a title/description to render it as a titled help section
(argparse's one supported nesting -- a mutex inside an argument group), and use
Option(action='store_true') for any bool member so the mutex reads as [--foo | --bar]
instead of expanding to --no-* variants. To put non-mutex parameters in the same section, list
its members in a groups= entry instead and leave the title off the mutex; declaring the section in
both places, a mutex that sits only partly in a groups= entry, or one that spans two of them all
raise ValueError. The other three nesting directions (an argument group in an argument group or
in a mutex, and a mutex in a mutex) are removed in argparse on Python 3.14 and cannot be expressed
here. These group-spec rules (and member references, double-assignment, and the required=True
rejection) are checked at decoration time from parameter names alone -- type hints are not resolved,
so forward-referenced annotations still decorate -- meaning a misconfigured group raises when the
class is defined rather than on first command use. The one group rule that needs the annotations
(a required member in a mutually exclusive group) fires when the parser is built.
A parameter annotated with a @dataclass that subclasses ArgumentBlock is a
reusable argument block: each of the dataclass's init fields is expanded into a flat command-line
argument (field name == argument name) at the parameter's position, and the parsed values are
reconstructed into a dataclass instance passed to the command. This lets several commands share one
block without duplicating parameters, and a base block shared by inheritance is the reuse mechanism
(a subclass of a block is itself a block):
@dataclass
class CommonArgs(cmd2.ArgumentBlock):
verbose: Annotated[bool, Option("-v", "--verbose")] = False
output: Annotated[Path | None, Option("--output")] = None
class MyApp(cmd2.Cmd):
@cmd2.with_annotated
def do_build(self, target: str, common: CommonArgs):
if common.verbose:
self.poutput(common.output)
The ArgumentBlock trait -- not "is a dataclass" -- is the trigger, so a plain
@dataclass is never expanded and can still be used as an ordinary single argument value (e.g. with an
Argument(converter=...)). Each field carries the usual Annotated[T, Option(...)] /
Annotated[T, Argument(...)] metadata and behaves exactly as a top-level parameter of the same shape
(type inference, completion, choices). The dataclass is the single source of truth for defaults: a field
with a default (default or default_factory) is emitted with argparse.SUPPRESS and filled by the
dataclass constructor at reconstruction time, so a default_factory yields a fresh value per invocation
(no shared-mutable default) and __post_init__ runs. A field with no default becomes a required
argument. A field whose type is itself a block is not expanded (no recursion) and raises the
unsupported-type error. Because fields expand flat, every field name must be unique across the command's
own parameters and every other block's fields -- a collision (which would put two argparse actions on one
namespace dest) raises TypeError when the parser is built. Block fields cannot participate in
groups= / mutually_exclusive_groups= (those reference the function's own parameter names, which are
validated without resolving type hints). A block must be the bare annotation of a regular parameter:
wrapping it in Annotated/Optional/a union, or using it as *args / **kwargs, raises
TypeError (to use a dataclass as a single value instead, make it a plain @dataclass with an
Argument(converter=...)). An ArgumentBlock subclass that is not a @dataclass has no fields and
also raises TypeError.
Unsupported patterns (raise TypeError):
- a non-Optional type with a
Nonedefault (e.g.name: str = None); annotate itT | Noneor use a non-None default.Any/object/unannotated are exempt - a scalar type with no converter (e.g.
datetime.datetime,uuid.UUID,bytes, or any custom class), which would silently arrive as a plain string. Supported scalars arestr,int,float,bool,decimal.Decimal,pathlib.Path,enum.Enumsubclasses, andLiteral[...](str/Any/objectpass through raw) str | int-- a union of multiple non-None types is ambiguous (unless every member is anenum.Enumsubclass, which resolves by trying each member's converter in turn)tuple[int, str, float]-- mixed element types (argparse applies onetype=per argument)*args: tuple[T, ...](or any collection element) -- the annotation is each value's type, so a collection element means a tuple-of-collections; annotate the element, e.g.*args: str*args: Annotated[T, Option(...)]--*argsis always positional; useArgument()*args: Annotated[T, Argument(nargs=N)]--*argsarity is fixed tonargs='*'- a keyword-only parameter annotated with
Argument()-- it marks a positional; useOption() - a required option (no default, not
T | None) in amutually_exclusive_groupsgroup -- only one member is supplied, so the others arrive asNone; give it a default orT | None Annotated[T, Argument(nargs=N)]producing a list ('*','+', integer>= 1) on a non-collectionT; uselist[T]ortuple[T, ...]to match the runtime shapeAnnotated[tuple[T, T], Argument(nargs=N)]whereNdiffers from the tuple's arityOption(action=...)whose result type mismatches the declared type, an unsupported action, or a non-list action on a collection (useappend/extend/append_constwithlist[T])- a variable-arity positional (
T | None,list[T],tuple[T, ...]) followed by another positional -- it must come last (def f(self, a: str, *rest: str)is fine)
When combining Annotated with Optional, the union should go inside:
Annotated[T | None, meta]. Annotated[T, meta] | None is ambiguous and raises -- unless the
inner type already carries the None (Annotated[T | None, meta] | None), in which case the
redundant outer | None is accepted as equivalent to Annotated[T | None, meta].
Path and Enum annotations also get automatic tab completion. A user-supplied
choices_provider or completer drives completion in place of the inferred static
choices, while the inferred type converter is kept so values still coerce to the
declared type (an Enum to its member, Literal[1, 2] to int) and out-of-type
values are rejected at parse time. An Enum accepts both member values and member names on the
command line (completion and --help show the values).
An explicit choices= is reconciled with the inferred type rather than fighting it: its values are
run through the inferred type converter so they match argparse's post-conversion comparison
(Annotated[int, Option('--n', choices=['1', '2'])] becomes choices=[1, 2], so --n 1
matches; a value the converter rejects is a build-time TypeError), and an explicit choices=
takes precedence over a type-inferred completer (the Path completer is dropped so the choices
drive both validation and completion). A choices_provider / completer you supply yourself
still wins over choices=.
Two hooks customize the string -> value conversion, parity with a hand-built add_argument(type=...)
(a raw type= in the metadata is rejected; use these instead):
converter-- aCallable[[str], Any]that replaces the inferredtype=converter. Because the converter owns the conversion, the annotation is no longer required to be a supported scalar -- any type is legal (Annotated[datetime, Argument(converter=parse_iso)]), and the "unsupported type" error is suppressed. The inferredchoicesand completer (which described the inferred value-space) are dropped; supplychoices=/completer/choices_providerto re-add them (an explicitchoices=is still run through your converter). argparse applies it per token, so on alist[T]it converts each value; a non-collection annotation such asAnykeeps a single token, so the converter may itself return a collection (Annotated[Any, Option('--idx', converter=parse_intset)]).preprocess-- aCallable[[str], str]that runs before the inferred converter, transforming the raw token while keeping the inferredtype=,choices, completer, and coercion. Use it to normalize input for a type that already has rich inference, e.g.Annotated[Color, Argument(preprocess= str.lower)]acceptsREDwhile still showing theColorchoices, orAnnotated[Path, Argument(preprocess=os.path.expanduser)]keeps the path completer. With a plainstr(no inferred converter) it becomes thetype=directly.
converter and preprocess are mutually exclusive on one parameter (fold the preprocessing into the
converter, which already receives the raw token), and neither may be combined with a value-less action
(store_true / store_false / count / store_const / append_const), which consumes no
token to convert.
NS_ATTR_BASE_ARGS
module-attribute
NS_ATTR_BASE_ARGS = constants.cmd2_public_attr_name(
"base_args"
)
NS_ATTR_PARENT_ARGS
module-attribute
NS_ATTR_PARENT_ARGS = constants.cmd2_public_attr_name(
"parent_args"
)
Cmd2ParserKwargs
Bases: TypedDict
Forwarded ctor kwargs for Cmd2ArgumentParser (PEP 692 Unpack).
Single source of truth mirroring the parser's __init__: add a field here to expose a new
ctor kwarg on the decorator's call site. All optional (total=False); suggest_on_error
and color only take effect on Python >= 3.14.
Argument
Argument(
*,
help_text=None,
metavar=None,
nargs=None,
choices=None,
choices_provider=None,
completer=None,
table_columns=None,
suppress_tab_hint=None,
const=_UNSET,
default=_UNSET,
allow_unknown_entry=False,
converter=None,
preprocess=None,
**extra_kwargs,
)
Bases: _BaseArgMetadata
Metadata for a positional argument in an Annotated type hint.
Initialise shared metadata fields.
const is the value stored on a present flag with no argument (Option only:
store_const/append_const); _UNSET distinguishes "no const" from const=None.
default mirrors the signature default (Option(default=v) == ... = v); supplying
both, or argparse.SUPPRESS, is rejected. allow_unknown_entry only affects Enum
annotations: when set, a token matched by neither a member value nor name is routed through
the enum's _missing_ hook (for aliases / special keywords) instead of being rejected
outright. converter replaces the inferred type= converter (and makes any annotation
type legal); preprocess runs before the inferred converter to transform the raw token while
keeping the inferred choices/completer. The two are mutually exclusive and neither combines with
a value-less action (see the module docstring). extra_kwargs forwards any other
add_argument parameter (incl. those from
register_argparse_argument_parameter) straight through.
Source code in cmd2/annotated.py
to_kwargs
Return non-None mapped fields, an explicit const, and any passthrough extra_kwargs.
Source code in cmd2/annotated.py
Option
Bases: _BaseArgMetadata
Metadata for an optional/flag argument in an Annotated type hint.
Positional *names are the flag strings (e.g. "--color", "-c"); when omitted
the decorator generates --param-name (underscores become dashes).
Initialise Option metadata.
action is a supported string action (store_true/store_false/count/
append/extend/store_const/append_const) or a custom
argparse.Action subclass (passed through; it owns storage, so the inferred
action and the action-specific constraints are skipped).
Source code in cmd2/annotated.py
to_kwargs
Return non-None fields as an argparse kwargs dict.
Source code in cmd2/annotated.py
ArgumentBlock
Marker base class for a reusable @with_annotated argument block.
Subclass it on a @dataclass to have with_annotated expand the
dataclass's fields into flat command-line arguments and reconstruct an instance at call time::
@dataclass
class CommonArgs(ArgumentBlock):
verbose: Annotated[bool, Option("-v", "--verbose")] = False
class MyApp(cmd2.Cmd):
@cmd2.with_annotated
def do_build(self, target: str, common: CommonArgs): ...
Only a class that inherits ArgumentBlock is treated as a block, so a plain @dataclass is left
alone and can still be used as an ordinary single argument value (e.g. via an Argument(converter=...)).
Inheritance doubles as the reuse mechanism: a subclass of a block is itself a block, so a shared base
block can be extended per command without repeating its fields. A block must be the bare annotation
of a regular parameter -- wrapping it in Annotated/Optional/a union, or using it as *args /
**kwargs, raises TypeError.
To share a block between a command and its subcommands, name the parameter cmd2_base_args on the
command and cmd2_parent_args on each subcommand that should receive it::
@dataclass
class SharedOpts(ArgumentBlock):
verbose: Annotated[bool, Option("-v", "--verbose")] = False
class MyApp(cmd2.Cmd):
@cmd2.with_annotated(base_command=True)
def do_root(self, cmd2_subcommand_func, cmd2_base_args: SharedOpts): ...
@cmd2.with_annotated(subcommand_to="root")
def root_show(self, cmd2_parent_args: SharedOpts):
self.poutput(cmd2_parent_args.verbose) # parsed on `root`, read here
A base command and its subcommands parse into one shared argparse.Namespace. cmd2_base_args
adds the block's flags to the command's own parser; cmd2_parent_args adds no arguments and is
reconstructed from the values an ancestor parsed (root --verbose show, not root show --verbose).
A cmd2_parent_args subcommand whose ancestors never declare a matching cmd2_base_args raises a
clear error the first time it runs.
Group
Argument-group definition for with_annotated(groups=...) / mutually_exclusive_groups=....
Initialise an argument group definition.
| PARAMETER | DESCRIPTION |
|---|---|
members
|
parameter names to place in the group (at least one)
TYPE:
|
title
|
group title shown as a help section header
TYPE:
|
description
|
group description shown under the title
TYPE:
|
required
|
TYPE:
|
Source code in cmd2/annotated.py
build_parser_from_function
build_parser_from_function(
func,
*,
skip_params=_SKIP_PARAMS,
groups=None,
mutually_exclusive_groups=None,
parser_class=None,
**parser_kwargs,
)
Inspect a function's signature and build a Cmd2ArgumentParser.
The lower-level entry point behind with_annotated. parser_kwargs is forwarded to
the parser ctor (see Cmd2ParserKwargs); when description is omitted, the first
paragraph of func.__doc__ is used.
| PARAMETER | DESCRIPTION |
|---|---|
func
|
the command function to inspect
TYPE:
|
skip_params
|
parameter names to exclude from the parser
TYPE:
|
groups
|
TYPE:
|
mutually_exclusive_groups
|
TYPE:
|
parser_class
|
custom parser class (defaults to the configured default)
TYPE:
|
parser_kwargs
|
forwarded
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Cmd2ArgumentParser
|
a fully configured |
Source code in cmd2/annotated.py
2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 | |
with_annotated
with_annotated(
func: Callable[_CommandParams, _CommandReturn],
) -> Callable[_CommandParams, _CommandReturn]
with_annotated(
func: None = ...,
*,
ns_provider: Callable[..., Namespace] | None = ...,
preserve_quotes: bool = ...,
with_unknown_args: bool = ...,
base_command: bool = ...,
subcommand_to: str | None = ...,
help: str | None = ...,
aliases: Sequence[str] = ...,
deprecated: bool = ...,
groups: tuple[Group, ...] | None = ...,
mutually_exclusive_groups: tuple[Group, ...]
| None = ...,
parser_class: type[Cmd2ArgumentParser] | None = ...,
subcommand_required: bool = ...,
subcommand_metavar: str = ...,
subcommand_title: str | None = ...,
subcommand_description: str | None = ...,
**parser_kwargs: Unpack[Cmd2ParserKwargs],
) -> _WithAnnotatedDecorator
with_annotated(
func=None,
*,
ns_provider=None,
preserve_quotes=False,
with_unknown_args=False,
base_command=False,
subcommand_to=None,
help=None,
aliases=(),
deprecated=False,
groups=None,
mutually_exclusive_groups=None,
parser_class=None,
subcommand_required=True,
subcommand_metavar="SUBCOMMAND",
subcommand_title=None,
subcommand_description=None,
**parser_kwargs,
)
Decorate a do_* method to build its argparse parser from type annotations.
| PARAMETER | DESCRIPTION |
|---|---|
func
|
the command function (when used without parentheses)
TYPE:
|
ns_provider
|
callable returning a prepopulated Namespace (not with
TYPE:
|
preserve_quotes
|
preserve quotes in arguments (not with
TYPE:
|
with_unknown_args
|
capture unknown args as the
TYPE:
|
base_command
|
add
TYPE:
|
subcommand_to
|
parent command name; function must be named
TYPE:
|
help
|
subcommand help text (only with
TYPE:
|
aliases
|
alternative subcommand names (only with
TYPE:
|
deprecated
|
mark the subcommand deprecated in
TYPE:
|
groups
|
TYPE:
|
mutually_exclusive_groups
|
TYPE:
|
parser_class
|
custom parser class (defaults to the configured default)
TYPE:
|
subcommand_required
|
whether a subcommand must be supplied (
TYPE:
|
subcommand_metavar
|
metavar for the subcommands group (
TYPE:
|
subcommand_title
|
title for the subcommands
TYPE:
|
subcommand_description
|
description for that section (
TYPE:
|
parser_kwargs
|
any
TYPE:
|
Source code in cmd2/annotated.py
2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 | |