Parameters

Typically, Cyclopts gets all the information it needs from object names, type hints, and the function docstring:

from cyclopts import App

app = App(help="This is help for the root application.")

@app.command
def foo(value: int):  # Cyclopts uses the ``value`` name and ``int`` type hint
    """Cyclopts uses this short description for help.

    Parameters
    ----------
    value: int
        Cyclopts uses this description for ``value``'s help.
    """

app()

Running the example:

$ my-script --help
Usage: my-script COMMAND

This is help for the root application.

╭─ Commands ──────────────────────────────────────────────────────────╮
│ foo        Cyclopts uses this short description for help.           │
│ --help,-h  Display this message and exit.                           │
│ --version  Display application version.                             │
╰─────────────────────────────────────────────────────────────────────╯

$ my-script foo --help
Usage: my-script [ARGS] [OPTIONS]

Cyclopts uses this short description for help.

╭─ Parameters ─────────────────────────────────────────────────────────────────────────╮
│ *  VALUE --value  Cyclopts uses this description for value's help. [required]        │
╰──────────────────────────────────────────────────────────────────────────────────────╯

This keeps the code as clean and terse as possible. However, if more control is required, we can provide additional information by annotating type hints with Parameter.

from cyclopts import App, Parameter
from typing import Annotated

app = App()

@app.command
def foo(bar: Annotated[int, Parameter(...)]):
    pass

app()

Parameter gives complete control on how Cyclopts processes the annotated parameter. See the API page for all configurable options. This page will investigate some of the more common use-cases.

Note

Parameter can also be used as a decorator. This is particularly useful for class definitions.

Naming

Like command names, CLI parameter names are derived from their python counterparts. However, sometimes customization is needed.

Manual Naming

Parameter names (and their short forms) can be manually specified:

from cyclopts import App, Parameter
from typing import Annotated

app = App()

@app.default
def main(
    *,
    foo: Annotated[str, Parameter(name=["--foo", "-f"])],  # Adding a short-form
    bar: Annotated[str, Parameter(name="--something-else")],
):
    pass

app()
$ my-script --help

Usage: main COMMAND [OPTIONS]
╭─ Commands ──────────────────────────────────────────────╮
│ --help -h  Display this message and exit.               │
│ --version  Display application version.                 │
╰─────────────────────────────────────────────────────────╯
╭─ Parameters ────────────────────────────────────────────╮
│ *  --foo             -f  [required]                     │
│ *  --something-else      [required]                     │
╰─────────────────────────────────────────────────────────╯

Manually set names via Parameter.name are not subject to Parameter.name_transform.

Name Transform

The name transform function that converts the python variable name to it's CLI counterpart can be configured by setting Parameter.name_transform (defaults to default_name_transform()).

from cyclopts import App, Parameter
from typing import Annotated

app = App()

def name_transform(s: str) -> str:
    return s.upper()

@app.default
def main(
    *,
    foo: Annotated[str, Parameter(name_transform=name_transform)],
    bar: Annotated[str, Parameter(name_transform=name_transform)],
):
    pass

app()
$ my-script --help
Usage: main COMMAND [OPTIONS]

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

Notice how the parameter is now --FOO instead of the standard --foo.

Note

The returned string is before the standard -- is prepended.

Generally, it is not very useful to set the name transform on individual parameters; it would be easier/clearer to manually specify the name. However, we can change the default name transform for the entire app by configuring the app's default_parameter.

To change the name_transform across your entire app, add the following to your App configuration:

app = App(
    default_parameter=Parameter(name_transform=my_custom_name_transform),
)

Help

It is recommended to use docstrings for your parameter help, but if necessary, you can explicitly set a help string:

@app.command
def foo(value: Annotated[int, Parameter(help="THIS IS USED.")]):
    """
    Parameters
    ----------
    value: int
        This description is not used; got overridden.
    """
$ my-script foo --help
╭─ Parameters ──────────────────────────────────────────────────╮
│ *  VALUE,--value  THIS IS USED. [required]                    │
╰───────────────────────────────────────────────────────────────╯

Converters

Cyclopts has a powerful coercion engine that automatically converts CLI string tokens to the types hinted in a function signature. However, sometimes a custom converter is required.

Lets consider a case where we want the user to specify a file size, and we want to allows suffixes like "MB".

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

app = App()

mapping = {
    "kb": 1024,
    "mb": 1024 * 1024,
    "gb": 1024 * 1024 * 1024,
}

def byte_units(type_, tokens: Sequence[Token]) -> int:
    # type_ is ``int``,
    value = tokens[0].value.lower()
    try:
        return type_(value)  # If this works, it didn't have a suffix.
    except ValueError:
        pass
    number, suffix = value[:-2], value[-2:]
    return int(number) * mapping[suffix]

@app.command
def zero(file: Path, size: Annotated[int, Parameter(converter=byte_units)]):
    """Creates a file of all-zeros."""
    print(f"Writing {size} zeros to {file}.")
    file.write_bytes(bytes(size))

app()
$ my-script zero out.bin 100
Writing 100 zeros to out.bin.

$ my-script zero out.bin 1kb
Writing 1024 zeros to out.bin.

$ my-script zero out.bin 3mb
Writing 3145728 zeros to out.bin.

The converter function gets the annotated type, and the Token s parsed for this argument. Tokens are Cyclopt's way of bookkeeping user inputs; in the last command the tokens object would look like:

 # tokens is a length-1 tuple. The variable "size" only takes in 1 token:
 tuple(
   Token(
      keyword=None,  # "3mb" was provided positionally, not by keyword
      value='3mb',   # The string from the command line
      source='cli',  # The value came from the command line, as opposed to other Cyclopts mechanisms.
      index=0,       # For the variable "size", this is the first (0th) token.
   ),
)

Validating Input

Just because data is of the correct type, doesn't mean it's valid. If we had a program that accepts integer user age as an input, -1 is an integer, but not a valid age.

from cyclopts import App, Parameter
from typing import Annotated

app = App()

def validate_age(type_, value):
    if value < 0:
        raise ValueError("Negative ages not allowed.")
    if value > 150:
        raise ValueError("You are too old to be using this application.")

@app.default
def allowed_to_buy_alcohol(age: Annotated[int, Parameter(validator=validate_age)]):
    print("Under 21: prohibited." if age < 21 else "Good to go!")

app()
$ my-script 30
Good to go!

$ my-script 10
Under 21: prohibited.

$ my-script -1
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ Invalid value "-1" for "AGE". Negative ages not allowed.                     │
╰──────────────────────────────────────────────────────────────────────────────╯

$ my-script 200
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ Invalid value "200" for "AGE". You are too old to be using this application. │
╰──────────────────────────────────────────────────────────────────────────────╯

Certain builtin error types (ValueError, TypeError, AssertionError) will be re-interpreted by Cyclopts and formatted into a prettier message for the application user.

Cyclopts has some builtin validators for common situations We can create a similar app as above:

from cyclopts import App, Parameter, validators
from typing import Annotated

app = App()

@app.default
def allowed_to_buy_alcohol(age: Annotated[int, Parameter(validator=validators.Number(gte=0, lte=150))]):
    # gte - greater than or equal to
    # lte - less than or equal to
    print("Under 21: prohibited." if age < 21 else "Good to go!")

app()

Taking this one step further, Cyclopts has some builtin convenience types. If we didn't care about the upper age bound, we could simplify the application to:

from cyclopts import App
from cyclopts.types import NonNegativeInt

app = App()

@app.default
def allowed_to_buy_alcohol(age: NonNegativeInt):
    print("Under 21: prohibited." if age < 21 else "Good to go!")

app()

Parameter Resolution

Cyclopts can combine multiple Parameter annotations together. Say you want to define a new int type that uses the byte-centric converter from above.

We can define the type:

ByteSize = Annotated[int, Parameter(converter=byte_units)]

We can then either directly annotate a function parameter with this:

@app.command
def zero(size: ByteSize):
    pass

or even stack annotations to add additional features, like a validator:

def must_be_multiple_of_4096(type_, value):
    assert value % 4096 == 0, "Size must be a multiple of 4096"


@app.command
def zero(size: Annotated[ByteSize, Parameter(validator=must_be_multiple_of_4096)]):
    pass

Python automatically flattens out annotations, so this is interpreted as:

Annotated[ByteSize, Parameter(converter=byte_units), Parameter(validator=must_be_multiple_of_4096)]

Cyclopts will search right-to-left for set parameter attributes until one is found. I.e. right-most parameter attributes have the highest priority.

$ my-script 1234
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ Invalid value "1234" for "SIZE". Size must be a multiple of 4096             │
╰──────────────────────────────────────────────────────────────────────────────╯

See Parameter Resolution Order for more details.