Skip to content

Field Types

Thanks to features provided by pydantic's data definitions, argdantic supports a large amount of field types, starting from the standard library up to JSON inputs.

Primitive types

Considering primitive, non-complex data types, the library supports the following:

  • str: values accepted as is, parsed as simple text without further processing.

  • int: tries to convert any given input into an integer through int(value).

  • float: similarly, tries to convert any given input into a floating point number through float(value).

  • bytes: similar to strings, however in this case the underlying representation remains in bytes.

  • bool: by default, booleans are intended as flag options. In this case any boolean field will have two corresponding CLI flags --field/--no-field.

The following example shows a brief overview of the primitive types:

primitives.py
from argdantic import ArgParser

cli = ArgParser()


@cli.command()
def status(name: str, age: int, weight: float, data: bytes, flag: bool):
    print(f"name: {name}")
    print(f"age: {age}")
    print(f"weight: {weight}")
    print(f"data: {data}")
    print(f"flag: {flag}")


if __name__ == "__main__":
    cli()

With the following help message:

$ python primitives.py --help
> usage: primitives.py [-h] --name TEXT --age INT --weight FLOAT --data BYTES (--flag | --no-flag)
>
> optional arguments:
>   -h, --help      show this help message and exit
>   --name TEXT     (required)
>   --age INT       (required)
>   --weight FLOAT  (required)
>   --data BYTES    (required)
>   --flag
>   --no-flag

Note

Observe that the --flag and --no-flag options are not marked as required. That is the expected behaviour: strictly speaking, taken individually, they are not required. However, being mutually exclusive, one of either --flag or --no-flag is still needed.

`argdantic`` takes care of converting the provided fields into argparse arguments, so that the automatically generated description reamins as faithful as possible. Bear in mind that types are exploited only for documentation purposes, the final type checking will be carried out by pydantic. Most complex types are often interpreted as strings, unless specified otherwise.

Complex types

Thanks to the powerful data definitions provided by pydantic, argdantic supports a large amount of complex types, Currently, the following types have been tested and supported:

Standard Library types

Generally speaking, non-typed complex types will default to strings unless specified otherwise.

  • list: without specifying the internal type, list fields will behave as multiple options of string items. Internally, argdantic exploits _argparse's nargs option to handle sequences. In this case, the argument can be repeated multiple times to build a list. For instance, python cli.py --add 1 2 will result in a list [1, 2].

  • tuple: similar to lists, this will behave as an unbounded sequence of strings, with multiple parameters.

  • dict: dictionaries are interpreted as JSON strings. In this case, there will be no further validation. Given that valid JSON strings require double quotes, arguments provided through the command line must use single-quoted strings. For instance, python cli.py --extras '{"items": 12}' will be successfully parsed, while python cli.py --extras "{'items': 12}" will not.

  • set: again, from a command line point of view, sets are a simple list of values. In this case, repeated values will be excluded. For instance, python cli.py --add a --add b --add a will result in a set {'a', 'b'}.

  • frozenset: frozen sets adopt the same behavior as normal sets, with the only difference that they remain immutable.

  • deque: similarly, deques act as sequences from a CLI standpoint, while being treaded as double-ended queues in code.

  • range: ranges are interpreted as a sequence of integers, with the same behavior as lists and tuples.

Typing Containers

  • Any: For obvious reasons, Any fields will behave as str options without further processing.

  • Optional: optional typing can be interpreted as syntactic sugar, meaning it will not have any effect on the underlying validation, but it provides an explicit declaration that the field can also accept None as value.

  • List: Similar to standard lists, typing Lists behave as sequences of items. In this case however the inner type is exploited to provide further validation through pydantic. For instance, python cli.py --add a --add b will result in a validation error for a list of integers List[int].

  • Tuple: typing Tuples can behave in two ways: when using a variable length structure (i.e., Tuple[int] or Tuple[int, ...]), tuples act as a sequence of typed items, validated through pydantic, where the parameter is specified multiple times. When using a _fixed length structure (i.e., Tuple[int, int] or similar), they are considered as fixed nargs options, where the parameter is specified once, followed by the sequence of values separated by whitespaces. For instance . python cli.py --items a b c will results in a tuple ('a', 'b', 'c'). If the items tuple specified only two items, the command will result in a validation error.

  • Dict: Similar to the standard dict field, typing dictionaries require a JSON string as input. However, inner types allow for a finer validation: for instance, considering a metrics: Dict[str, float] field, --metrics '{"f1": 0.93}' is accepted, while --metrics '{"auc": "a"}' is not a valid input.

  • Deque: with the same reasoning of typed lists and tuples, Deques will act as sequences with a specific type.

  • Set: As you guessed, typed sets act as multiple options where repeated items are excluded, with additional type validation on the items themselves.

  • FrozenSet: as with Sets, but they represent immutable structures after parsing.

  • Sequence and Iterables: with no surpise, sequences and iterables act as sequences, nothing much to add here.

Warning

for obvious reasons, Union typings are not supported at this time. Parsing a multi-valued parameter is really more of a phylosophical problem than a technical one. Future releases will consider the support for this typing.

The code below provides a relatively comprehensive view of most container types supported through argdantic.

containers.py
from typing import Deque, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple

from argdantic import ArgParser

cli = ArgParser()


@cli.command()
def run(
    simple_list: list,
    list_of_ints: List[int],
    simple_tuple: tuple,
    multi_typed_tuple: Tuple[int, float, str, bool],
    simple_dict: dict,
    dict_str_float: Dict[str, float],
    simple_set: set,
    set_bytes: Set[bytes],
    frozen_set: FrozenSet[int],
    none_or_str: Optional[str],
    sequence_of_ints: Sequence[int],
    compound: Dict[str, List[Set[int]]],
    deque: Deque[int],
):
    print(f"simple_list: {simple_list}")
    print(f"list_of_ints: {list_of_ints}")
    print(f"simple_tuple: {simple_tuple}")
    print(f"multi_typed_tuple: {multi_typed_tuple}")
    print(f"simple_dict: {simple_dict}")
    print(f"dict_str_float: {dict_str_float}")
    print(f"simple_set: {simple_set}")
    print(f"set_bytes: {set_bytes}")
    print(f"frozen_set: {frozen_set}")
    print(f"none_or_str: {none_or_str}")
    print(f"sequence_of_ints: {sequence_of_ints}")
    print(f"compound: {compound}")
    print(f"deque: {deque}")


if __name__ == "__main__":
    cli()

Executing this script with the help command will provide the description for the current configuration. Also, defaults are allowed and validated.

$ python containers.py --help
> usage: containers.py [-h] --simple-list TEXT [TEXT ...] --list-of-ints INT [INT ...]
>   --simple-tuple TEXT [TEXT ...] --multi-typed-tuple INT FLOAT TEXT BOOL --simple-dict JSON
>   --dict-str-float JSON --simple-set TEXT [TEXT ...] --set-bytes BYTES [BYTES ...]
>   --frozen-set INT [INT ...] --none-or-str TEXT --sequence-of-ints INT [INT ...]
>   --compound JSON --deque INT [INT ...]
>
> optional arguments:
>   -h, --help            show this help message and exit
>   --simple-list TEXT [TEXT ...]           (required)
>   --list-of-ints INT [INT ...]            (required)
>   --simple-tuple TEXT [TEXT ...]          (required)
>   --multi-typed-tuple INT FLOAT TEXT BOOL (required)
>   --simple-dict JSON                      (required)
>   --dict-str-float JSON                   (required)
>   --simple-set TEXT [TEXT ...]            (required)
>   --set-bytes BYTES [BYTES ...]           (required)
>   --frozen-set INT [INT ...]              (required)
>   --none-or-str TEXT                      (required)
>   --sequence-of-ints INT [INT ...]        (required)
>   --compound JSON                         (required)
>   --deque INT [INT ...]                   (required)

Literals and Enums

Sometimes it may be useful to directly limit the choices of certain fields, by letting the user select among a fixed list of values. In this case, argdantic provides this feature using pydantic's support for Enum and Literal types, parsed from the command line through the choice argument option.

While Enums represent the standard way to provide choice-based options, Literals can be seen as a lightweight enumeration. In general, the latter are simpler and easier to handle than the former for most use cases. Enums on the other hand provide both a name and a value component, where only the former is exploited for the parameter definition. The latter can represent any kind of object, therefore making enums more suitable for more complex use cases.

The following script presents a sample of possible choice definitions in clidantic:

choices.py
from enum import Enum, IntEnum
from typing import Literal

from argdantic import ArgParser

cli = ArgParser()


class ToolEnum(Enum):
    hammer = "Hammer"
    screwdriver = "Screwdriver"


class HTTPEnum(IntEnum):
    ok = 200
    not_found = 404
    internal_error = 500


@cli.command()
def run(
    a: Literal["one", "two"] = "two",
    b: Literal[1, 2] = 2,
    c: Literal[True, False] = True,
    d: ToolEnum = ToolEnum.hammer,
    e: HTTPEnum = HTTPEnum.not_found,
):
    print(f"a: {a}")
    print(f"b: {b}")
    print(f"c: {c}")
    print(f"d: {d}")
    print(f"e: {e}")


if __name__ == "__main__":
    cli()

Warning

As you probably noticed, the string enumeration only subclasses Enum. Strictly speaking, ToolEnum(str, Enum) would be a better inheritance definition, however this breaks the type inference by providing two origins.

Currently, there are two solutions:

  • simply use Enum, it should be fine in most cases.
  • use StrEnum, which however is only available since Python 3.11.

Launching the help for this script will result in the following output:

$ python choices.py --help
> usage: choices.py [-h] [--a [one|two]] [--b [1|2]] [--c [True|False]] [--d [hammer|screwdriver]] [--e [ok|not_found|internal_error]]
>
> optional arguments:
>   -h, --help            show this help message and exit
>   --a [one|two]                       (default: two)
>   --b [1|2]                           (default: 2)
>   --c [True|False]                    (default: True)
>   --d [hammer|screwdriver]            (default: ToolEnum.hammer)
>   --e [ok|not_found|internal_error]   (default: HTTPEnum.not_found)

You can notice that, even without explicit description, choice-based fields will automatically provide the list of possible values. Defaults also behave as expected: both literals and enums will accept any of the allowed values as default, and it that case the selected item will be displayed as default in the console. Again, note that the CLI exploits the name field in enum-based arguments for readability, not its actual value.

Calling the script with a wrong choice will result in an error message, displaying the list of allowed values:

$ python choices.py --a three
> usage: choices.py [-h] [--a [one|two]] [--b [1|2]] [--c [True|False]] [--d [hammer|screwdriver]] [--e [ok|not_found|internal_error]]
> choices.py: error: argument --a: invalid choice: three (choose from [one|two])

Module types

Note

Coming soon!