Parameters

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

import cyclopts

app = cyclopts.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()
$ 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 terse and clean as possible. However, if more control is required, we can use Parameter along with the python builtin Annotated. Prior to Python 3.9, Annotated has to be imported from typing_extensions; in more recent python versions it can be directly imported from the typing module.

from cyclopts import Parameter
from typing_extensions import Annotated


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

Parameter gives complete control on how Cyclopts processes the annotated parameter. See the API page for all configurable options.

Naming

Like command names, commandline parameters are derived from their python function argument counterparts. This automatic command name transform can be configured by Parameter.name_transform. Note that the resulting string is before the standard -- is prepended.

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),
)

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

Help

It's 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
from typing_extensions import Annotated
from pathlib import Path

app = App()

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


def byte_units(type_, *values):
    value = values[0].lower()
    try:
        return int(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 all the string tokens parsed for this argument. The returned value gets used by the function.

Validating Input

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

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: int):
    if age < 21:
        print("Under 21: prohibited.")
    else:
        print("Good to go!")


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

$ my-script 10
Under 21: prohibited.

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

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

Parameter Resolution

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


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

See Parameter Resolution Order for more details.