# Coercion Rules

This page intends to serve as a terse set of type coercion rules that Cyclopts follows.

Automatic coercion can always be overridden by the `Parameter.converter` field. Typically, the `converter` function will receive a single token, but it may receive multiple tokens if the annotated type is iterable (e.g. `list`, `set`). The number of tokens can be explicitly controlled with `n_tokens`, which is useful when the type signature doesn't match the desired CLI token consumption.

## No Hint

If no explicit type hint is provided:

- If the parameter has a non-None default value, interpret the type as `type(default_value)`.

      from cyclopts import App

      app = App()

      @app.default
      def default(value=5):
          print(f"{value=} {type(value)=}")

      app()

      $ my-program 3
      value=3 type(value)=<class 'int'>

- Otherwise, interpret the type as string.

      from cyclopts import App

      app = App()

      @app.default
      def default(value):
          print(f"{value=} {type(value)=}")

      app()

      $ my-program foo
      value='foo' type(value)=<class 'str'>

## Any

A standalone `Any` type hint is equivalent to No Hint

## Str

No operation is performed, CLI tokens are natively strings.

    from cyclopts import App

    app = App()

    @app.default
    def default(value: str):
        print(f"{value=} {type(value)=}")

    app()

    $ my-program foo
    value='foo' type(value)=<class 'str'>

## Int

For convenience, Cyclopts provides a richer feature-set of parsing integers than just naively calling `int`.

- Accepts vanilla decimal values (e.g. `123`, `3.1415`). Floating-point values will be rounded prior to casting to an `int`.

- Accepts binary values (strings starting with `0b`)

- Accepts octal values (strings starting with `0o`)

- Accepts hexadecimal values (strings starting with `0x`).

### Counting Flags

For parameters that need to track the number of times a flag appears (e.g., verbosity levels like `-vvv`), use `Parameter.count` with an `int` type hint.

    from cyclopts import App, Parameter
    from typing import Annotated

    app = App()

    @app.default
    def main(verbose: Annotated[int, Parameter(alias="-v", count=True)] = 0):
        print(f"Verbosity level: {verbose}")

    app()

    $ my-program
    Verbosity level: 0

    $ my-program -v
    Verbosity level: 1

    $ my-program -vvv
    Verbosity level: 3

    $ my-program --verbose --verbose
    Verbosity level: 2

    $ my-program -v --verbose -vv
    Verbosity level: 4

## Float

Token gets cast as `float(token)`. For example, `float("3.14")`.

## Complex

Token gets cast as `complex(token)`. For example, `complex("3+5j")`

## Bool

1. If specified as a keyword, booleans are interpreted flags that take no parameter. The default false-like flag are `--no-FLAG-NAME`. See `Parameter.negative` for more about this feature.

   Example:

       from cyclopts import App

       app = App()

       @app.command
       def foo(my_flag: bool):
           print(my_flag)

       app()

       $ my-program foo --my-flag
       True

       $ my-program foo --no-my-flag
       False

2. If specified as a positional argument, a case-insensitive lookup is performed:

   - If the token is a true-like value `{"yes", "y", "1", "true", "t"}`, then it is parsed as `True`.

   - If the token is a false-like value `{"no", "n", "0", "false", "f"}`, then it is parsed as `False`.

   - Otherwise, a `CoercionError` will be raised.

   Cyclopts is stricter than traditional `bool` casting; the provided value must be one of the above. For example, `2` is not considered a true-like value and will raise an error.

       $ my-program foo 1
       True

       $ my-program foo 0
       False

       $ my-program foo 2
       ╭─ Error ───────────────────────────────────────╮
       │ Invalid value for --my-flag: unable to        │
       │ convert "2" into bool.                        │
       ╰───────────────────────────────────────────────╯

       $ my-program foo not-a-true-or-false-value
       ╭─ Error ─────────────────────────────────────────────────╮
       │ Invalid value for --my-flag: unable to convert          │
       │ "not-a-true-or-false-value" into bool.                  │
       ╰─────────────────────────────────────────────────────────╯

3. If specified as a keyword with a value attached with an `=`, then the provided value will be parsed according to positional argument rules above (2).

>     from cyclopts import App

>     app = App()

>     @app.command
>     def foo(my_flag: bool):
>         print(my_flag)

>      app()

>     $ my-program foo --my-flag=true
>     True

>     $ my-program foo --my-flag=false
>     False

>     $ my-program foo --no-my-flag=true
>     False

>     $ my-program foo --no-my-flag=false
>     True

## List

Unlike more simple types like `str` and `int`, lists use different parsing rules depending on whether the values are provided positionally or by keyword.

### Positional

When arguments are provided positionally:

- If `Parameter.allow_leading_hyphen` is `False` (default behavior), reaching an option-like token will stop parsing for this parameter. If the number of consumed tokens is not a multiple of the required number of tokens to create an element of the list, a `MissingArgumentError` will be raised.

      from cyclopts import App

      app = App()

      @app.command
      def foo(values: list[int]):  # 1 CLI token per element
         print(values)

      @app.command
      def bar(values: list[tuple[int, str]]):  # 2 CLI tokens per element
         print(values)

      app()

      $ my-program foo 1 2 3
      [1, 2, 3]

      $ my-program bar 1 one 2 two
      [(1, 'one'), (2, 'two')]

      $ my-program bar 1 one 2
      ╭─ Error ─────────────────────────────────────────────────────╮
      │ Command "bar" parameter "--values" requires 2 arguments.    │
      │ Only got 1.                                                 │
      ╰─────────────────────────────────────────────────────────────╯

- If `Parameter.allow_leading_hyphen` is `True`, CLI tokens will be consumed unconditionally until exhausted.

      from cyclopts import App, Parameter
      from pathlib import Path
      from typing import Annotated

      app = App()

      @app.default
      def main(
         files: Annotated[list[Path], Parameter(allow_leading_hyphen=True)],
         some_flag: bool = False,
       ):
         print(f"{some_flag=}")
         print(f"Analyzing files {files}")

      app()

      $ my-program foo.bin bar.bin --fizz.bin buzz.bin --some-flag
      some_flag=True
      Analyzing files [PosixPath('foo.bin'), PosixPath('bar.bin'), PosixPath('--fizz.bin'), PosixPath('buzz.bin')]

  Known keyword arguments are parsed first (in this case, `--some-flag`). To unambiguously pass in values positionally, provide them after a bare `--`:

      $ my-program -- foo.bin bar.bin --fizz.bin buzz.bin --some-flag
      some_flag=False
      Analyzing files [PosixPath('foo.bin'), PosixPath('bar.bin'), PosixPath('--fizz.bin'), PosixPath('buzz.bin'), PosixPath('--some-flag')]

### Keyword

When arguments are provided by keyword:

- Tokens will be consumed until enough data is collected to form the type-hinted object.

- The keyword can be specified multiple times.

- If `Parameter.allow_leading_hyphen` is `False` (default behavior), reaching an option-like token will raise `MissingArgumentError` if insufficient tokens have been parsed.

      from cyclopts import App

      app = App()

      @app.command
      def foo(values: list[int]):  # 1 CLI token per element
         print(values)

      @app.command
      def bar(values: list[tuple[int, str]]):  # 2 CLI tokens per element
         print(values)

      app()

      $ my-program foo --values 1 --values 2 --values 3
      [1, 2, 3]

      $ my-program bar --values 1 one --values 2 two
      [(1, 'one'), (2, 'two')]

      $ my-program bar --values 1 --values 2
      ╭─ Error ─────────────────────────────────────────────────────╮
      │ Command "bar" parameter "--values" requires 2 arguments.    │
      │ Only got 1.                                                 │
      ╰─────────────────────────────────────────────────────────────╯

- If `Parameter.consume_multiple` is `True`, all remaining tokens will be consumed (until an option-like token is reached if `Parameter.allow_leading_hyphen` is `False`).

  `Parameter.consume_multiple` also accepts an `int` (minimum element count) or a `tuple[int, int]` for `(min, max)` bounds. See the `Parameter.consume_multiple` API docs for details.

      from cyclopts import App, Parameter
      from typing import Annotated

      app = App()

      @app.default
      def foo(values: Annotated[list[int], Parameter(consume_multiple=True)]):  # 1 CLI token per element
         print(values)

      app()

      $ my-program foo --values 1 2 3
      [1, 2, 3]

- If `Parameter.allow_repeating` is `False`, a keyword option cannot be specified more than once. This is especially useful in combination with `Parameter.consume_multiple` to allow `--foo a b c` but reject `--foo a --foo b`. If `Parameter.allow_repeating` is `True`, scalar types use "last wins" semantics instead of raising an error.

      from cyclopts import App, Parameter
      from typing import Annotated

      app = App()

      @app.default
      def foo(values: Annotated[list[int], Parameter(consume_multiple=True, allow_repeating=False)]):
         print(values)

      app()

      $ my-program --values 1 2 3
      [1, 2, 3]

      $ my-program --values 1 --values 2
      ╭─ Error ──────────────────────────────────────────────────╮
      │ Parameter --values specified multiple times.             │
      ╰─────────────────────────────────────────────────────────╯

### Empty List

Commonly, if we want a default list for a parameter in a function, we set the default value to `None` in the signature and then set it to the actual list in the function body:

    def foo(extensions: Optional[list] = None):
       if extensions is None:
          extensions = [".png", ".jpg"]

We do this because mutable defaults is a common unexpected source of bugs in python.

However, sometimes we actually want to specify an empty list. To get an empty list pass in the flag `--empty-MY-LIST-NAME`.

    from cyclopts import App

    app = App()

    @app.default
    def main(extensions: list | None = None):
       if extensions is None:
          extensions = [".png", ".jpg"]
       print(f"{extensions=}")

    app()

    $ my-program
    extensions=['.png', '.jpg']

    $ my-program --empty-extensions
    extensions=[]

See `Parameter.negative` for more about this feature.

### Positional Only With Subsequent Parameters

When a list is positional-only, it will consume tokens such that it leaves enough tokens for subsequent positional-only parameters.

    from pathlib import Path
    from cyclopts import App

    app = App()

    @app.default
    def main(srcs: list[Path], dst: Path, /):  # "/" makes all prior parameters POSITIONAL_ONLY
        print(f"Processing files {srcs!r} to {dst!r}.")

    app()

    $ my-program foo.bin bar.bin output.bin
    Processing files [PosixPath('foo.bin'), PosixPath('bar.bin')] to PosixPath('output.bin').

The console wildcard `*` is expanded by the console, so this example will naturally work with wildcards.

    $ ls foo
    buzz.bin fizz.bin

    $ my-program foo/*.bin output.bin
    Processing files [PosixPath('foo/buzz.bin'), PosixPath('foo/fizz.bin')] to PosixPath('output.bin').

## Iterable

Follows the same rules as List. The passed in data will be a `list`.

## Sequence

Follows the same rules as List. The passed in data will be a `list`.

## Set

Follows the same rules as List, but the resulting datatype is a `set`.

## Frozenset

Follows the same rules as Set, but the resulting datatype is a `frozenset`.

## Tuple

- The inner type hint(s) will be applied independently to each element. Enough CLI tokens will be consumed to populate the inner types.

- Nested fixed-length tuples are allowed: E.g. `tuple[tuple[int, str], str]` will consume 3 CLI tokens.

- Indeterminite-size tuples `tuple[type, ...]` are only supported at the root-annotation level and behave similarly to List.

    from cyclopts import App

    app = App()

    @app.default
    def default(coordinates: tuple[float, float, str]):
       print(f"{coordinates=}")

    app()

And invoke our script:

    $ my-program --coordinates 3.14 2.718 my-coord-name
    coordinates=(3.14, 2.718, 'my-coord-name')

## Dict

Cyclopts can populate dictionaries using keyword dot-notation:

    from cyclopts import App

    app = App()

    @app.default
    def default(message: str, *, mapping: dict[str, str] | None = None):
        if mapping:
            for find, replace in mapping.items():
                message = message.replace(find, replace)
        print(message)

    app()

    $ my_program 'Hello Cyclopts users!'
    Hello Cyclopts users!

    $ my_program 'Hello Cyclopts users!' --mapping.Hello Hey
    Hey Cyclopts users!

    $ my_program 'Hello Cyclopts users!' --mapping.Hello Hey --mapping.users developers
    Hey Cyclopts developers!

Due to the way of specifying keys, it is recommended to make dict parameters keyword-only; dicts cannot be populated positionally. If you do not wish for the user to be able to specify arbitrary keys, see User-Defined Classes. For specifying arbitrary keywords at the root level, see kwargs.

## Union

The unioned types will be iterated left-to-right until a successful coercion is performed. `None` type hints are ignored.

    from cyclopts import App
    from typing import Union

    app = App()

    @app.default
    def default(a: Union[None, int, str]):
        print(type(a))

    app()

    $ my-program 10
    <class 'int'>

    $ my-program bar
    <class 'str'>

## Optional

`Optional[...]` is syntactic sugar for `Union[..., None]`. See Union rules.

## Literal

The `Literal` type is a good option for limiting user input to a set of choices. Like Union, the `Literal` options will be iterated left-to-right until a successful coercion is performed. Cyclopts attempts to coerce the input token into the type of each `Literal` option.

    from cyclopts import App
    from typing import Literal

    app = App()

    @app.default
    def default(value: Literal["foo", "bar", 3]):
        print(f"{value=} {type(value)=}")

    app()

    $ my-program foo
    value='foo' type(value)=<class 'str'>

    $ my-program bar
    value='bar' type(value)=<class 'str'>

    $ my-program 3
    value=3 type(value)=<class 'int'>

    $ my-program fizz
    ╭─ Error ─────────────────────────────────────────────────╮
    │ Invalid value "fizz" for VALUE. Choose from: "foo",     │
    │ "bar", 3.                                               │
    ╰─────────────────────────────────────────────────────────╯

## Enum

While Literal is the recommended way of providing the user a set of choices, another method is using `Enum`.

The `Parameter.name_transform` gets applied to all `Enum` names, as well as the CLI provided token. By default,this means that a case-insensitive name lookup is performed. If an enum name contains an underscore, the CLI parameter may instead contain a hyphen, `-`. Leading/Trailing underscores will be stripped.

If coming from Typer, Cyclopts Enum handling is the reverse of Typer. Typer attempts to match the token to an Enum value; Cyclopts attempts to match the token to an Enum name. This is done because generally the name of the enum is meant to be human readable, while the value has some program/machine significance.

As a real-world example, the PNG image format supports 5 different color-types, which gets encoded into a 1-byte int in the image header.

    from cyclopts import App
    from enum import IntEnum

    app = App()

    class ColorType(IntEnum):
        GRAYSCALE = 0
        RGB = 2
        PALETTE = 3
        GRAYSCALE_ALPHA = 4
        RGBA = 6

    @app.default
    def default(color_type: ColorType = ColorType.RGB):
        print(f"Writing color-type value: {color_type} to the image header.")

    app()

    $ my-program
    Writing color-type value: 2 to the image header.

    $ my-program grayscale-alpha
    Writing color-type value: 4 to the image header.

## Flag

`Flag` enums (and by extension, `IntFlag`) are treated as a collection of boolean flags.

The `Parameter.name_transform` gets applied to all `Flag` names, as well as the CLI provided token. By default, this means that a case-insensitive name lookup is performed. If an enum name contains an underscore, the CLI parameter may instead contain a hyphen, `-`. Leading/Trailing underscores will be stripped.

    from cyclopts import App
    from enum import Flag, auto

    app = App()

    class Permission(Flag):
        READ = auto()
        WRITE = auto()
        EXECUTE = auto()

    @app.default
    def default(permissions: Permission = Permission.READ):
        print(f"Permissions: {permissions}")

    app()

    $ my-program
    Permissions: Permission.READ

    $ my-program write
    Permissions: Permission.WRITE

    $ my-program read write
    Permissions: Permission.READ|WRITE

    $ my-program --permissions.write
    Permissions: Permission.WRITE

    $ my-program --permissions.write --permissions.read
    Permissions: Permission.READ|WRITE

Note

If you want to directly expose the flags as booleans (e.g. `--read`), then see Namespace Flattening.

## date

Cyclopts supports parsing dates into a `date` object. It uses `fromisoformat()` under the hood, so the only supported format is `%Y-%m-%d` (e.g. 1956-01-31). However, if you use newer Python (>= 3.11), it also supports other formats such as `%Y%m%d` (e.g., 20191204), 2021-W01-1, etc, defined by ISO 8601.

## datetime

Cyclopts supports parsing timestamps into a `datetime` object. The supplied time must be in one of the following formats:

- `%Y-%m-%d` (e.g. 1956-01-31)

- `%Y-%m-%dT%H:%M:%S` (e.g. 1956-01-31T10:00:00)

- `%Y-%m-%d %H:%M:%S` (e.g. 1956-01-31 10:00:00)

- `%Y-%m-%dT%H:%M:%S%z` (e.g. 1956-01-31T10:00:00+0000)

- `%Y-%m-%dT%H:%M:%S.%f` (e.g. 1956-01-31T10:00:00.123456)

- `%Y-%m-%dT%H:%M:%S.%f%z` (e.g. 1956-01-31T10:00:00.123456+0000)

## timedelta

Cyclopts supports parsing time durations into a `timedelta` object. The supplied time must be in one of the following formats:

- `30s` - 30 seconds

- `5m` - 5 minutes

- `2h` - 2 hours

- `1d` - 1 day

- `3w` - 3 weeks

- `6M` - 6 months (approximate)

- `1y` - 1 year (approximate)

Combining durations is also supported:

- "1h30m" - 1 hour and 30 minutes

- "1d12h" - 1 day and 12 hours

## User-Defined Classes

Cyclopts supports classically defined user classes, as well as classes defined by the following dataclass-like libraries:

- attrs

- dataclass

- NamedTuple

- pydantic

- TypedDict

Note

For `pydantic` classes, Cyclopts will not internally perform type conversions and instead relies on pydantic's coercion engine.

Subkey parsing allows for assigning values positionally and by keyword with a dot-separator.

    from cyclopts import App
    from dataclasses import dataclass
    from typing import Literal

    app = App()

    @dataclass
    class User:
       name: str
       age: int
       region: Literal["us", "ca"] = "us"

    @app.default
    def main(user: User):
       print(user)

    app()

    $ my-program --help
    Usage: main COMMAND [ARGS] [OPTIONS]

    ╭─ Commands ──────────────────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                                       │
    │ --version  Display application version.                                         │
    ╰─────────────────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ────────────────────────────────────────────────────────────────────╮
    │ *  USER.NAME --user.name      [required]                                        │
    │ *  USER.AGE --user.age        [required]                                        │
    │    USER.REGION --user.region  [choices: us, ca] [default: us]                   │
    ╰─────────────────────────────────────────────────────────────────────────────────╯

    $ my-program 'Bob Smith' 30
    User(name='Bob Smith', age=30, region='us')

    $ my-program --user.name 'Bob Smith' --user.age 30
    User(name='Bob Smith', age=30, region='us')

    $ my-program --user.name 'Bob Smith' 30 --user.region=ca
    User(name='Bob Smith', age=30, region='ca')

Cyclopts will recursively search for `Parameter` annotations and respect them:

    from cyclopts import App, Parameter
    from dataclasses import dataclass
    from typing import Annotated

    app = App()

    @dataclass
    class User:
       # Beginning with "--" will completely override the parenting parameter name.
       name: Annotated[str, Parameter(name="--nickname")]
       # Not beginning with "--" will tack it on to the parenting parameter name.
       age: Annotated[int, Parameter(name="years-young")]

    @app.default
    def main(user: Annotated[User, Parameter(name="player")]):
       print(user)

    app()

    $ my-program --help
    Usage: main COMMAND [ARGS] [OPTIONS]

    ╭─ Commands ────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                 │
    │ --version  Display application version.                   │
    ╰───────────────────────────────────────────────────────────╯
    ╭─ Parameters ──────────────────────────────────────────────╮
    │ *  NICKNAME --nickname     [required]                     │
    │ *  PLAYER.YEARS-YOUNG      [required]                     │
    │      --player.years-young                                 │
    ╰───────────────────────────────────────────────────────────╯

### Namespace Flattening

The special parameter name `"*"` will remove the immediate parameter's name from the dotted-hierarchal name:

    from cyclopts import App, Parameter
    from dataclasses import dataclass
    from typing import Annotated

    app = App()

    @dataclass
    class User:
       name: str
       age: int

    @app.default
    def main(user: Annotated[User, Parameter(name="*")]):
       print(user)

    app()

    $ my-program --help
    Usage: main COMMAND [ARGS] [OPTIONS]

    ╭─ Commands ─────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.              │
    │ --version  Display application version.                │
    ╰────────────────────────────────────────────────────────╯
    ╭─ Parameters ───────────────────────────────────────────╮
    │ *  NAME --name  [required]                             │
    │ *  AGE --age    [required]                             │
    ╰────────────────────────────────────────────────────────╯

This can be used to conveniently share parameters between commands, and to create a global config object. See Sharing Parameters.

### Docstrings

Docstrings from the class are used for the help page. Docstrings from the command have priority over class docstrings, if supplied:

    from cyclopts import App
    from dataclasses import dataclass

    app = App()

    @dataclass
    class User:
       name: str
       "First and last name of the user."

       age: int
       "Age in years of the user."

    @app.default
    def main(user: User):
       """A short summary of what this program does.

       Parameters
       ----------
       user.age: int
          User's age docstring from the command docstring.
       """
       print(user)

    app()

    $ my-program --help
    Usage: main COMMAND [ARGS] [OPTIONS]

    A short summary of what this program does.

    ╭─ Commands ──────────────────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                                       │
    │ --version  Display application version.                                         │
    ╰─────────────────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ────────────────────────────────────────────────────────────────────╮
    │ *  USER.NAME --user.name  First and last name of the user. [required]           │
    │ *  USER.AGE --user.age    User's age docstring from the command docstring.      │
    │                           [required]                                            │
    ╰─────────────────────────────────────────────────────────────────────────────────╯

### Parameter(accepts_keys=False)

If the class is annotated with `Parameter(accepts_keys=False)`, then no dot-notation subkeys are exported. The class parameter will consume enough tokens to populate the required positional arguments.

    from cyclopts import App, Parameter
    from dataclasses import dataclass
    from typing import Annotated, Literal

    app = App()

    @dataclass
    class User:
       name: str
       age: int
       region: Literal["us", "ca"] = "us"

    @app.default
    def main(user: Annotated[User, Parameter(accepts_keys=False)]):
       print(user)

    app()

    $ my-program --help
    Usage: main COMMAND [ARGS] [OPTIONS]

    ╭─ Commands ─────────────────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                                      │
    │ --version  Display application version.                                        │
    ╰────────────────────────────────────────────────────────────────────────────────╯
    ╭─ Parameters ───────────────────────────────────────────────────────────────────╮
    │ *  USER --user  [required]                                                     │
    ╰────────────────────────────────────────────────────────────────────────────────╯

    $ my-program 'Bob Smith' 27
    User(name='Bob Smith', age=27, region='us')

    $ my-program 'Bob Smith'
    ╭─ Error ────────────────────────────────────────────────────────────────────────╮
    │ Parameter --user requires 2 arguments. Only got 1.                             │
    ╰────────────────────────────────────────────────────────────────────────────────╯

In this example, we are unable to change the `region` parameter of `User` from the CLI.