cyclopts - Man Page

Name

cyclopts — cyclopts 4.7.0

[image: Python compat] [image]

[image: PyPI] [image]
<https://pypi.org/project/cyclopts/> [image: ReadTheDocs] [image]
<https://cyclopts.readthedocs.io> [image: codecov] [image]
<https://codecov.io/gh/BrianPugh/cyclopts>

----

Documentation: <https://cyclopts.readthedocs.io>

Source Code: <https://github.com/BrianPugh/cyclopts>

----

Cyclopts is a modern, easy-to-use command-line interface (CLI) framework that aims to provide an intuitive & efficient developer experience.

Why Cyclopts?

Installation

Cyclopts requires Python >=3.10; to install Cyclopts, run:

pip install cyclopts

Quick Start

Execute the script from the command line:

$ python start.py 3
Looping! 0
Looping! 1
Looping! 2

When you need more control:

Execute the script from the command line:

$ python demo.py
Hello world! This runs when no command is specified.

$ python demo.py foo 3
Looping! 0
Looping! 1
Looping! 2

With just a few additional lines of code, we have a full-featured CLI app. See the docs <https://cyclopts.readthedocs.io> for more advanced usage.

Compared to Typer

Cyclopts is what you thought Typer was. Cyclopts's includes information from docstrings, support more complex types (even Unions and Literals!), and include proper validation support. See the documentation for a complete Typer comparison <https://cyclopts.readthedocs.io/en/latest/vs_typer/README.html>.

Consider the following short 29-line Cyclopts application:

import cyclopts
from typing import Literal

app = cyclopts.App()

@app.command
def deploy(
    env: Literal["dev", "staging", "prod"],
    replicas: int | Literal["default", "performance"] = "default",
):
    """Deploy code to an environment.

    Parameters
    ----------
    env
        Environment to deploy to.
    replicas
        Number of workers to spin up.
    """
    if replicas == "default":
        replicas = 10
    elif replicas == "performance":
        replicas = 20

    print(f"Deploying to {env} with {replicas} replicas.")


if __name__ == "__main__":
    app()
$ my-script deploy --help
Usage: my-script.py deploy [ARGS] [OPTIONS]

Deploy code to an environment.

╭─ Parameters ────────────────────────────────────────────────────────────────────────────────────╮
│ *  ENV --env            Environment to deploy to. [choices: dev, staging, prod] [required]      │
│    REPLICAS --replicas  Number of workers to spin up. [choices: default, performance] [default: │
│                         default]                                                                │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

$ my-script deploy staging
Deploying to staging with 10 replicas.

$ my-script deploy staging 7
Deploying to staging with 7 replicas.

$ my-script deploy staging performance
Deploying to staging with 20 replicas.

$ my-script deploy nonexistent-env
╭─ Error ────────────────────────────────────────────────────────────────────────────────────────────╮
│ Error converting value "nonexistent-env" to typing.Literal['dev', 'staging', 'prod'] for "--env".  │
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯

$ my-script --version
0.0.0

In its current state, this application would be impossible to implement in Typer. However, lets see how close we can get with Typer (47-lines):

import typer
from typing import Annotated, Literal
from enum import Enum

app = typer.Typer()

class Environment(str, Enum):
    dev = "dev"
    staging = "staging"
    prod = "prod"

def replica_parser(value: str):
    if value == "default":
        return 10
    elif value == "performance":
        return 20
    else:
        return int(value)

def _version_callback(value: bool):
    if value:
        print("0.0.0")
        raise typer.Exit()

@app.callback()
def callback(
    version: Annotated[
        bool | None, typer.Option("--version", callback=_version_callback)
    ] = None,
):
    pass

@app.command(help="Deploy code to an environment.")
def deploy(
    env: Annotated[Environment, typer.Argument(help="Environment to deploy to.")],
    replicas: Annotated[
        int,
        typer.Argument(
            parser=replica_parser,
            help="Number of workers to spin up.",
        ),
    ] = replica_parser("default"),
):
    print(f"Deploying to {env.name} with {replicas} replicas.")

if __name__ == "__main__":
    app()
$ my-script deploy --help

Usage: my-script deploy [OPTIONS] ENV:{dev|staging|prod} [REPLICAS]

 Deploy code to an environment.

╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────╮
│ *    env           ENV:{dev|staging|prod}  Environment to deploy to. [default: None] [required] │
│      replicas      [REPLICAS]              Number of workers to spin up. [default: 10]          │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────╮
│ --help          Show this message and exit.                                                     │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

$ my-script deploy staging
Deploying to staging with 10 replicas.

$ my-script deploy staging 7
Deploying to staging with 7 replicas.

$ my-script deploy staging performance
Deploying to staging with 20 replicas.

$ my-script deploy nonexistent-env
Usage: my-script.py deploy [OPTIONS] ENV:{dev|staging|prod} [REPLICAS]
Try 'my-script.py deploy --help' for help.
╭─ Error ─────────────────────────────────────────────────────────────────────────────────────────╮
│ Invalid value for '[REPLICAS]': nonexistent-env                                                 │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

$ my-script --version
0.0.0

The Typer implementation is 47 lines long, while the Cyclopts implementation is just 29 (38% shorter!). Not only is the Cyclopts implementation significantly shorter, but the code is easier to read. Since Typer does not support Unions, the choices for replica could not be displayed on the help page. Cyclopts is much more terse, much more readable, and much more intuitive to use.

For extensive documentation on all the features Cyclopts has to offer, checkout the API <#api> page.

Installation

Cyclopts requires Python >=3.10 and can be installed from PyPI via:

python -m pip install cyclopts

To install directly from github, you can run:

python -m pip install git+https://github.com/BrianPugh/cyclopts.git

For Cyclopts development, its recommended to use uv:

git clone https://github.com/BrianPugh/cyclopts.git
cd cyclopts
uv sync --all-extras

Getting Started

Cyclopts relies heavily on function parameter type hints. If you are new to type hints or need a refresher, checkout the mypy cheatsheet <https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html>.

A Basic Cyclopts Application

The most basic Cyclopts application is as follows:

from cyclopts import App

app = App()

@app.default
def main():
    print("Hello World!")

if __name__ == "__main__":
    app()

Save this as main.py and execute it to see:

$ python main.py
Hello World!

The App <#cyclopts.App> class offers various configuration options that we'll explore in more detail later. The app object has a decorator method, default <#cyclopts.App.default>, which registers a function as the default action. In this example, the main function is our default action, and is executed when no CLI command is provided.

Function Arguments

Let's add some arguments to make this program a little more interesting.

from cyclopts import App

app = App()

@app.default
def main(name):
    print(f"Hello {name}!")

if __name__ == "__main__":
    app()

Executing the script with the argument Alice produces the following:

$ python main.py Alice
Hello Alice!

Code explanation:

  1. The function main() was registered to app as the default action.
  2. Calling app() at the bottom triggers the app to begin parsing CLI inputs.
  3. Cyclopts identifies "Alice" as a positional argument and matches it to the parameter name. In the absence of an explicit type hint, Cyclopts defaults to parsing the value as a str.

    Note:

    Without a type annotation, Cyclopts will actually first attempt to use the type of the parameter's default value. If the parameter doesn't have a default value, it will then fallback to str. See Coercion Rules <#coercion-rules>.

  4. Cyclopts calls the registered default function main("Alice"), and the greeting is printed.

Multiple Arguments

Extending the example, lets add more arguments and type hints:

from cyclopts import App

app = App()

@app.default
def main(name: str, count: int, formal: bool = False):
    for _ in range(count):
       if formal:
          print(f"Hello {name}!")
       else:
          print(f"Hey {name}!")

if __name__ == "__main__":
    app()
$ python main.py Alice 3
Hey Alice!
Hey Alice!
Hey Alice!

$ python main.py Alice 3 --formal
Hello Alice!
Hello Alice!
Hello Alice!

The command line input "3" is converted to an integer because the parameter count has the type hint int <https://docs.python.org/3/library/functions.html#int>. Boolean parameters (e.g., --formal in this example) are interpreted as flags. Cyclopts natively handles all python builtin types (and more! <#coercion-rules>). Cyclopts adheres to Python's argument binding rules, allowing for both positional and keyword arguments. All of the following CLI invocations are equivalent:

$ python main.py Alice 3                  # Supplying arguments positionally.
$ python main.py --name Alice --count 3   # Supplying arguments via keywords.
$ python main.py --name=Alice --count=3   # Using = for matching keywords to values is allowed.
$ python main.py --count 3 --name=Alice   # Keyword order does not matter.
$ python main.py Alice --count 3          # Positional followed by keyword
$ python main.py --count 3 Alice          # Keywords can come before positional if the keyword is later in the function signature.
$ python main.py --count 3 -- Alice       # Using the POSIX convention to indicate the end of keywords

Like calling functions in python, positional arguments cannot be specified after a prior argument in the function signature was specified via keyword. For example, you cannot supply the count value "3" positionally while the value for name is specified via keyword:

# The following are NOT allowed.
$ python main.py --name=Alice 3  # invalid python: main(name="Alice", 3)
$ python main.py 3 --name=Alice  # invalid python: main(3, name="Alice")

Adding a Help Page

All CLI apps need to have a help page explaining how to use the application. By default, Cyclopts adds the --help (and the shortform -h) commands to your CLI. We can add application-level help documentation when creating our app:

from cyclopts import App

app = App(help="Help string for this demo application.")

@app.default
def main(name: str, count: int):
    for _ in range(count):
        print(f"Hello {name}!")

if __name__ == "__main__":
    app()
$ python main.py --help
Usage: main COMMAND [ARGS] [OPTIONS]

Help string for this demo application.

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

Help flags can be changed with help_flags <#cyclopts.App.help_flags>.

Let's add some help documentation for our parameters. Cyclopts uses the function's docstring and can interpret ReST, Google, Numpydoc-style and Epydoc docstrings (shoutout to docstring_parser <https://github.com/rr-/docstring_parser>).

from cyclopts import App

app = App()

@app.default
def main(name: str, count: int):
    """Help string for this demo application.

    Parameters
    ----------
    name: str
        Name of the user to be greeted.
    count: int
        Number of times to greet.
    """
    for _ in range(count):
        print(f"Hello {name}!")

if __name__ == "__main__":
    app()
$ python main.py --help
Usage: main COMMAND [ARGS] [OPTIONS]

Help string for this demo application.

╭─ Commands ──────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                           │
│ --version  Display application version.                             │
╰─────────────────────────────────────────────────────────────────────╯
╭─ Parameters ────────────────────────────────────────────────────────╮
│ *  NAME --name    Name of the user to be greeted. [required]        │
│ *  COUNT --count  Number of times to greet. [required]              │
╰─────────────────────────────────────────────────────────────────────╯
Note:

If App.help <#cyclopts.App.help> is not explicitly set, Cyclopts will fallback to the first line (short description) of the registered @app.default function's docstring.

Run

An alternative, terser API is available for simple applications with a single command. The run() <#cyclopts.run> function takes in a single callable (usually a function) and runs it as a Cyclopts application.

import cyclopts

def main(name: str, count: int):
    for _ in range(count):
        print(f"Hello {name}!")

if __name__ == "__main__":
    cyclopts.run(main)

The run() <#cyclopts.run> function is intentionally simple. If greater control is required, then use the conventional App <#cyclopts.App> interface.

Commands

There are two different ways of registering functions:

  1. app.default <#cyclopts.App.default> - Registers an action for when no registered command is provided. This was previously demonstrated in Getting Started <#getting-started>.

    A sub-app cannot be registered with app.default <#cyclopts.App.default>. If no default command is registered, Cyclopts will display the help-page.

  2. app.command <#cyclopts.App.command> - Registers a function or App <#cyclopts.App> as a command.

This section will detail how to use the @app.command <#cyclopts.App.command> decorator.

Registering a Command

The @app.command <#cyclopts.App.command> decorator adds a command to a Cyclopts application.

from cyclopts import App

app = App()

@app.command
def fizz(n: int):
    print(f"FIZZ: {n}")

@app.command
def buzz(n: int):
    print(f"BUZZ: {n}")

app()

We can now control which command runs from the CLI:

$ my-script fizz 3
FIZZ: 3

$ my-script buzz 4
BUZZ: 4

$ my-script fuzz
╭─ Error ────────────────────────────────────────────────────────────────────╮
│ Unknown command "fuzz". Did you mean "fizz"?                               │
╰────────────────────────────────────────────────────────────────────────────╯

Registering a SubCommand

The app.command <#cyclopts.App.command> method can also register another Cyclopts App <#cyclopts.App> as a command.

from cyclopts import App

app = App()
sub_app = App(name="foo")  # "foo" would be a better variable name than "sub_app".
# "sub_app" in this example emphasizes the name comes from name="foo".
app.command(sub_app)  # Registers sub_app to command "foo"
# Or, as a one-liner:  sub_app = app.command(App(name="foo"))


@sub_app.command
def bar(n: int):
    print(f"BAR: {n}")


# Alternatively, access subapps from app like a dictionary.
@app["foo"].command
def baz(n: int):
    print(f"BAZ: {n}")


app()
$ my-script foo bar 3
BAR: 3

$ my-script foo baz 4
BAZ: 4

The subcommand may have their own registered default action. Cyclopts's command structure is fully recursive.

Flattening SubCommands

Sometimes you want to make all commands from a sub-app directly accessible from the parent app, without requiring users to type the intermediate subcommand name.

You can flatten a sub-app by registering it with the special name="*":

from cyclopts import App

app = App()
tools_app = App(name="tools")

@tools_app.command
def compress(file: str):
    print(f"Compressing {file}")

@tools_app.command
def extract(file: str):
    print(f"Extracting {file}")

# Flatten: make all tools_app commands directly accessible
app.command(tools_app, name="*")

app()
$ my-script compress data.txt
Compressing data.txt

$ my-script extract archive.zip
Extracting archive.zip

Caveats of flattening:

  • Parent app commands take precedence over flattened commands if there are name collisions.
  • Multiple sub-apps can be flattened into the same parent app.
  • You cannot supply additional configuration kwargs when using name="*".
  • Only App <#cyclopts.App> instances can be flattened (not functions or import paths).

Flattening is useful for organizing related commands into logical groups in your code while keeping the CLI interface simple and flat.

SubCommand Configuration

Subcommands inherit configuration from their parent apps.

from cyclopts import App

# Root app with specific error handling
root_app = App(
    exit_on_error=False,
    print_error=False,
)

# Child app inherits parent's settings
child_app = root_app.command(App(name="child"))

@child_app.default
def child_action():
    return "Child executed successfully"

# Child can override parent settings if needed
grandchild_app = child_app.command(App(name="grandchild", exit_on_error=True))

When parent_app("child ...") is called, the child command will use the parent's error handling settings unless explicitly overridden.

Changing Command Name

By default, commands are registered to the python function's name with underscores replaced with hyphens. Any leading or trailing underscores will be stripped. For example, the function _foo_bar() will become the command foo-bar. This renaming is done because CLI programs generally tend to use hyphens instead of underscores. The name transform can be configured by App.name_transform <#cyclopts.App.name_transform>. For example, to make CLI command names be identical to their python function name counterparts, we can configure App <#cyclopts.App> as follows:

from cyclopts import App

app = App(name_transform=lambda s: s)

@app.command
def foo_bar():  # will now be "foo_bar" instead of "foo-bar"
    print("running function foo_bar")

app()
$ my-script foo_bar
running function foo_bar

Alternatively, the name can be manually changed in the @app.command <#cyclopts.App.command> decorator. Manually set names are not subject to App.name_transform <#cyclopts.App.name_transform>.

from cyclopts import App

app = App()

@app.command(name="bar")
def foo():  # function name will NOT be used.
    print("Hello World!")

app()
$ my-script bar
Hello World!

Finally, if you would like to register an additional name to the Cyclopts-derived names, you can set an alias <#cyclopts.App.alias>:

from cyclopts import App

app = App()

@app.command(alias="bar")
def foo():  # both "foo" and "bar" will trigger this function.
    print("Running foo.")

app()
$ my-script foo
Running bar.

$ my-script bar
Running bar.

Adding Help

There are a few ways to add a help string to a command:

  1. If the function has a docstring, the short description will be used as the help string for the command. This is generally the preferred method of providing help strings.
  2. If the registered command is a sub app, the sub app's help <#cyclopts.App.help> field will be used.

    sub_app = App(name="foo", help="Help text for foo.")
    app.command(sub_app)
  3. The help <#cyclopts.App.help> field of @app.command <#cyclopts.App.command>. If provided, the docstring or subapp help field will not be used.

    from cyclopts import App
    
    app = App()
    
    @app.command
    def foo():
        """Help string for foo."""
        pass
    
    @app.command(help="Help string for bar.")
    def bar():
        """This got overridden."""
    
    app()
    $ my-script --help
    ╭─ Commands ────────────────────────────────────────────────────────────╮
    │ bar        Help string for bar.                                       │
    │ foo        Help string for foo.                                       │
    │ --help,-h  Display this message and exit.                             │
    │ --version  Display application version.                               │
    ╰───────────────────────────────────────────────────────────────────────╯

Async

Cyclopts also works with async commands; when an async command is encountered, an event loop will be automatically created using the specified backend parameter (default asyncio <https://docs.python.org/3/library/asyncio.html#module-asyncio>).

import asyncio
from cyclopts import App

app = App()

@app.command
async def foo():
    await asyncio.sleep(10)

app()

When calling from within an existing async context, await <https://docs.python.org/3/reference/expressions.html#await> the async method run_async() <#cyclopts.App.run_async>:

async def main():
    result = await app.run_async(["foo"])
    # Instead of: app(["foo"]) which would raise RuntimeError

Decorated Function Details

Cyclopts does not modify the decorated function in any way. The returned function is the exact same function being decorated and can be used exactly as if it were not decorated by Cyclopts.

See Also

For improved CLI startup performance with large applications, see Lazy Loading <#lazy-loading>.

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 <https://docs.python.org/3/library/typing.html#typing.Annotated> type hints with Parameter <#cyclopts.Parameter>.

from cyclopts import App, Parameter
from typing import Annotated

app = App()

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

app()

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

Note:

Parameter <#cyclopts.Parameter> can also be used as a decorator. This is particularly useful for class definitions <#namespace-flattening>.

Naming

Like command names <#command-changing-name>, 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
    # Equivalently, you could have done Parameter(alias="-f")
    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 <#cyclopts.Parameter.name> are not subject to Parameter.name_transform <#cyclopts.Parameter.name_transform>. Alternatively, additional names can be added to the Cyclopts-derived names (instead of completely overriding them) with Parameter.alias <#cyclopts.Parameter.alias>.

Note:

Docstrings should always use the Python variable name from the function signature.

@app.default
def main(internal_name: Annotated[str, Parameter(name="external-name")]):
    """Command description.

    Parameters
    ----------
    internal_name:            # Use the Python variable name
        Help text here.
    """

This follows standard Python documentation conventions; the parameter will still appear as --external-name on the CLI.

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 <#cyclopts.Parameter.name_transform> (defaults to default_name_transform() <#cyclopts.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 <#default-parameter>.

To change the name_transform <#cyclopts.Parameter.name_transform> across your entire app, add the following to your App <#cyclopts.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 <#cyclopts.Parameter.converter> is required to transform the input string tokens into the desired type.

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 <#cyclopts.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.
   ),
)

Controlling Token Count

By default, Cyclopts infers how many tokens a parameter should consume from its type hint. For example, int <https://docs.python.org/3/library/functions.html#int> consumes 1 token, tuple[int, int] consumes 2, and list[int] consumes all remaining tokens. When using custom converters, you may need to override this inference with Parameter.n_tokens <#cyclopts.Parameter.n_tokens>:

from cyclopts import App, Parameter
from typing import Annotated

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

def parse_point(type_, tokens):
    """Parse a coordinate string like '10,20' into a Point."""
    x, y = tokens[0].value.split(",")
    return Point(int(x), int(y))

app = App()

@app.default
def main(pos: Annotated[Point, Parameter(n_tokens=1, converter=parse_point, accepts_keys=False)]):
    """Without n_tokens=1, Cyclopts would expect 2 tokens based on Point's __init__ signature."""
    print(f"Position: ({pos.x}, {pos.y})")

app()
$ my-script --pos 10,20
Position: (10, 20)

The Parameter.accepts_keys <#cyclopts.Parameter.accepts_keys> parameter prevents Cyclopts from generating nested options like --pos.x and --pos.y.

Alternative to the above syntax, you can directly decorate the converter function itself with Parameter <#cyclopts.Parameter> to define its behavior. This keeps all the information organized in a single location.

from cyclopts import App, Parameter
from typing import Annotated

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

@Parameter(n_tokens=1, accepts_keys=False)
def parse_point(type_, tokens):
    """Parse a coordinate string like '10,20' into a Point."""
    x, y = tokens[0].value.split(",")
    return Point(int(x), int(y))

app = App()

@app.default
def main(pos: Annotated[Point, Parameter(converter=parse_point)]):
    """The converter's n_tokens and accepts_keys are automatically inherited."""
    print(f"Position: ({pos.x}, {pos.y})")

app()
$ my-script --pos 10,20
Position: (10, 20)

You can also decorate classes directly with the converter:

@Parameter(converter=parse_point)
class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

@app.default
def main(pos: Point):
    """No Annotated wrapper needed - converter is part of the class definition."""
    print(f"Position: ({pos.x}, {pos.y})")

Using Classmethods as Converters

Converter functions are often closely associated with the class they create, making classmethods a natural choice. Cyclopts supports using classmethods as converters through forward string references (for class decoration) or direct references (for function annotations):

from cyclopts import App, Parameter
from typing import Annotated

# Decorate the classmethod to configure n_tokens and accepts_keys
# (decorator must go above @classmethod)
@Parameter(converter="parse")
class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    @Parameter(n_tokens=1, accepts_keys=False)
    @classmethod
    def parse(cls, tokens):
        """Parse a coordinate string like '10,20' into a Point.

        Note: classmethod signature is (cls, tokens), not (type_, tokens)
        """
        x, y = tokens[0].value.split(",")
        return cls(int(x), int(y))

app = App()

@app.default
def main(pos: Point):
    """The classmethod's n_tokens and accepts_keys are automatically inherited."""
    print(f"Position: ({pos.x}, {pos.y})")

app()
$ my-script --pos 10,20
Position: (10, 20)

Alternatively, you can reference the classmethod directly in function annotations:

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    @classmethod
    def parse(cls, tokens):
        x, y = tokens[0].value.split(",")
        return cls(int(x), int(y))

@app.default
def main(pos: Annotated[Point, Parameter(converter=Point.parse, n_tokens=1, accepts_keys=False)]):
    print(f"Position: ({pos.x}, {pos.y})")

Note on classmethod signatures: Classmethods used as converters should have the signature (cls, tokens) rather than (type_, tokens). Cyclopts automatically detects bound methods and calls them with just the tokens parameter, since cls is already bound.

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 <https://docs.python.org/3/library/exceptions.html#ValueError>, TypeError <https://docs.python.org/3/library/exceptions.html#TypeError>, AssertionError <https://docs.python.org/3/library/exceptions.html#AssertionError>) will be re-interpreted by Cyclopts and formatted into a prettier message for the application user.

Cyclopts has some builtin validators <#parameter-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 <#annotated-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 <#cyclopts.Parameter> annotations together. Say you want to define a new int <https://docs.python.org/3/library/functions.html#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 <#parameter-resolution-order> for more details.

Default Parameter

The default values of Parameter <#cyclopts.Parameter> for an app can be configured via App.default_parameter <#cyclopts.App.default_parameter>.

For example, to disable the negative <#cyclopts.Parameter.negative> flag feature across your entire app:

from cyclopts import App, Parameter

app = App(default_parameter=Parameter(negative=()))

@app.command
def foo(*, flag: bool):
    pass

app()

Consequently, --no-flag is no longer an allowed flag:

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

╭─ Parameters ──────────────────────────────────────────────────╮
│ *  --flag  [required]                                         │
╰───────────────────────────────────────────────────────────────╯

Explicitly annotating the parameter with  negative <#cyclopts.Parameter.negative> overrides this configuration and works as expected:

from cyclopts import App, Parameter
from typing import Annotated

app = App(default_parameter=Parameter(negative=()))

@app.command
def foo(*, flag: Annotated[bool, Parameter(negative="--anti-flag")]):
    pass

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

╭─ Parameters ──────────────────────────────────────────────────╮
│ *  --flag --anti-flag  [required]                             │
╰───────────────────────────────────────────────────────────────╯

Resolution Order

When resolving what the Parameter <#cyclopts.Parameter> values for an individual function parameter should be, explicitly set attributes of higher priority Parameter <#cyclopts.Parameter> s override lower priority Parameter <#cyclopts.Parameter> s. The resolution order is as follows:

  1. Highest Priority: Parameter-annotated command function signature Annotated[..., Parameter()].
  2. Group.default_parameter <#cyclopts.Group.default_parameter> that the parameter belongs to.
  3. App.default_parameter <#cyclopts.App.default_parameter> of the app that registered the command.
  4. Group.default_parameter <#cyclopts.Group.default_parameter> of the app that the function belongs to.
  5. Lowest Priority: (2-4) recursively of the parenting app call-chain.

Any of Parameter's fields can be set to None to revert back to the true-original Cyclopts default.

Skipping Private Parameters

The Parameter.parse <#cyclopts.Parameter.parse> attribute can accept a regex pattern to selectively skip parameters based on their name. This is useful for defining "private" parameters that are externally injected (e.g. a Meta App <#meta-app>, dependency-injection framework, etc) rather than parsed from the CLI.

For example, to skip all underscore-prefixed parameters:

from typing import Annotated
from cyclopts import App, Parameter

# The regex "^(?!_)" matches names that do NOT start with underscore.
app = App(default_parameter=Parameter(parse="^(?!_)"))

@app.command
def greet(name: str, *, _db: Database):
    user = _db.get_user(name)
    print(f"Hello {user.full_name}!")

@app.meta.default
def launcher(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)]):
    # Create shared resources
    db = Database("myapp.db")

    # Parse CLI and get ignored (non-parsed) parameters
    command, bound, ignored = app.parse_args(tokens)

    # Inject ignored parameters
    for name, type_ in ignored.items():
        if type_ is Database:
            bound.kwargs[name] = db

    return command(*bound.args, **bound.kwargs)

if __name__ == "__main__":
    app.meta()
$ my-script --help
Usage: my-script COMMAND

╭─ Commands ────────────────────────────────────────────────────╮
│ greet                                                         │
│ --help,-h  Display this message and exit.                     │
│ --version  Display application version.                       │
╰───────────────────────────────────────────────────────────────╯

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

╭─ Parameters ──────────────────────────────────────────────────╮
│ *  NAME,--name  [required]                                    │
╰───────────────────────────────────────────────────────────────╯

Notice that _db does not appear in the help screen. Parameters that don't match the regex pattern are added to the ignored dictionary returned by App.parse_args() <#cyclopts.App.parse_args>, making them available for meta-app injection.

Like all other Parameter configurations, explicitly annotating with parse=True overrides the app-level regex:

from typing import Annotated
from cyclopts import App, Parameter

app = App(default_parameter=Parameter(parse="^(?!_)"))

@app.default
def main(name: str, *, _verbose: Annotated[bool, Parameter(parse=True)] = False):
    """_verbose IS parsed despite the underscore prefix"""
Important:

Parameters that are not parsed (either via parse=False or a non-matching regex pattern) must be either:

  • Keyword-only (defined after * in the function signature), or
  • Have a default value

    # Valid: keyword-only parameter
    def main(*, _context: dict): ...
    
    # Valid: has default value
    def main(_context: dict = None): ...
    
    # Invalid: positional without default - raises ValueError
    def main(_context: dict): ...

Groups

Groups offer a way of organizing parameters and commands on the help-page; for example:

Usage: my-script.py create [OPTIONS]

╭─ Vehicle (choose one) ───────────────────────────────────────────────────────╮
│ --car    [default: False]                                                    │
│ --truck  [default: False]                                                    │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Engine ─────────────────────────────────────────────────────────────────────╮
│ --hp         [default: 200]                                                  │
│ --cylinders  [default: 6]                                                    │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Wheels ─────────────────────────────────────────────────────────────────────╮
│ --wheel-diameter  [default: 18]                                              │
│ --rims,--no-rims  [default: False]                                           │
╰──────────────────────────────────────────────────────────────────────────────╯

They also provide an additional abstraction layer that validators <#api-validators> can operate on.

Groups can be created in two ways:

  1. Explicitly creating a Group <#cyclopts.Group> object.
  2. Implicitly with a string. This will implicitly create a group, Group(my_str_group_name), if it doesn't exist. If there exists a Group <#cyclopts.Group> object with the same name within the command/parameter context, it will join that group.

    Warning:

    While convenient and terse, mistyping a group name will unintentionally create a new group!

Every command and parameter belongs to at least one group.

Group(s) can be provided to the group keyword argument of app.command <#cyclopts.App.command> and Parameter <#cyclopts.Parameter>. Like Parameter <#cyclopts.Parameter>, the Group <#cyclopts.Group> class itself only marks objects with metadata; the group does not contain direct references to it's members. This means that groups can be reused across commands.

Command Groups

An example of using groups to organize commands:

from cyclopts import App

app = App()

# Change the group of "--help" and "--version" to the implicitly created "Admin" group.
app["--help"].group = "Admin"
app["--version"].group = "Admin"

@app.command(group="Admin")
def info():
    """Print debugging system information."""
    print("Displaying system info.")

@app.command
def download(path, url):
    """Download a file."""
    print(f"Downloading {url} to {path}.")

@app.command
def upload(path, url):
    """Upload a file."""
    print(f"Downloading {url} to {path}.")

app()
$ python my-script.py --help
Usage: my-script.py COMMAND

╭─ Admin ──────────────────────────────────────────────────────────────────────╮
│ info       Print debugging system information.                               │
│ --help,-h  Display this message and exit.                                    │
│ --version  Display application version.                                      │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ download  Download a file.                                                   │
│ upload    Upload a file.                                                     │
╰──────────────────────────────────────────────────────────────────────────────╯

The default group is defined by the registering app's App.group_commands <#cyclopts.App.group_commands>, which defaults to a group named "Commands".

Parameter Groups

Like commands above, parameter groups allow us to organize parameters on the help page. They also allow us to add additional inter-parameter validators (e.g. mutually-exclusive parameters). An example of using groups with parameters:

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

app = App()

vehicle_type_group = Group(
    "Vehicle (choose one)",
    default_parameter=Parameter(negative=""),  # Disable "--no-" flags
    validator=validators.MutuallyExclusive(),  # Only one option is allowed to be selected.
)

@app.command
def create(
    *,  # force all subsequent variables to be keyword-only
    # Using an explicitly created group object.
    car: Annotated[bool, Parameter(group=vehicle_type_group)] = False,
    truck: Annotated[bool, Parameter(group=vehicle_type_group)] = False,
    # Implicitly creating an "Engine" group.
    hp: Annotated[float, Parameter(group="Engine")] = 200,
    cylinders: Annotated[int, Parameter(group="Engine")] = 6,
    # You can explicitly create groups in-line.
    wheel_diameter: Annotated[float, Parameter(group=Group("Wheels"))] = 18,
    # Groups within the function signature can always be referenced with a string.
    rims: Annotated[bool, Parameter(group="Wheels")] = False,
):
    pass

app()
$ python my-script.py create --help
Usage: my-script.py create [OPTIONS]

╭─ Engine ──────────────────────────────────────────────────────╮
│ --hp         [default: 200]                                   │
│ --cylinders  [default: 6]                                     │
╰───────────────────────────────────────────────────────────────╯
╭─ Vehicle (choose one) ────────────────────────────────────────╮
│ --car    [default: False]                                     │
│ --truck  [default: False]                                     │
╰───────────────────────────────────────────────────────────────╯
╭─ Wheels ──────────────────────────────────────────────────────╮
│ --wheel-diameter  [default: 18]                               │
│ --rims --no-rims  [default: False]                            │
╰───────────────────────────────────────────────────────────────╯

$ python my-script.py create --car --truck
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid values for group "Vehicle (choose one)". Mutually     │
│ exclusive arguments: {--car, --truck}                         │
╰───────────────────────────────────────────────────────────────╯

In this example, we use the MutuallyExclusive <#cyclopts.validators.MutuallyExclusive> validator to make it so the user can only specify --car or --truck.

The default groups are defined by the registering app:

  • App.group_arguments <#cyclopts.App.group_arguments> for positional-only arguments, which defaults to a group named "Arguments".
  • App.group_parameters <#cyclopts.App.group_parameters> for all other parameters, which defaults to a group named "Parameters".

Validators

Group validators offer a way of jointly validating group parameter members of CLI-provided values. Groups with an empty name, or with show=False, are a way of using group validators without impacting the help-page.

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

app = App()

mutually_exclusive = Group(
   # This Group has no name, so it won't impact the help page.
   validator=validators.MutuallyExclusive(),
   # show_default=False - Showing "[default: False]" isn't too meaningful for mutually-exclusive options.
   # negative="" - Don't create a "--no-" flag
   default_parameter=Parameter(show_default=False, negative=""),
)

@app.command
def foo(
    car: Annotated[bool, Parameter(group=(app.group_parameters, mutually_exclusive))] = False,
    truck: Annotated[bool, Parameter(group=(app.group_parameters, mutually_exclusive))] = False,
):
    print(f"{car=} {truck=}")

app()
$ python demo.py foo --help
Usage: demo.py foo [ARGS] [OPTIONS]

╭─ Parameters ──────────────────────────────────────────────────────╮
│ CAR,--car                                                         │
│ TRUCK,--truck                                                     │
╰───────────────────────────────────────────────────────────────────╯

$ python demo.py foo --car
car=True truck=False

$ python demo.py foo --truck
car=False truck=True

$ python demo.py foo --car --truck
╭─ Error ───────────────────────────────────────────────────────────╮
│  Mutually exclusive arguments: {--car, --truck}                   │
╰───────────────────────────────────────────────────────────────────╯

See Group.validator <#cyclopts.Group.validator> for details.

Cyclopts has some builtin group-validators for common use-cases. <#group-validators>

Help Page

Groups form titled panels on the help-page.

Groups with an empty name, or with show=False <#cyclopts.Group.show>, are not shown on the help-page. This is useful for applying additional grouping logic (such as applying a LimitedChoice <#cyclopts.validators.LimitedChoice> validator) without impacting the help-page.

By default, the ordering of panels is alphabetical. However, the sorting can be manipulated by Group.sort_key <#cyclopts.Group.sort_key>. See it's documentation for usage.

The Group.create_ordered() <#cyclopts.Group.create_ordered> convenience classmethod creates a Group <#cyclopts.Group> with a sort_key <#cyclopts.Group.sort_key> value drawn drawn from a global monotonically increasing counter. This means that the order in the help-page will match the order that the groups were instantiated.

from cyclopts import App, Group

app = App()

plants = Group.create_ordered("Plants")
animals = Group.create_ordered("Animals")
fungi = Group.create_ordered("Fungi")

@app.command(group=animals)
def zebra():
    pass

@app.command(group=plants)
def daisy():
    pass

@app.command(group=fungi)
def portobello():
    pass

app()
$ my-script --help

Usage: scratch.py COMMAND

╭─ Plants ───────────────────────────────────────────────────────────╮
│ daisy                                                              │
╰────────────────────────────────────────────────────────────────────╯
╭─ Animals ──────────────────────────────────────────────────────────╮
│ zebra                                                              │
╰────────────────────────────────────────────────────────────────────╯
╭─ Fungi ────────────────────────────────────────────────────────────╮
│ portobello                                                         │
╰────────────────────────────────────────────────────────────────────╯
╭─ Commands ─────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                          │
│ --version  Display application version.                            │
╰────────────────────────────────────────────────────────────────────╯

Even when using Group.create_ordered() <#cyclopts.Group.create_ordered>, a sort_key <#cyclopts.Group.sort_key> can still be supplied; the global counter will only be used to break sorting ties.

Parameter Validators

In CLI applications, users have the freedom to input a wide range of data. This flexibility can lead to inputs the application does not expect. By coercing the input into a data type (like an int <https://docs.python.org/3/library/functions.html#int>), we are already limiting the input to a certain degree (e.g. "foo" cannot be coerced into an integer). To further restrict the user input, you can populate the validator <#cyclopts.Parameter.validator> field of Parameter <#cyclopts.Parameter>.

A validator is any callable object (such as a function) that has the signature:

def validator(type_, value: Any) -> None:
    pass  # Raise any exception here if ``value`` is invalid.

Validation happens after the data converter runs. Any of AssertionError <https://docs.python.org/3/library/exceptions.html#AssertionError>, TypeError <https://docs.python.org/3/library/exceptions.html#TypeError> or ValidationError will be promoted to a cyclopts.ValidationError <#cyclopts.ValidationError> so that the exception gets presented to the end-user in a nicer way. More than one validator can be supplied as a list to the validator <#cyclopts.Parameter.validator> field.

Cyclopts has some builtin common validators in the cyclopts.validators <#api-validators> module. See Types <#annotated-types> for common specific definitions provided as convenient pre-annotated types.

Path

The Path <#cyclopts.validators.Path> validator ensures certain properties of the parsed pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> object, such as asserting the file must exist.

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

app = App()

@app.default()
def foo(path: Annotated[Path, Parameter(validator=validators.Path(exists=True))]):
    print(f"File contents:\n{path.read_text()}")

app()
$ echo Hello World > my_file.txt

$ my-script my_file.txt
File contents:
Hello World

$ my-script this_file_does_not_exist.txt
╭─ Error ────────────────────────────────────────────────────────────╮
│ Invalid value "this_file_does_not_exist.txt" for "PATH".           │
│ "this_file_does_not_exist.txt" does not exist.                     │
╰────────────────────────────────────────────────────────────────────╯

See Annotated Path Types <#annotated-path-types> for Annotated-Type equivalents of common Path converter/validators.

Number

The Number <#cyclopts.validators.Number> validator can set minimum and maximum input values.

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

app = App()

@app.default()
def foo(n: Annotated[int, Parameter(validator=validators.Number(gte=0, lt=16))]):
    print(f"Your number in hex is {str(hex(n))[2]}.")

app()
$ my-script 0
Your number in hex is 0.

$ my-script 15
Your number in hex is f.

$ my-script 16
╭─ Error ────────────────────────────────────────────────────────────╮
│ Invalid value "16" for "N". Must be < 16.                          │
╰────────────────────────────────────────────────────────────────────╯

See Annotated Number Types <#annotated-number-types> for Annotated-Type equivalents of common Number converter/validators.

Group Validators

Group validators operate on a set of parameters, ensuring that their values are mutually compatible <#parameter-groups>. Validator(s) for a group can be set via the Group.validator <#cyclopts.Group.validator> attribute. An individual validator is a callable object/function with signature:

def validator(argument_collection: ArgumentCollection):
    "Raise an exception if something is invalid."

Cyclopts has some builtin common group validators in the cyclopts.validators <#api-validators> module.

LimitedChoice

Limits the number of specified arguments within the group. Most commonly used for mutually-exclusive arguments (default behavior).

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

app = App()

vehicle = Group(
    "Vehicle (choose one)",
    default_parameter=Parameter(negative=""),  # Disable "--no-" flags
    validator=validators.LimitedChoice(),  # Mutually Exclusive Options
)

@app.default
def main(
    *,
    car: Annotated[bool, Parameter(group=vehicle)] = False,
    truck: Annotated[bool, Parameter(group=vehicle)] = False,
):
    if car:
        print("I'm driving a car.")
    if truck:
        print("I'm driving a truck.")

app()
$ python drive.py --help
Usage: main COMMAND [OPTIONS]

╭─ Commands ─────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                          │
│ --version  Display application version.                            │
╰────────────────────────────────────────────────────────────────────╯
╭─ Vehicle (choose one) ─────────────────────────────────────────────╮
│ --car    [default: False]                                          │
│ --truck  [default: False]                                          │
╰────────────────────────────────────────────────────────────────────╯

$ python drive.py --car
I'm driving a car.

$ python drive.py --car --truck
╭─ Error ────────────────────────────────────────────────────────────╮
│ Invalid values for group "Vehicle (choose one)". Mutually          │
│ exclusive arguments: {--car, --truck}                              │
╰────────────────────────────────────────────────────────────────────╯

See the LimitedChoice <#cyclopts.validators.LimitedChoice> docs for more info.

MutuallyExclusive

Alias for LimitedChoice <#cyclopts.validators.LimitedChoice> with default arguments. Exists primarily because the usage/implication will be more directly obvious and searchable to developers than LimitedChoice <#cyclopts.validators.LimitedChoice>. Since this class takes no arguments, an already instantiated version mutually_exclusive <#cyclopts.validators.mutually_exclusive> is also provided for convenience.

all_or_none

Group validator that enforces that either all parameters in the group must be supplied an argument, or none of them.

from typing import Annotated

from cyclopts import App, Group, Parameter
from cyclopts.validators import all_or_none

app = App()

group_1 = Group(validator=all_or_none)
group_2 = Group(validator=all_or_none)


@app.default
def default(
    foo: Annotated[bool, Parameter(group=group_1)] = False,
    bar: Annotated[bool, Parameter(group=group_1)] = False,
    fizz: Annotated[bool, Parameter(group=group_2)] = False,
    buzz: Annotated[bool, Parameter(group=group_2)] = False,
):
    print(f"{foo=} {bar=}")
    print(f"{fizz=} {buzz=}")


if __name__ == "__main__":
    app()
$ python all_or_none.py
foo=False bar=False
fizz=False buzz=False

$ python all_or_none.py --foo
╭─ Error ──────────────────────────────────────────────────────────╮
│ Missing argument: --bar                                          │
╰──────────────────────────────────────────────────────────────────╯

$ python all_or_none.py --foo --bar
foo=True bar=True
fizz=False buzz=False

$ python all_or_none.py --foo --bar --fizz
╭─ Error ────────────────────────────────────────────────────────────╮
│ Missing argument: --buzz                                           │
╰────────────────────────────────────────────────────────────────────╯

$ python all_or_none.py --foo --bar --fizz --buzz
foo=True bar=True
fizz=True buzz=True

See the all_or_none <#cyclopts.validators.all_or_none> docs for more info.

Help

A help screen is standard for every CLI application. Cyclopts by-default adds --help and -h flags to the application:

$ my-application --help
Usage: my-application COMMAND

My application short description.

╭─ Commands ─────────────────────────────────────────────────────────╮
│ foo        Foo help string.                                        │
│ bar        Bar help string.                                        │
│ --help -h  Display this message and exit.                          │
│ --version  Display application version.                            │
╰────────────────────────────────────────────────────────────────────╯

Cyclopts derives the components of the help string from a variety of sources. The source resolution order is as follows (as applicable):

  1. The help field in the @app.command <#cyclopts.App.command> decorator.

    app = cyclopts.App()
    
    
    @app.command(help="This is the highest precedence help-string for 'bar'.")
    def bar():
        pass

    When registering an App <#cyclopts.App> object, supplying help via the @app.command <#cyclopts.App.command> decorator is forbidden to reduce ambiguity and will raise a ValueError <https://docs.python.org/3/library/exceptions.html#ValueError>. See (2).

  2. Via App.help <#cyclopts.App.help>.

    app = cyclopts.App(help="This help string has highest precedence at the app-level.")
    
    sub_app = cyclopts.App(help="This is the help string for the 'foo' subcommand.")
    app.command(sub_app, name="foo")
    app.command(sub_app, name="foo", help="This is illegal and raises a ValueError.")
  3. The __doc__ docstring of the registered @app.default <#cyclopts.App.default> command. Cyclopts parses the docstring to populate short-descriptions and long-descriptions at the command-level, as well as at the parameter-level.

    app = cyclopts.App()
    app.command(cyclopts.App(), name="foo")
    
    
    @app.default
    def bar(val1: str):
        """This is the primary application docstring.
    
        Parameters
        ----------
        val1: str
            This will be parsed for val1 help-string.
        """
    
    
    @app["foo"].default  # You can access sub-apps like a dictionary.
    def foo_handler():
        """This will be shown for the "foo" command."""
    Note:

    Docstrings should always use the Python variable name from the function signature.

    @app.default
    def main(internal_name: Annotated[str, Parameter(name="external-name")]):
        """Command description.
    
        Parameters
        ----------
        internal_name:            # Use the Python variable name
            Help text here.
        """

    This follows standard Python documentation conventions; the parameter will still appear as --external-name on the CLI.

  4. This resolution order, but of the Meta App <#meta-app>.

    app = cyclopts.App()
    
    
    @app.meta.default
    def bar():
        """This is the primary application docstring."""

Markup Format

While the standard markup language for docstrings in Python is reStructuredText (see PEP-0287 <https://peps.python.org/pep-0287/>), Cyclopts defaults to Markdown for better readability and simplicity. Cyclopts mostly respects PEP-0257 <https://peps.python.org/pep-0257/>, but has some slight differences for developer ergonomics:

  1. The "summary line" (AKA short-description) may actually be multiple lines. Cyclopts will unwrap the first block of text and interpret it as the short description. The first block of text ends at the first double-newline (i.e. a single blank line) is reached.

    def my_command():
        """
        This entire sentence
        is part of the short description and will
        have all the newlines removed.
    
        This is the beginning of the long description.
        """
  2. If a docstring is provided with a long description, it must also have a short description.

By default, Cyclopts parses docstring descriptions as markdown and renders it appropriately. To change the markup format, set the App.help_format <#cyclopts.App.help_format> field accordingly. The different options are described below.

Subapps inherit their parent's App.help_format <#cyclopts.App.help_format> unless explicitly overridden. I.e. you only need to set App.help_format <#cyclopts.App.help_format> in your main root application for all docstrings to be parsed appropriately.

PlainText

Do not perform any additional parsing, display supplied text as-is.

from cyclopts import App

app = App(help_format="plaintext")

@app.default
def default():
    """My application summary.

    This is a pretty standard docstring; if there's a really long sentence
    I should probably wrap it because people don't like code that is more
    than 80 columns long.

    In this new paragraph, I would like to discuss the benefits of relaxing 80 cols to 120 cols.
    More text in this paragraph.

    Some new paragraph.
    """

app()
Usage: default COMMAND

My application summary.

This is a pretty standard docstring; if there's a really long
sentence
I should probably wrap it because people don't like code that is
more
than 80 columns long.

In this new paragraph, I would like to discuss the benefits of
relaxing 80 cols to 120 cols.
More text in this paragraph.

Some new paragraph.

╭─ Commands ─────────────────────────────────────────────────────╮
│ --help,-h  Display this message and exit.                      │
│ --version  Display application version.                        │
╰────────────────────────────────────────────────────────────────╯

Most noteworthy, is no additional text reflow is performed; newlines are presented as-is.

Rich

Displays text as Rich Markup <https://rich.readthedocs.io/en/stable/markup.html>.

Note:

Newlines are interpreted literally.

from cyclopts import App

app = App(help_format="rich")

@app.default
def default():
   """Rich can display colors like [red]red[/red] easily.

   However, I cannot be bothered to figure out how to show that in documentation.
   """

app()

ReStructuredText

ReStructuredText can be enabled by setting help_format to "restructuredtext" or "rst".

app = App(help_format="restructuredtext")  # or "rst"

@app.default
def default():
    """My application summary.

    We can do RST things like have **bold text**.
    More words in this paragraph.

    This is a new paragraph with some bulletpoints below:

    * bullet point 1.
    * bullet point 2.
    """

app()

Resulting help:

Under most circumstances, plaintext (without any additional markup) looks prettier and reflows better when interpreted as restructuredtext (or markdown, for that matter).

Markdown

Markdown is the default parsing behavior of Cyclopts, so help_format won't need to be explicitly set. It's another popular markup language that Cyclopts can render.

app = App(help_format="markdown")  # or "md"
# or don't supply help_format at all; markdown is default.


@app.default
def default():
    """My application summary.

    We can do markdown things like have **bold text**.
    [Hyperlinks work as well.](https://cyclopts.readthedocs.io)
    """

Resulting help:

Help Flags

The default --help flags can be changed to different name(s) via the help_flags parameter.

app = cyclopts.App(help_flags="--show-help")
app = cyclopts.App(help_flags=["--send-help", "--send-help-plz", "-h"])

To disable the help-page entirely, set help_flags to an empty string or iterable.

app = cyclopts.App(help_flags="")
app = cyclopts.App(help_flags=[])

Help Epilogue

An epilogue is text displayed at the end of the help screen, after all command and parameter panels. This is commonly used for version information, support contact details, or additional notes.

The epilogue is set via the App.help_epilogue <#cyclopts.App.help_epilogue> attribute:

from cyclopts import App

app = App(
    name="myapp",
    help="My application description.",
    help_epilogue="Support: support@example.com"
)

@app.default
def main():
    """Main command."""
    pass

app()
$ myapp --help
Usage: myapp [ARGS]

My application description.

╭─ Commands ────────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                             │
│ --version  Display application version.                               │
╰───────────────────────────────────────────────────────────────────────╯

Support: support@example.com

Like App.help_format <#cyclopts.App.help_format>, epilogues inherit from parent to child apps. This allows you to set a single epilogue that applies across your entire application:

parent = App(
    name="myapp",
    help_epilogue="Version 1.0.0 | support@example.com"
)

# Child inherits parent's epilogue
child = App(name="process", help="Process data files.")
parent.command(child)

# Another child overrides with its own epilogue
admin = App(
    name="admin",
    help="Admin commands.",
    help_epilogue="Admin Tools v2.0 | USE WITH CAUTION"
)
parent.command(admin)

parent()
$ myapp process --help
Usage: myapp process

Process data files.

Version 1.0.0 | support@example.com    # Inherited from parent

$ myapp admin --help
Usage: myapp admin

Admin commands.

Admin Tools v2.0 | USE WITH CAUTION    # Overridden by child

To disable the epilogue for a specific subcommand, set it to an empty string:

no_epilogue = App(name="internal", help_epilogue="")
parent.command(no_epilogue)

Help Customization

For advanced customization of help screen appearance, including custom formatters, styled panels, and dynamic column layouts, see Help Customization <#help-customization>.

Version

All CLI applications should have the basic ability to check the installed version; i.e.:

$ my-application --version
7.5.8

By default, Cyclopts adds a command, --version <#cyclopts.App.version_print>:, that does exactly this. Cyclopts try's to reasonably figure out your package's version by itself. The resolution order for determining the version string is as follows:

  1. An explicitly supplied version string or callable to the root Cyclopts application:

    from cyclopts import App
    
    app = App(version="7.5.8")
    
    app()

    If a callable is provided, it will be invoked when running the --version command:

    from cyclopts import App
    
    def get_my_application_version() -> str:
        return "7.5.8"
    
    app = App(version=get_my_application_version)
    app()
  2. The invoking-package's Distribution Package's Version Number <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package> via importlib.metadata.version <https://docs.python.org/3.12/library/importlib.metadata.html#distribution-versions>. Cyclopts attempts to derive the package module that instantiated the App <#cyclopts.App> object by traversing the call stack.
  3. The invoking-package's defacto PEP8 standard <https://peps.python.org/pep-0008/#module-level-dunder-names> __version__ string. Cyclopts attempts to derive the package module that instantiated the App <#cyclopts.App> object by traversing the call stack.

    # mypackage/__init__.py
    __version__ = "7.5.8"
    
    # mypackage/__main__.py
    # ``App`` will use ``mypackage.__version__``.
    app = cyclopts.App()
  4. The default version string "0.0.0" will be displayed.

In short, if your CLI application is a properly structured python package, Cyclopts will automatically derive the correct version.

The --version flag can be changed to a different name(s) via the version_flags parameter.

app = cyclopts.App(version_flags="--show-version")
app = cyclopts.App(version_flags=["--version", "-v"])

To disable the --version flag, set version_flags to an empty string or iterable.

app = cyclopts.App(version_flags="")
app = cyclopts.App(version_flags=[])

Shell Completion

Cyclopts provides shell completion (tab completion) for bash, zsh, and fish shells.

Development & Standalone Scripts

Shell completion systems (bash, zsh, fish) can only provide completion for installed commands (executables in your $PATH), not for arbitrary Python scripts like python myapp.py. This is a fundamental limitation of how shells work.

To work around this during development, Cyclopts provides a cyclopts run command that acts as a wrapper:

$ cyclopts run myapp.py --help
$ cyclopts run myapp.py:app --verbose

Since cyclopts itself is an installed command, the shell can provide completion for it. The cyclopts run command then loads and executes your script, giving you completion for your development scripts without needing to package and install them.

Script Path Format:

  • cyclopts run script.py - Auto-detects the App object. If an App object cannot be determined, it will raise an error.
  • cyclopts run script.py:app - Explicitly specifies the App object to run

This is particularly useful during development before packaging your application.

Virtual Environment Behavior:

cyclopts run imports your script directly into the same Python process (no subprocess is created). This means:

  • It uses whatever Python interpreter is currently running cyclopts
  • Your script has access to all packages installed in the current environment
  • You must install cyclopts in your project's virtual environment
  • To use: activate your venv, then run cyclopts run script.py

    $ source .venv/bin/activate  # or your venv activation method
    $ cyclopts run myapp.py
Note:

Completion for your script's commands comes through the cyclopts CLI completion. Install it once with: cyclopts --install-completion

Warning:

Performance: cyclopts run uses dynamic completion, which imports your script and calls Python on every tab press. This can be slow if your script has heavy imports.

To mitigate slow imports during development, consider using Lazy Loading <#lazy-loading> for your commands. For production or frequent use, install static completion using the methods below. Static completion is pre-generated and does not call Python, making it instantaneous.

To install completion specifically for your standalone script (without using cyclopts run), you can use the Manual Installation approach below with your script's App object.

Installation

Programmatic Installation (Recommended)

Add completion installation to your CLI application using App.register_install_completion_command <#cyclopts.App.register_install_completion_command>:

from cyclopts import App

app = App(name="myapp")
app.register_install_completion_command()

# Your commands here...

if __name__ == "__main__":
    app()

Users can then install completion by running:

myapp --install-completion

Manual Installation

For programmatic control, use App.install_completion <#cyclopts.App.install_completion> directly:

from cyclopts import App
from pathlib import Path

app = App(name="myapp")

# Install for current shell
install_path = app.install_completion()
print(f"Installed completion to {install_path}")

# Install for specific shell
install_path = app.install_completion(shell="zsh")

# Install to custom location
install_path = app.install_completion(
    shell="bash",
    output=Path("/custom/path/completion.sh"),
)

Default Installation Paths

  • Zsh: ~/.zsh/completions/_<app_name>
  • Bash: ~/.local/share/bash-completion/completions/<app_name>
  • Fish: ~/.config/fish/completions/<app_name>.fish

Script Generation

To generate a completion script without installing it, use App.generate_completion <#cyclopts.App.generate_completion>:

from cyclopts import App

app = App(name="myapp")
script = app.generate_completion(shell="zsh")
print(script)

Shell Configuration

By default, Cyclopts modifies your shell RC file to enable completion:

  • Zsh: Adds to ~/.zshrc
  • Bash: Adds to ~/.bashrc
  • Fish: No modification needed (automatically loads from ~/.config/fish/completions/)

After installation, restart your shell or source the RC file.

To install without modifying shell RC files, use:

app.register_install_completion_command(add_to_startup=False)

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 <#cyclopts.Parameter.converter> field. Typically, the converter <#cyclopts.Parameter.converter> function will receive a single token, but it may receive multiple tokens if the annotated type is iterable (e.g. list <https://docs.python.org/3/library/stdtypes.html#list>, set <https://docs.python.org/3/library/stdtypes.html#set>). The number of tokens can be explicitly controlled with n_tokens <#cyclopts.Parameter.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 <#cyclopts.Parameter.count> with an int <https://docs.python.org/3/library/functions.html#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 <#cyclopts.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:

    Cyclopts is stricter than traditional bool <https://docs.python.org/3/library/functions.html#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 <https://docs.python.org/3/library/stdtypes.html#str> and int <https://docs.python.org/3/library/functions.html#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 <#cyclopts.Parameter.allow_leading_hyphen> is False <https://docs.python.org/3/library/constants.html#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 <#cyclopts.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 <#cyclopts.Parameter.allow_leading_hyphen> is True <https://docs.python.org/3/library/constants.html#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 <#cyclopts.Parameter.allow_leading_hyphen> is False <https://docs.python.org/3/library/constants.html#False> (default behavior), reaching an option-like token will raise MissingArgumentError <#cyclopts.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 <#cyclopts.Parameter.consume_multiple> is True <https://docs.python.org/3/library/constants.html#True>, all remaining tokens will be consumed (until an option-like token is reached if Parameter.allow_leading_hyphen <#cyclopts.Parameter.allow_leading_hyphen> is False <https://docs.python.org/3/library/constants.html#False>)

    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]

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 <https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments>.

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 <#cyclopts.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 <https://docs.python.org/3/library/stdtypes.html#list>.

Sequence

Follows the same rules as List. The passed in data will be a list <https://docs.python.org/3/library/stdtypes.html#list>.

Set

Follows the same rules as List, but the resulting datatype is a set <https://docs.python.org/3/library/stdtypes.html#set>.

Frozenset

Follows the same rules as Set, but the resulting datatype is a frozenset <https://docs.python.org/3/library/stdtypes.html#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 <#args-kwargs-kwargs>.

Union

The unioned types will be iterated left-to-right until a successful coercion is performed. None <https://docs.python.org/3/library/constants.html#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 <https://docs.python.org/3/library/typing.html#typing.Literal> type is a good option for limiting user input to a set of choices. Like Union, the Literal <https://docs.python.org/3/library/typing.html#typing.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 <https://docs.python.org/3/library/typing.html#typing.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 for "VALUE": unable to convert "fizz"     │
│ into one of {'foo', 'bar', 3}.                          │
╰─────────────────────────────────────────────────────────╯

Enum

While Literal is the recommended way of providing the user a set of choices, another method is using Enum <https://docs.python.org/3/library/enum.html#enum.Enum>.

The Parameter.name_transform <#cyclopts.Parameter.name_transform> gets applied to all Enum <https://docs.python.org/3/library/enum.html#enum.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 <https://typer.tiangolo.com>, 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 <https://www.w3.org/TR/2003/REC-PNG-20031110/#6Colour-values>, which gets encoded into a 1-byte int in the image header <https://www.w3.org/TR/2003/REC-PNG-20031110/#11IHDR>.

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 <https://docs.python.org/3/library/enum.html#enum.Flag> enums (and by extension, IntFlag <https://docs.python.org/3/library/enum.html#enum.IntFlag>) are treated as a collection of boolean flags.

The Parameter.name_transform <#cyclopts.Parameter.name_transform> gets applied to all Flag <https://docs.python.org/3/library/enum.html#enum.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 <#namespace-flattening>.

date

Cyclopts supports parsing dates into a date <https://docs.python.org/3/library/datetime.html#datetime.date> object. It uses fromisoformat() <https://docs.python.org/3/library/datetime.html#datetime.date.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 <https://docs.python.org/3/library/datetime.html#datetime.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 <https://docs.python.org/3/library/datetime.html#datetime.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:

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 <#cyclopts.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 <#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.

Text Editor

Some CLI programs require users to edit more complex fields in a text editor. For example, git may open a text editor for the user when rebasing or editing a commit message. While not directly related to CLI command parsing, Cyclopts provides cyclopts.edit() <#cyclopts.edit> to satisfy this common need.

Here is an example application that mimics git commit functionality.

# git.py
import cyclopts
from textwrap import dedent
import sys

app = cyclopts.App(name="git")

@app.command
def commit():
    try:
        response = cyclopts.edit(  # blocks until text editor is closed.
            dedent(  # removes the  leading 4-tab indentation.
                """\


                # Please enter the commit message for your changes.Lines starting
                # with '#' will be ignored, and an empty message aborts the commit.
                """
            )
        )
    except (cyclopts.EditorDidNotSaveError, cyclopts.EditorDidNotChangeError):
        print("Aborting commit due to empty commit message.")
        sys.exit(1)
    filtered = "\n".join(x for x in response.split("\n") if not x.startswith("#"))
    filtered = filtered.strip()  # remove leading/trailing whitespace.
    print(f"Your commit message: {filtered}")

if __name__ == "__main__":
    app()

Running python git.py commit will bring up a text editor with the pre-defined text, and then return the contents of the file. For more interactive CLI prompting, we recommend using the questionary <https://github.com/tmbo/questionary> package. See edit() <#cyclopts.edit> API page for more advanced usage.

API

class cyclopts.App(name: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = None, help: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, usage: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, *, alias: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = None, default_command=None, default_parameter: Parameter <#cyclopts.Parameter> | None <https://docs.python.org/3/library/constants.html#None> = None, config: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = None, version: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], str <https://docs.python.org/3/library/stdtypes.html#str>] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], Coroutine <https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine>[Any <https://docs.python.org/3/library/typing.html#typing.Any>, Any <https://docs.python.org/3/library/typing.html#typing.Any>, str <https://docs.python.org/3/library/stdtypes.html#str>]] = None, version_flags: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = ['--version'], show: bool <https://docs.python.org/3/library/functions.html#bool> = True, console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, error_console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, help_flags: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = ['--help', '-h'], help_format: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['markdown', 'md', 'plaintext', 'restructuredtext', 'rst', 'rich'] | None <https://docs.python.org/3/library/constants.html#None> = None, help_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, help_epilogue: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, version_format: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['markdown', 'md', 'plaintext', 'restructuredtext', 'rst', 'rich'] | None <https://docs.python.org/3/library/constants.html#None> = None, group: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = None, group_arguments: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Group <#cyclopts.Group> = None, group_parameters: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Group <#cyclopts.Group> = None, group_commands: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Group <#cyclopts.Group> = None, validator: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = None, name_transform: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[str <https://docs.python.org/3/library/stdtypes.html#str>], str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None, sort_key: Any <https://docs.python.org/3/library/typing.html#typing.Any> = None, end_of_options_delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, print_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, exit_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, verbose: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, suppress_keyboard_interrupt: bool <https://docs.python.org/3/library/functions.html#bool> = True, backend: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['asyncio', 'trio'] | None <https://docs.python.org/3/library/constants.html#None> = None, help_formatter: None <https://docs.python.org/3/library/constants.html#None> | Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['default', 'plain'] | Any <https://docs.python.org/3/library/typing.html#typing.Any> = None, result_action: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = None)

Cyclopts Application.

name: str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None

Name of application, or subcommand if registering to another application. Name resolution order:

  1. User specified name parameter.
  2. If a default function has been registered, the name of that function.
  3. If the module name is __main__.py, the name of the encompassing package.
  4. The value of sys.argv[0] <https://docs.python.org/3/library/sys.html#sys.argv>; i.e. the name of the python script.

Multiple names can be provided in the case of a subcommand, but this is relatively unusual.

Special value "*" can be used when registering sub-apps with command() to flatten all commands from the sub-app into the parent app. See Flattening SubCommands <#flattening-subcommands> for details.

Example:

from cyclopts import App

app = App()
app.command(App(name="foo"))

@app["foo"].command
def bar():
    print("Running bar.")

app()
$ my-script foo bar
Running bar.
alias: str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None

Extends name with additional names. Unlike name, this does not override Cyclopts-derived names.

from cyclopts import App

app = App()

@app.command(alias="bar")
def foo():
    print("Running foo.")

app()
$ my-script foo
Running bar.

$ my-script bar
Running bar.
help: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Text to display on help screen. If not supplied, fallbacks to parsing the docstring of function registered with App.default().

from cyclopts import App

app = App(help="This is my help string.")
app()
$ my-script --help
Usage: scratch.py COMMAND

This is my help string.

╭─ Commands ────────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                             │
│ --version  Display application version.                               │
╰───────────────────────────────────────────────────────────────────────╯
help_flags: str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = ("--help", "-h")

CLI flags that trigger help_print(). Set to an empty list to disable this feature. Defaults to ["--help", "-h"].

help_format: Literal['plaintext', 'markdown', 'md', 'restructuredtext', 'rst'] | None <https://docs.python.org/3/library/constants.html#None> = None

The markup language used in function docstring. If None <https://docs.python.org/3/library/constants.html#None>, fallback to parenting help_format. If no help_format is defined, falls back to "markdown".

help_formatter: None <https://docs.python.org/3/library/constants.html#None> | Literal['default', 'plain'] | HelpFormatter <#cyclopts.help.protocols.HelpFormatter> = None

Help formatter to use for rendering help panels.

  • If None <https://docs.python.org/3/library/constants.html#None> (default), inherits from parent App, eventually defaulting to DefaultFormatter.
  • If "default", uses DefaultFormatter.
  • If "plain", uses PlainFormatter for no-frills plain text output.
  • If a callable (see HelpFormatter protocol), uses the provided formatter.

Example:

from cyclopts import App
from cyclopts.help import DefaultFormatter, PlainFormatter, PanelSpec

# Use plain text formatter
app = App(help_formatter="plain")

# Use default formatter with customization
app = App(help_formatter=DefaultFormatter(
    panel_spec=PanelSpec(border_style="blue")
))

See Help Customization <#help-customization> for detailed examples and advanced usage.

help_epilogue: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Text to display at the end of the help screen, after all help panels. Commonly used for version information, contact details, or additional notes. If None <https://docs.python.org/3/library/constants.html#None>, no epilogue is displayed. If not set, attempts to inherit from parenting App.

The epilogue supports the same formatting as help based on help_format (markdown, plaintext, restructuredtext, or rich).

Example:

from cyclopts import App

app = App(
    name="myapp",
    help="My application help.",
    help_epilogue="Support: support@example.com"
)
app()
$ my-script --help
Usage: myapp COMMAND

My application help.

╭─ Commands ────────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                             │
│ --version  Display application version.                               │
╰───────────────────────────────────────────────────────────────────────╯

Support: support@example.com
help_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Prints the help-page before printing an error. If not set, attempts to inherit from parenting App, eventually defaulting to False <https://docs.python.org/3/library/constants.html#False>.

print_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Print a rich-formatted error on error. If not set, attempts to inherit from parenting App, eventually defaulting to True <https://docs.python.org/3/library/constants.html#True>.

exit_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

If there is an error parsing the CLI tokens, invoke sys.exit(1) <https://docs.python.org/3/library/sys.html#sys.exit>. Otherwise, continue to raise the exception. If not set, attempts to inherit from parenting App, eventually defaulting to True <https://docs.python.org/3/library/constants.html#True>.

verbose: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Populate exception strings with more information intended for developers. If not set, attempts to inherit from parenting App, eventually defaulting to False <https://docs.python.org/3/library/constants.html#False>.

version_format: Literal['plaintext', 'markdown', 'md', 'restructuredtext', 'rst'] | None <https://docs.python.org/3/library/constants.html#None> = None

The markup language used in the version string. If None <https://docs.python.org/3/library/constants.html#None>, fallback to parenting version_format. If no version_format is defined, falls back to resolved help_format.

usage: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Text to be displayed in lieue of the default Usage: app COMMAND ... at the beginning of the help-page. Set to an empty-string "" to disable showing the default usage.

show: bool <https://docs.python.org/3/library/functions.html#bool> = True

Show this command on the help screen. Hidden commands (show=False) are still executable.

from cyclopts import App
app = App()

@app.command
def foo():
   print("Running foo.")

@app.command(show=False)
def bar():
   print("Running bar.")

app()
$ my-script foo
Running foo.

$ my-script bar
Running bar.

$ my-script --help
Usage: scratch.py COMMAND

╭─ Commands ─────────────────────────────────────────────────╮
│ foo                                                        │
│ --help -h  Display this message and exit.                  │
│ --version  Display application version.                    │
╰────────────────────────────────────────────────────────────╯
sort_key: Any = None

Modifies command display order on the help-page.

  1. If sort_key is a generator, it will be consumed immediately with next() <https://docs.python.org/3/library/functions.html#next> to get the actual value.
  2. If sort_key, or any of it's contents, are Callable, then invoke it sort_key(app) and apply the returned value to (3) if None <https://docs.python.org/3/library/constants.html#None>, (4) otherwise.
  3. For all commands with sort_key==None (default value), sort them alphabetically. These sorted commands will be displayed after sort_key != None list (see 4).
  4. For all commands with sort_key!=None, sort them by (sort_key, app.name). It is the user's responsibility that sort_key s are comparable.

Example usage:

from cyclopts import App

app = App()

@app.command  # sort_key not specified; will be sorted AFTER bob/charlie.
def alice():
    """Alice help description."""

@app.command(sort_key=2)
def bob():
    """Bob help description."""

@app.command(sort_key=1)
def charlie():
    """Charlie help description."""

app()

Resulting help-page:

Usage: demo.py COMMAND

╭─ Commands ──────────────────────────────────────────────────╮
│ charlie    Charlie help description.                        │
│ bob        Bob help description.                            │
│ alice      Alice help description.                          │
│ --help -h  Display this message and exit.                   │
│ --version  Display application version.                     │
╰─────────────────────────────────────────────────────────────╯

Using generators (e.g., itertools.count() <https://docs.python.org/3/library/itertools.html#itertools.count>):

import itertools
from cyclopts import App

app = App()
counter = itertools.count()

@app.command(sort_key=counter)
def beta():
    """Beta help description."""

@app.command(sort_key=counter)
def alpha():
    """Alpha help description."""

app()
Usage: demo.py COMMAND

╭─ Commands ──────────────────────────────────────────────────╮
│ beta       Beta help description.                           │
│ alpha      Alpha help description.                          │
│ --help -h  Display this message and exit.                   │
│ --version  Display application version.                     │
╰─────────────────────────────────────────────────────────────╯
version: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Callable = None

Version to be displayed when a version_flags is parsed. Defaults to the version of the package instantiating App. If a Callable <https://docs.python.org/3/library/typing.html#typing.Callable>, it will be invoked with no arguments when version is queried.

version_flags: str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = ("--version",)

Token(s) that trigger version_print(). Set to an empty list to disable version feature. Defaults to ["--version"].

console: Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> = None

Default Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> to use when displaying runtime messages. Cyclopts console resolution is as follows:

  1. Any explicitly passed in console to methods like App.__call__(), App.parse_args(), etc.
  2. The relevant subcommand's App.console attribute, if not None <https://docs.python.org/3/library/constants.html#None>.
  3. The parenting App.console (and so on), if not None <https://docs.python.org/3/library/constants.html#None>.
  4. If all values are None <https://docs.python.org/3/library/constants.html#None>, then the default Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> is used.
error_console: Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> = None

Default Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> to use when displaying error messages. Cyclopts error_console resolution is as follows:

  1. Any explicitly passed in error_console to methods like App.__call__(), App.parse_args(), etc.
  2. The relevant subcommand's App.error_console attribute, if not None <https://docs.python.org/3/library/constants.html#None>.
  3. The parenting App.error_console (and so on), if not None <https://docs.python.org/3/library/constants.html#None>.
  4. If all values are None <https://docs.python.org/3/library/constants.html#None>, then a default Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> with stderr=True is used.

This separation of error output from normal output follows Unix conventions, allowing users to redirect error messages independently from normal output (e.g., program > output.txt 2> errors.txt).

default_parameter: Parameter <#cyclopts.Parameter> = None

Default Parameter configuration. Unspecified values of command-annotated Parameter will inherit these values. See Default Parameter <#default-parameter> for more details.

group: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Group <#cyclopts.Group> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str> | Group <#cyclopts.Group>] = None

The group(s) that default_command belongs to.

group_commands: Group <#cyclopts.Group> = Group("Commands")

The default Group that sub-commands are assigned to.

group_arguments: Group <#cyclopts.Group> = Group("Arguments")

The default Group that positional-only parameters are assigned to.

group_parameters: Group <#cyclopts.Group> = Group("Parameters")

The default Group that non-positional-only parameters are assigned to.

validator: None <https://docs.python.org/3/library/constants.html#None> | Callable | list <https://docs.python.org/3/library/stdtypes.html#list>[Callable] = []

A function (or list of functions) where all the converted CLI-provided variables will be keyword-unpacked, regardless of their positional/keyword-type in the command function signature. The python variable names will be used, which may differ from their CLI names.

Example usage:

def validator(**kwargs):
    "Raise an exception if something is invalid."

This validator runs after Parameter and Group validators.

name_transform: Callable[[str <https://docs.python.org/3/library/stdtypes.html#str>], str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None

A function that converts function names to their CLI command counterparts.

The function must have signature:

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

The returned string should be without a leading --. If None <https://docs.python.org/3/library/constants.html#None> (default value), uses default_name_transform(). Subapps inherit from the first non-None <https://docs.python.org/3/library/constants.html#None> parent name_transform.

config: None <https://docs.python.org/3/library/constants.html#None> | Callable | Iterable[Callable] = None

A function or list of functions that are consecutively executed after parsing CLI tokens and environment variables. These function(s) are called before any conversion and validation. Each config function must have signature:

def config(app: "App", commands: Tuple[str, ...], arguments: ArgumentCollection):
    """Modifies given mapping inplace with some injected values.

    Parameters
    ----------
    app: App
       The current command app being executed.
    commands: Tuple[str, ...]
       The CLI strings that led to the current command function.
    arguments: ArgumentCollection
       Complete ArgumentCollection for the app.
       Modify this collection inplace to influence values provided to the function.
    """

The intended use-case of this feature is to allow users to specify functions that can load defaults from some external configuration. See cyclopts.config for useful builtins and Config Files <#config-files> for examples.

end_of_options_delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

All tokens after this delimiter will be force-interpreted as positional arguments. If not set, attempts to inherit from parenting App, eventually defaulting to POSIX-standard "--". Set to an empty string to disable.

suppress_keyboard_interrupt: bool <https://docs.python.org/3/library/functions.html#bool> = True

If the application receives a keyboard interrupt (Ctrl-C), suppress the error message and exit gracefully. Set to False <https://docs.python.org/3/library/constants.html#False> to let KeyboardInterrupt <https://docs.python.org/3/library/exceptions.html#KeyboardInterrupt> propagate normally.

backend: Literal['asyncio', 'trio'] | None <https://docs.python.org/3/library/constants.html#None> = None

The async backend to use when executing async commands. If not set, attempts to inherit from parenting App, eventually defaulting to "asyncio".

Example:

from cyclopts import App

app = App(backend="asyncio")

@app.default
async def main():
    await some_async_operation()

app()

The backend can also be overridden on a per-call basis:

app(backend="trio")  # Override the app's backend for this call
result_action: Literal['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable[[Any], Any] | Iterable[Literal[...] | Callable[[Any], Any]] | None <https://docs.python.org/3/library/constants.html#None> = None

Controls how App.__call__() and App.run_async() handle command return values. By default ("print_non_int_sys_exit"), the app will call sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with an appropriate exit code. This default was chosen for consistent functionality between standalone scripts, and console entrypoints.

Can be a predefined literal string, a custom callable that takes the result and returns a processed value, or a sequence of actions to be applied left-to-right in a pipeline.

Each predefined mode's exact behavior is shown below:

"print_non_int_sys_exit" (default)

The default CLI mode. Prints non-int values to stdout, then calls sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with the appropriate exit code.

if isinstance(result, bool):
    sys.exit(0 if result else 1)  # i.e. True is success
elif isinstance(result, int):
    sys.exit(result)
elif result is not None:
    print(result)
    sys.exit(0)
else:
    sys.exit(0)
"return_value"

Returns the command's value unchanged. Use for embedding Cyclopts in other Python code or testing.

return result
"call_if_callable"

Calls the result if it's callable (with no arguments), otherwise returns it unchanged. Useful for the dataclass command pattern where commands return class instances with __call__ methods. Intended to be used in composition with other result actions (e.g., ["call_if_callable", "print_non_int_sys_exit"]).

return result() if callable(result) else result

See Dataclass Commands <#dataclass-commands> for usage examples.

"sys_exit"

Never prints output. Calls sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with the appropriate exit code. Useful for CLI apps that handle their own output and just need exit code handling.

if isinstance(result, bool):
    sys.exit(0 if result else 1)  # i.e. True is success
elif isinstance(result, int):
    sys.exit(result)
else:
    sys.exit(0)
"print_non_int_return_int_as_exit_code"

Prints non-int values, returns int/bool as exit codes. Useful for testing and embedding.

if isinstance(result, bool):
    return 0 if result else 1  # i.e. True is success
elif isinstance(result, int):
    return result
elif result is not None:
    print(result)
    return 0
else:
    return 0
"print_str_return_int_as_exit_code"

Only prints string return values. Returns int/bool as exit codes, silently returns 0 for other types.

if isinstance(result, str):
    print(result)
    return 0
elif isinstance(result, bool):
    return 0 if result else 1  # i.e. True is success
elif isinstance(result, int):
    return result
else:
    return 0
"print_str_return_zero"

Only prints string return values, always returns 0. Useful for simple output-only CLIs.

if isinstance(result, str):
    print(result)
return 0
"print_non_none_return_int_as_exit_code"

Prints all non-None values (including ints), returns int/bool as exit codes.

if result is not None:
    print(result)
if isinstance(result, bool):
    return 0 if result else 1  # i.e. True is success
elif isinstance(result, int):
    return result
return 0
"print_non_none_return_zero"

Prints all non-None values (including ints), always returns 0.

if result is not None:
    print(result)
return 0
"return_int_as_exit_code_else_zero"

Never prints output. Returns int/bool as exit codes, 0 for all other types. Useful for silent CLIs.

if isinstance(result, bool):
    return 0 if result else 1  # i.e. True is success
elif isinstance(result, int):
    return result
else:
    return 0
"return_none"

Always returns None, regardless of the command's return value.

return None
"return_zero"

Always returns 0, regardless of the command's return value.

return 0
"print_return_zero"

Always prints the result (even None), then always returns 0.

print(result)
return 0
"sys_exit_zero"

Always calls sys.exit(0) <https://docs.python.org/3/library/sys.html#sys.exit>, regardless of the command's return value.

sys.exit(0)
"print_sys_exit_zero"

Always prints the result (even None), then calls sys.exit(0) <https://docs.python.org/3/library/sys.html#sys.exit>.

print(result)
sys.exit(0)
Custom Callable

Provide a function for fully custom result handling. Receives the command's return value and returns a processed value.

def custom_handler(result):
    if result is None:
        return 0
    elif isinstance(result, str):
        print(f"[OUTPUT] {result}")
        return 0
    return result

app = App(result_action=custom_handler)
Sequence of Actions

Provide a sequence (list or tuple) of actions to create a result-processing pipeline. Actions are applied left-to-right, with each action receiving the result of the previous action.

def uppercase(result):
    return result.upper() if isinstance(result, str) else result

def add_prefix(result):
    return f"[OUTPUT] {result}" if isinstance(result, str) else result

# Pipeline: result → uppercase → add_prefix → return
app = App(result_action=[uppercase, add_prefix, "return_value"])

@app.command
def greet(name: str) -> str:
    return f"hello {name}"

result = app(["greet", "world"])
# result == "[OUTPUT] HELLO WORLD"

Actions in a sequence can be any combination of predefined literal strings and custom callables. Empty sequences raise a ValueError at app initialization.

Example:

from cyclopts import App

# For CLI applications with console_scripts entry points
app = App(result_action="print_non_int_return_int_as_exit_code")

@app.command
def greet(name: str) -> str:
    return f"Hello {name}!"

app()

See Result Action <#result-action> for detailed examples and usage patterns.

version_print(console: Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated>[Console | None <https://docs.python.org/3/library/constants.html#None>, Parameter <#cyclopts.Parameter>(parse=False)] = None) -> None <https://docs.python.org/3/library/constants.html#None>

Print the application version.

Parameters

console (Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console>) -- Console to print version string to. If not provided, follows the resolution order defined in App.console.

__getitem__(key: str <https://docs.python.org/3/library/stdtypes.html#str>) -> App <#cyclopts.App>

Get the subapp from a command string.

All commands get registered to Cyclopts as subapps. The actual function handler is at app[key].default_command.

If the command was registered via lazy loading (import path string), it will be imported and resolved on first access.

Example usage:

from cyclopts import App

app = App()
app.command(App(name="foo"))


@app["foo"].command
def bar():
    print("Running bar.")


app()
__iter__() -> Iterator <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator>[str <https://docs.python.org/3/library/stdtypes.html#str>]

Iterate over command & meta command names.

Example usage:

from cyclopts import App

app = App()


@app.command
def foo():
    pass


@app.command
def bar():
    pass


# help and version flags are treated as commands.
assert list(app) == ["--help", "-h", "--version", "foo", "bar"]
parse_commands(tokens: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, include_parent_meta=True) -> tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...], tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[App <#cyclopts.App>, ...], list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>]]

Extract out the command tokens from a command.

You are probably actually looking for parse_args().

Parameters
Returns
  • tuple[str, ...] -- Strings that are interpreted as a valid command chain.
  • tuple[App, ...] -- The execution path - apps that will be invoked in order.
  • list[str] -- The remaining non-command tokens.
command(obj: T, name: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, alias: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, **kwargs: object <https://docs.python.org/3/library/functions.html#object>) -> T
command(obj: None <https://docs.python.org/3/library/constants.html#None> = None, name: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, alias: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, **kwargs: object <https://docs.python.org/3/library/functions.html#object>) -> Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[T], T]
command(obj: str <https://docs.python.org/3/library/stdtypes.html#str>, name: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, alias: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, **kwargs: object <https://docs.python.org/3/library/functions.html#object>) -> None <https://docs.python.org/3/library/constants.html#None>

Decorator to register a function as a CLI command.

Example usage:

from cyclopts import App

app = App()

@app.command
def foo():
    print("foo!")

@app.command(name="buzz")
def bar():
    print("bar!")

# Lazy loading via import path
app.command("myapp.commands:create_user", name="create")

app()
$ my-script foo
foo!

$ my-script buzz
bar!

$ my-script create
# Imports and runs myapp.commands:create_user
Parameters
  • obj (Callable | App <#cyclopts.App> | str <https://docs.python.org/3/library/stdtypes.html#str> | None) -- Function, App, or import path string to be registered as a command. For lazy loading, provide a string in format "module.path:function_or_app_name".
  • name (None | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>]) --

    Name(s) to register the command to. If not provided, defaults to:

    • If registering an App, then the app's name.
    • If registering a function, then the function's name after applying name_transform.
    • If registering via import path, then the attribute name after applying name_transform.

    Special value "*" flattens all sub-App commands into this app (App instances only). See Flattening SubCommands <#flattening-subcommands> for details.

  • **kwargs -- Any argument that App can take.
default(obj: T, *, validator: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], Any <https://docs.python.org/3/library/typing.html#typing.Any>] | None <https://docs.python.org/3/library/constants.html#None> = None) -> T
default(obj: None <https://docs.python.org/3/library/constants.html#None> = None, *, validator: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], Any <https://docs.python.org/3/library/typing.html#typing.Any>] | None <https://docs.python.org/3/library/constants.html#None> = None) -> Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[T], T]

Decorator to register a function as the default action handler.

Example usage:

from cyclopts import App

app = App()


@app.default
def main():
    print("Hello world!")


app()
$ my-script
Hello world!
assemble_argument_collection(*, default_parameter: Parameter <#cyclopts.Parameter> | None <https://docs.python.org/3/library/constants.html#None> = None, parse_docstring: bool <https://docs.python.org/3/library/functions.html#bool> = False) -> ArgumentCollection <#cyclopts.ArgumentCollection>

Assemble the argument collection for this app.

Parameters
Returns

All arguments for this app.

Return type

ArgumentCollection <#cyclopts.ArgumentCollection>

parse_known_args(tokens: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, error_console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, end_of_options_delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None) -> tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], Any <https://docs.python.org/3/library/typing.html#typing.Any>], BoundArguments <https://docs.python.org/3/library/inspect.html#inspect.BoundArguments>, list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>], dict <https://docs.python.org/3/library/stdtypes.html#dict>[str <https://docs.python.org/3/library/stdtypes.html#str>, Any <https://docs.python.org/3/library/typing.html#typing.Any>]]

Interpret arguments into a registered function, BoundArguments <https://docs.python.org/3/library/inspect.html#inspect.BoundArguments>, and any remaining unknown tokens.

Parameters
Returns
  • command (Callable) -- Bare function to execute.
  • bound (inspect.BoundArguments) -- Bound arguments for command.
  • unused_tokens (list[str]) -- Any remaining CLI tokens that didn't get parsed for command.
  • ignored (dict[str, Any]) -- A mapping of python-variable-name to annotated type of any parameter with annotation parse=False. Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated> will be resolved. Intended to simplify meta apps <#meta-app>.
parse_args(tokens: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, error_console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, print_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, exit_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, help_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, verbose: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, end_of_options_delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None) -> tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>, BoundArguments <https://docs.python.org/3/library/inspect.html#inspect.BoundArguments>, dict <https://docs.python.org/3/library/stdtypes.html#dict>[str <https://docs.python.org/3/library/stdtypes.html#str>, Any <https://docs.python.org/3/library/typing.html#typing.Any>]]

Interpret arguments into a function and BoundArguments <https://docs.python.org/3/library/inspect.html#inspect.BoundArguments>.

Raises

UnusedCliTokensError <#cyclopts.UnusedCliTokensError> -- If any tokens remain after parsing.

Parameters
Returns
  • command (Callable) -- Function associated with command action.
  • bound (inspect.BoundArguments) -- Parsed and converted args and kwargs to be used when calling command.
  • ignored (dict[str, Any]) -- A mapping of python-variable-name to type-hint of any parameter with annotation parse=False. Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated> will be resolved. Intended to simplify meta apps <#meta-app>.
__call__(tokens: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, error_console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, print_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, exit_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, help_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, verbose: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, end_of_options_delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, backend: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['asyncio', 'trio'] | None <https://docs.python.org/3/library/constants.html#None> = None, result_action: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>] | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>]] | None <https://docs.python.org/3/library/constants.html#None> = None) -> Any <https://docs.python.org/3/library/typing.html#typing.Any>

Interprets and executes a command.

Parameters
Returns

return_value -- The value the command function returns.

Return type

Any

async run_async(tokens: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, *, console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, error_console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, print_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, exit_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, help_on_error: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, verbose: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, end_of_options_delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, backend: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['asyncio', 'trio'] | None <https://docs.python.org/3/library/constants.html#None> = None, result_action: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>] | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>]] | None <https://docs.python.org/3/library/constants.html#None> = None) -> Any <https://docs.python.org/3/library/typing.html#typing.Any>

Async equivalent of __call__() for use within existing event loops.

This method should be used when you're already in an async context (e.g., Jupyter notebooks, existing async applications) and need to execute a Cyclopts command without creating a new event loop.

Parameters
Returns

return_value -- The value the command function returns.

Return type

Any

Examples

import asyncio
from cyclopts import App

app = App()

@app.command
async def my_async_command():
    await asyncio.sleep(1)
    return "Done!"


# In an async context (e.g., Jupyter notebook or existing async app):
async def main():
    result = await app.run_async(["my-async-command"])
    print(result)  # Prints: Done!


asyncio.run(main())
help_print(tokens: Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated>[None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>], Parameter <#cyclopts.Parameter>(show=False)] = None, *, console: Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated>[Console | None <https://docs.python.org/3/library/constants.html#None>, Parameter <#cyclopts.Parameter>(parse=False)] = None) -> None <https://docs.python.org/3/library/constants.html#None>

Print the help page.

Parameters
generate_docs(output_format: DocFormat = 'markdown', recursive: bool <https://docs.python.org/3/library/functions.html#bool> = True, include_hidden: bool <https://docs.python.org/3/library/functions.html#bool> = False, heading_level: int <https://docs.python.org/3/library/functions.html#int> = 1, max_heading_level: int <https://docs.python.org/3/library/functions.html#int> = 6, flatten_commands: bool <https://docs.python.org/3/library/functions.html#bool> = False) -> str <https://docs.python.org/3/library/stdtypes.html#str>

Generate documentation for this CLI application.

Parameters
Returns

The generated documentation.

Return type

str <https://docs.python.org/3/library/stdtypes.html#str>

Raises

ValueError <https://docs.python.org/3/library/exceptions.html#ValueError> -- If an unsupported output format is specified.

Examples

>>> app = App(name="myapp", help="My CLI Application")
>>> docs = app.generate_docs()  # Generate markdown as string
>>> html_docs = app.generate_docs(output_format="html")  # Generate HTML
>>> rst_docs = app.generate_docs(output_format="rst")  # Generate RST
>>> # To write to file, caller can do:
>>> # Path("docs/cli.md").write_text(docs)
generate_completion(*, prog_name: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, shell: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['zsh', 'bash', 'fish'] | None <https://docs.python.org/3/library/constants.html#None> = None) -> str <https://docs.python.org/3/library/stdtypes.html#str>

Generate shell completion script for this application.

Parameters
  • prog_name (str <https://docs.python.org/3/library/stdtypes.html#str> | None) -- Program name for completion. If None, uses first name from app.name.
  • shell (Literal["zsh", "bash", "fish"] | None) -- Shell type. If None, automatically detects current shell. Supported shells: "zsh", "bash", "fish".
Returns

Complete shell completion script.

Return type

str <https://docs.python.org/3/library/stdtypes.html#str>

Examples

Auto-detect shell and generate completion:

>>> app = App(name="myapp")
>>> script = app.generate_completion()
>>> Path("_myapp").write_text(script)

Explicitly specify shell type:

>>> script = app.generate_completion(shell="zsh")
Raises
install_completion(*, shell: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['zsh', 'bash', 'fish'] | None <https://docs.python.org/3/library/constants.html#None> = None, output: Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> | None <https://docs.python.org/3/library/constants.html#None> = None, add_to_startup: bool <https://docs.python.org/3/library/functions.html#bool> = True) -> Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>

Install shell completion script to appropriate location.

Generates and writes the completion script to a shell-specific location.

Parameters
  • shell (Literal["zsh", "bash", "fish"] | None) -- Shell type for completion. If not specified, attempts to auto-detect current shell.
  • output (Path <#cyclopts.validators.Path> | None) -- Output path for the completion script. If not specified, uses shell-specific default: - zsh: ~/.zsh/completions/_<prog_name> (or $ZSH_CUSTOM/completions/_<prog_name> with oh-my-zsh) - bash: ~/.local/share/bash-completion/completions/<prog_name> - fish: ~/.config/fish/completions/<prog_name>.fish
  • add_to_startup (bool <https://docs.python.org/3/library/functions.html#bool>) -- If True (default), adds source line to shell RC file to ensure completion is loaded. Set to False if completions are already configured to auto-load.
Returns

Path where the completion script was installed.

Return type

Path <#cyclopts.validators.Path>

Examples

Auto-detect shell and install:

>>> app = App(name="myapp")
>>> path = app.install_completion()

Install for specific shell:

>>> path = app.install_completion(shell="zsh")

Install to custom path:

>>> path = app.install_completion(output=Path("/custom/path"))

Install without modifying RC files:

>>> path = app.install_completion(shell="bash", add_to_startup=False)
Raises
register_install_completion_command(name: str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = '--install-completion', add_to_startup: bool <https://docs.python.org/3/library/functions.html#bool> = True, **kwargs) -> None <https://docs.python.org/3/library/constants.html#None>

Register a command for installing shell completion.

This is a convenience method that creates a command which calls install_completion(). For more control over the command implementation, users can manually define their own command.

Parameters

Examples

Register install-completion command:

>>> app = App(name="myapp")
>>> app.register_install_completion_command()
>>> app()  # Now responds to: myapp --install-completion

Use a custom command name:

>>> app.register_install_completion_command(name="--setup-completion")

Customize help text:

>>> app.register_install_completion_command(help="Install shell completion for myapp.")

Customize command registration:

>>> app.register_install_completion_command(group="Setup", help_flags=[])

Install without modifying RC files:

>>> app.register_install_completion_command(add_to_startup=False)

See also:

install_completion

The underlying method that performs the installation.

interactive_shell(prompt: str <https://docs.python.org/3/library/stdtypes.html#str> = '$ ', quit: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[str <https://docs.python.org/3/library/stdtypes.html#str>] = None, dispatcher: Dispatcher | None <https://docs.python.org/3/library/constants.html#None> = None, console: Console | None <https://docs.python.org/3/library/constants.html#None> = None, exit_on_error: bool <https://docs.python.org/3/library/functions.html#bool> = False, result_action: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>] | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>]] | None <https://docs.python.org/3/library/constants.html#None> = None, **kwargs) -> None <https://docs.python.org/3/library/constants.html#None>

Create a blocking, interactive shell.

All registered commands can be executed in the shell.

Parameters
update(app: App <#cyclopts.App>)

Copy over all commands from another App.

Commands from the meta app will not be copied over.

Parameters

app (cyclopts.App <#cyclopts.App>) -- All commands from this application will be copied over.

class cyclopts.Parameter(name=None, *, converter: ~collections.abc.Callable[[...], ~typing.Any] | str | None = None, validator=(), alias=None, negative: None | ~typing.Any | ~collections.abc.Iterable[~typing.Any] = None, group: None | ~typing.Any | ~collections.abc.Iterable[~typing.Any] = None, parse=None, show: bool | None = None, show_default: None | bool | ~collections.abc.Callable[[~typing.Any], ~typing.Any] = None, show_choices=None, help: str | None = None, show_env_var=None, env_var=None, env_var_split: ~collections.abc.Callable = <function env_var_split>, negative_bool=None, negative_iterable=None, negative_none=None, required: bool | None = None, allow_leading_hyphen: bool = False, requires_equals: bool = False, name_transform: ~collections.abc.Callable[[str], str] | None = None, accepts_keys: bool | None = None, consume_multiple=None, json_dict: bool | None = None, json_list: bool | None = None, count=None, n_tokens: int | None = None)

Cyclopts configuration for individual function parameters with Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated>.

Example usage:

from cyclopts import app, Parameter
from typing import Annotated

app = App()


@app.default
def main(foo: Annotated[int, Parameter(name="bar")]):
    print(foo)


app()
$ my-script 100
100

$ my-script --bar 100
100
name: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = None

Name(s) to expose to the CLI. If not specified, cyclopts will apply name_transform to the python parameter name.

from cyclopts import App, Parameter
from typing import Annotated

app = App()

@app.default
def main(foo: Annotated[int, Parameter(name=("bar", "-b"))]):
   print(f"{foo=}")

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

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

$ my-script --bar 100
foo=100

$ my-script -b 100
foo=100

If specifying name in a nested data structure (e.g. a dataclass), beginning the name with a hyphen - will override any hierarchical dot-notation.

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

app = App()

@dataclass
class User:
   id: int  # default behavior
   email: Annotated[str, Parameter(name="--email")]  # overrides
   pwd: Annotated[str, Parameter(name="password")]  # dot-notation with parent

@app.command
def create(user: User):
   print(f"Creating {user=}")

app()
$ my-script create --help
Usage: scratch.py create [ARGS] [OPTIONS]

╭─ Parameters ───────────────────────────────────────────────────╮
│ *  USER.ID --user.id  [required]                               │
│ *  EMAIL --email      [required]                               │
│ *  USER.PASSWORD      [required]                               │
│      --user.password                                           │
╰────────────────────────────────────────────────────────────────╯
alias: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = None

Additional name(s) to expose to the CLI. Unlike name, this does not override Cyclopts-derived names.

The following two examples are functionally equivalent:

@app.default
def main(foo: Annotated[int, Parameter(name=["--foo", "-f"])]):
    pass
@app.default
def main(foo: Annotated[int, Parameter(alias="-f")]):
    pass
converter: Callable | None <https://docs.python.org/3/library/constants.html#None> = None

A function that converts tokens into an object. The converter should have signature:

def converter(type_, tokens) -> Any:
    pass

Where type_ is the parameter's type hint, and tokens is either:

  • A list[cyclopts.Token] of CLI tokens (most commonly).

    from cyclopts import App, Parameter
    from typing import Annotated
    
    app = App()
    
    def converter(type_, tokens):
       assert type_ == tuple[int, int]
       return tuple(2 * int(x.value) for x in tokens)
    
    @app.default
    def main(coordinates: Annotated[tuple[int, int], Parameter(converter=converter)]):
       print(f"{coordinates=}")
    
    app()
    $ python my-script.py 7 12
    coordinates=(14, 24)
  • A dict of Token if keys are specified in the CLI. E.g.

    $ python my-script.py --foo.key1=val1

    would be parsed into:

    tokens = {
       "key1": ["val1"],
    }

If not provided, defaults to Cyclopts's internal coercion engine. If a pydantic type-hint is provided, Cyclopts will disable its internal coercion engine (including this converter argument) and leave the coercion to pydantic.

The number of tokens passed to the converter is inferred from the type hint by default, but can be explicitly controlled with n_tokens. This is useful when the type signature doesn't match the desired CLI token consumption. When loading complex objects with multiple fields, it may also be useful to combine with accepts_keys.

Decorating Converters: Converter functions can be decorated with Parameter to define reusable conversion behavior:

@Parameter(n_tokens=1, accepts_keys=False)
def load_from_id(type_, tokens):
    """Load object from database by ID."""
    return fetch_from_db(tokens[0].value)

@app.default
def main(obj: Annotated[MyType, Parameter(converter=load_from_id)]):
    # Automatically inherits n_tokens=1 and accepts_keys=False
    pass

Classmethod Support: Converters can be classmethods. Use string references for class decoration or direct references in annotations. Classmethod signature should be (cls, tokens) instead of (type_, tokens):

@Parameter(converter="from_env")
class Config:
    @Parameter(n_tokens=1, accepts_keys=False)
    @classmethod
    def from_env(cls, tokens):
        env = tokens[0].value
        configs = {"dev": ("localhost", 8080), "prod": ("api.example.com", 443)}
        return cls(*configs[env])
validator: None <https://docs.python.org/3/library/constants.html#None> | Callable | Iterable[Callable] = None

A function (or list of functions) that validates data returned by the converter.

def validator(type_, value: Any) -> None:
    pass  # Raise a TypeError, ValueError, or AssertionError here if data is invalid.
group: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Group <#cyclopts.Group> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str> | Group <#cyclopts.Group>] = None

The group(s) that this parameter belongs to. This can be used to better organize the help-page, and/or to add additional conversion/validation logic (such as ensuring mutually-exclusive arguments).

If None <https://docs.python.org/3/library/constants.html#None>, defaults to one of the following groups:

  1. Parenting App.group_arguments if the parameter is POSITIONAL_ONLY. By default, this is Group("Arguments").
  2. Parenting App.group_parameters otherwise. By default, this is Group("Parameters").

See Groups <#groups> for examples.

negative: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = None

Name(s) for empty iterables or false boolean flags.

  • For booleans, defaults to no-{name} (see negative_bool).
  • For iterables, defaults to empty-{name} (see negative_iterable).

Set to an empty list or string to disable the creation of negative flags.

Example usage:

from cyclopts import App, Parameter
from typing import Annotated

app = App()

@app.default
def main(*, verbose: Annotated[bool, Parameter(negative="--quiet")] = False):
   print(f"{verbose=}")

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

╭─ Commands ─────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                      │
│ --version  Display application version.                        │
╰────────────────────────────────────────────────────────────────╯
╭─ Parameters ───────────────────────────────────────────────────╮
│ --verbose --quiet  [default: False]                            │
╰────────────────────────────────────────────────────────────────╯
negative_bool: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Prefix for negative boolean flags. Defaults to "no-".

negative_iterable: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Prefix for empty iterables (like lists and sets) flags. Defaults to "empty-".

negative_none: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Prefix for setting optional parameters to None <https://docs.python.org/3/library/constants.html#None>. Not enabled by default (no prefixes set).

Example:

from pathlib import Path
from typing import Annotated

from cyclopts import App, Parameter

app = App(
   default_parameter=Parameter(negative_none="none-")
)

@app.default
def default(path: Path | None = Path("data.bin")):
    print(f"{path=}")

app()
$ my-script
path=PosixPath('data.bin')

$ my-script --path=cat.jpeg
path=PosixPath('cat.jpeg')

$ my-script --none-path
path=None
allow_leading_hyphen: bool <https://docs.python.org/3/library/functions.html#bool> = False

Allow parsing non-numeric values that begin with a hyphen -. This is disabled (False <https://docs.python.org/3/library/constants.html#False>) by default, allowing for more helpful error messages for unknown CLI options.

requires_equals: bool <https://docs.python.org/3/library/functions.html#bool> = False

Require long options to use = to separate the option name from its value (e.g., --option=value). When enabled, the space-separated form --option value is rejected with a RequiresEqualsError.

  • Only applies to long-form options (those starting with --).
  • Short options (e.g., -o value) are not affected.
  • Boolean flags (e.g., --verbose) work regardless of this setting.
  • Can be set app-wide via App.default_parameter.
  • Cannot be combined with consume_multiple (raises ValueError <https://docs.python.org/3/library/exceptions.html#ValueError>). To provide multiple values for a list parameter, repeat the option (e.g., --urls=a --urls=b). To pass an empty iterable, use the negative flag (e.g., --empty-urls).

    from cyclopts import App, Parameter
    from typing import Annotated
    
    app = App()
    
    @app.default
    def main(*, name: Annotated[str, Parameter(requires_equals=True)]):
        print(f"Hello {name}")
    
    app()
    $ my-script --name=alice
    Hello alice
    
    $ my-script --name alice
    ╭─ Error ───────────────────────────────────────────────────────╮
    │ Parameter "--name" requires a value assigned with "=".        │
    │ Use "--name=VALUE".                                           │
    ╰───────────────────────────────────────────────────────────────╯
parse: None <https://docs.python.org/3/library/constants.html#None> | bool <https://docs.python.org/3/library/functions.html#bool> | str <https://docs.python.org/3/library/stdtypes.html#str> | re.Pattern <https://docs.python.org/3/library/re.html#re.Pattern> = None

Attempt to use this parameter while parsing. Annotated parameter must be keyword-only or have a default value. This is intended to be used with meta apps <#meta-app> for injecting values.

Regex patterns are primarily intended for use with App.default_parameter to define app-wide skip patterns. For example, if we wanted to skip all fields that begin with an underscore _:

import re
from cyclopts import App, Parameter

# Skip parsing underscore-prefixed KEYWORD_ONLY parameters (i.e. private parameters)
# Both string and pre-compiled patterns are supported:
app = App(default_parameter=Parameter(parse="^(?!_)"))
# or: app = App(default_parameter=Parameter(parse=re.compile("^(?!_)")))

@app.default
def main(visible: str, *, _injected: str = "default"):
    # _injected is NOT parsed from CLI; uses default value
    pass
required: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Indicates that the parameter must be supplied. Defaults to inferring from the function signature; i.e. False <https://docs.python.org/3/library/constants.html#False> if the parameter has a default, True <https://docs.python.org/3/library/constants.html#True> otherwise.

show: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Show this parameter on the help screen. Defaults to whether the parameter is parsed (usually True <https://docs.python.org/3/library/constants.html#True>).

show_default: None <https://docs.python.org/3/library/constants.html#None> | bool <https://docs.python.org/3/library/functions.html#bool> | Callable[[Any], Any] = None

If a variable has a default, display the default on the help page. Defaults to None <https://docs.python.org/3/library/constants.html#None>, similar to True <https://docs.python.org/3/library/constants.html#True>, but will not display the default if it is None <https://docs.python.org/3/library/constants.html#None>.

If set to a function with signature:

def formatter(value: Any) -> Any:
    ...

Then the function will be called with the default value, and the returned value will be used as the displayed default value.

Example formatting function:

def hex_formatter(value: int) -> str
   """Will result in something like "[default: 0xFF]" instead of "[default: 255]"."""
   return f"0x{value:X}"
show_choices: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = True

If a variable has a set of choices, display the choices on the help page.

help: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Help string to be displayed on the help page. If not specified, defaults to the docstring.

show_env_var: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = True

If a variable has env_var set, display the variable name on the help page.

env_var: None <https://docs.python.org/3/library/constants.html#None> | str <https://docs.python.org/3/library/stdtypes.html#str> | Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = None

Fallback to environment variable(s) if CLI value not provided. If multiple environment variables are given, the left-most environment variable with a set value will be used. If no environment variable is set, Cyclopts will fallback to the function-signature default.

env_var_split: Callable = cyclopts.env_var_split

Function that splits up the read-in env_var value. The function must have signature:

def env_var_split(type_: type, val: str) -> list[str]:
    ...

where type_ is the associated parameter type-hint, and val is the environment value.

name_transform: Callable[[str <https://docs.python.org/3/library/stdtypes.html#str>], str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None

A function that converts python parameter names to their CLI command counterparts.

The function must have signature:

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

If None <https://docs.python.org/3/library/constants.html#None> (default value), uses cyclopts.default_name_transform().

accepts_keys: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

If False <https://docs.python.org/3/library/constants.html#False>, treat the user-defined class annotation similar to a tuple. Individual class sub-parameters will not be addressable by CLI keywords. The class will consume enough tokens to populate all required positional parameters.

Default behavior (accepts_keys=True):

from cyclopts import App, Parameter
from typing import Annotated

app = App()

class Image:
   def __init__(self, path, label):
      self.path = path
      self.label = label

   def __repr__(self):
      return f"Image(path={self.path!r}, label={self.label!r})"

@app.default
def main(image: Image):
   print(f"{image=}")

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

╭─ Commands ──────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                           │
│ --version  Display application version.                             │
╰─────────────────────────────────────────────────────────────────────╯
╭─ Parameters ────────────────────────────────────────────────────────╮
│ *  IMAGE.PATH --image.path    [required]                            │
│ *  IMAGE.LABEL --image.label  [required]                            │
╰─────────────────────────────────────────────────────────────────────╯

$ my-program foo.jpg nature
image=Image(path='foo.jpg', label='nature')

$ my-program --image.path foo.jpg --image.label nature
image=Image(path='foo.jpg', label='nature')

Behavior when accepts_keys=False:

# Modify the default command function's signature.
@app.default
def main(image: Annotated[Image, Parameter(accepts_keys=False)]):
   print(f"{image=}")
$ my-program --help
Usage: main COMMAND [ARGS] [OPTIONS]

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

$ my-program foo.jpg nature
image=Image(path='foo.jpg', label='nature')

$ my-program --image foo.jpg nature
image=Image(path='foo.jpg', label='nature')

The accepts_keys=False option is commonly used with converter and n_tokens.

consume_multiple: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Lists use different parsing rules <rules.html#list> depending on whether the values are provided positionally or by keyword. If the parameter is specified positionally, Parameter.consume_multiple is ignored.

If the parameter is specified by keyword and consume_multiple=True, all remaining CLI tokens will be consumed until the stream is exhausted or an option-like token (typically a keyword) is reached (unless Parameter.allow_leading_hyphen is True <https://docs.python.org/3/library/constants.html#True>, in which case it will also be consumed).

from cyclopts import App, Parameter
from typing import Annotated

app = App()

@app.command
def name_ext(
    name: str,
    ext: Annotated[list[str], Parameter(consume_multiple=True)],
):
    for extension in ext:
        print(f"{name}.{extension}")

app()
$ my-program --name "my_file" --ext "txt" "pdf"  # stream is exhausted
my_file.txt
my_file.pdf

$ my-program --ext "txt" "pdf" --name "my_file"  # a keyword is reached
my_file.txt
my_file.pdf

When consume_multiple=True, providing the keyword flag without any values will create an empty container, equivalent to using the negative_iterable prefix (e.g., --empty-ext):

$ my-program --name "my_file" --ext
# No output - ext is an empty list []

$ my-program --name "my_file" --empty-ext
# No output - ext is an empty list []

If the parameter is specified by keyword and consume_multiple=False (the default), only a single element worth of CLI tokens will be consumed.

from cyclopts import App
from pathlib import Path

app = App()

@app.default
def name_ext(name: str, ext: list[str]): # same as `ext: Annotated[list[str], Parameter(consume_multiple=False)]``
    for extension in ext:
        print(f"{name}.{extension}")

app()
$ my-program --name "my_file" --ext "txt" "pdf"
╭─ Error ────────────────────────────────────────────╮
│ Unused Tokens: ['pdf'].                            │
╰────────────────────────────────────────────────────╯
json_dict: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Allow for the parsing of json-dict-strings as data. If None <https://docs.python.org/3/library/constants.html#None> (default behavior), acts like True <https://docs.python.org/3/library/constants.html#True>, unless the annotated type is union'd with str <https://docs.python.org/3/library/stdtypes.html#str>. When True <https://docs.python.org/3/library/constants.html#True>, data will be parsed as json if the following conditions are met:

  1. The parameter is specified as a keyword option; e.g. --movie.
  2. The referenced parameter is dataclass-like.
  3. The first character of the token is a {.
json_list: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Allow for the parsing of json-list-strings as data. If None <https://docs.python.org/3/library/constants.html#None> (default behavior), acts like True <https://docs.python.org/3/library/constants.html#True>, unless the annotated type has each element type str <https://docs.python.org/3/library/stdtypes.html#str>. When True <https://docs.python.org/3/library/constants.html#True>, data will be parsed as json if the following conditions are met:

  1. The referenced parameter is iterable (not including str <https://docs.python.org/3/library/stdtypes.html#str>).
  2. The first character of the token is a [.
count: bool <https://docs.python.org/3/library/functions.html#bool> = False

If True <https://docs.python.org/3/library/constants.html#True>, count the number of times the flag appears instead of parsing a value. Each occurrence increments the count by 1 (e.g., -vvv results in 3).

Requirements and behavior:

  • The parameter must have an int <https://docs.python.org/3/library/functions.html#int> type hint (or Optional[int]).
  • Short flags can be concatenated: -vvv is equivalent to -v -v -v.
  • Long flags can be repeated: --verbose --verbose results in 2.
  • Negative flag variants (e.g., --no-verbose) are not generated.

Common use case: verbosity levels.

from cyclopts import App, Parameter
from typing import Annotated

app = App()

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

app()
$ my-script -vvv
Verbosity level: 3

See Coercion Rules <#coercion-rules> for more details.

n_tokens: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None

Explicitly override the number of CLI tokens this parameter consumes.

By default, Cyclopts infers the token count from the parameter's type hint (e.g., int <https://docs.python.org/3/library/functions.html#int> consumes 1 token, tuple[int, int] consumes 2, list <https://docs.python.org/3/library/stdtypes.html#list> consumes all remaining). This attribute allows you to override that inference, which is particularly useful when:

  • Using custom converters that need a different token count than the type suggests.
  • Loading complex types from a single token (e.g., loading from a file path).
  • Implementing selection/lookup patterns where one token identifies an object.

Values:

  • None (default): Infer token count from the type hint.
  • non-negative integer: Consume exactly that many tokens.
  • -1: Consume all remaining tokens (similar to iterables).

For *args parameters, n_tokens specifies tokens per element. For example, n_tokens=2 with 6 tokens creates 3 elements.

from cyclopts import App, Parameter
from typing import Annotated

class Config:
    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port

def load_config(type_, tokens):
    # Load config from a file path (single token)
    filepath = tokens[0].value
    # ... load from file ...
    return Config("example.com", 8080)

app = App()

@app.default
def main(
    config: Annotated[
        Config,
        Parameter(n_tokens=1, converter=load_config, accepts_keys=False)
    ]
):
    print(f"Connecting to {config.host}:{config.port}")

app()
$ my-script --config prod.conf
Connecting to example.com:8080
classmethod combine(*parameters: Parameter <#cyclopts.Parameter> | None <https://docs.python.org/3/library/constants.html#None>) -> Parameter <#cyclopts.Parameter>

Returns a new Parameter with combined values of all provided parameters.

Parameters

*parameters (Parameter <#cyclopts.Parameter> | None) -- Parameters who's attributes override self attributes. Ordered from least-to-highest attribute priority.

classmethod default() -> Self <https://docs.python.org/3/library/typing.html#typing.Self>

Create a Parameter with all Cyclopts-default values.

This is different than just Parameter because the default values will be recorded and override all upstream parameter values.

__call__(obj: T) -> T

Decorator interface for annotating a function/class with a Parameter.

Most commonly used for directly configuring a class:

@Parameter(...)
class Foo: ...
class cyclopts.Group(name: str <https://docs.python.org/3/library/stdtypes.html#str> = '', help: str <https://docs.python.org/3/library/stdtypes.html#str> = '', *, show: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, sort_key: Any <https://docs.python.org/3/library/typing.html#typing.Any> = None, validator=None, default_parameter: Parameter <#cyclopts.Parameter> | None <https://docs.python.org/3/library/constants.html#None> = None, help_formatter: None <https://docs.python.org/3/library/constants.html#None> | Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['default', 'plain'] | Any <https://docs.python.org/3/library/typing.html#typing.Any> = None)

A group of parameters and/or commands in a CLI application.

name: str <https://docs.python.org/3/library/stdtypes.html#str> = ""

Group name used for the help-page and for group-referenced-by-string. This is a title, so the first character should be capitalized. If a name is not specified, it will not be shown on the help-page.

help: str <https://docs.python.org/3/library/stdtypes.html#str> = ""

Additional documentation shown on the help-page. This will be displayed inside the group's panel, above the parameters/commands.

show: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None

Show this group on the help-page. Defaults to None <https://docs.python.org/3/library/constants.html#None>, which will only show the group if a name is provided.

help_formatter: None <https://docs.python.org/3/library/constants.html#None> | Literal['default', 'plain'] | HelpFormatter <#cyclopts.help.protocols.HelpFormatter> = None

Help formatter to use for rendering this group's help panel.

  • If None <https://docs.python.org/3/library/constants.html#None> (default), inherits from the App's help_formatter.
  • If "default", uses DefaultFormatter.
  • If "plain", uses PlainFormatter for no-frills plain text output.
  • If a callable (see HelpFormatter protocol), uses the provided formatter.

This allows per-group customization of help appearance:

from cyclopts import App, Group, Parameter
from cyclopts.help import DefaultFormatter, PanelSpec
from typing import Annotated

app = App()

# Using string literal
simple_group = Group(
    "Simple Options",
    help_formatter="plain"
)

# Using custom formatter instance
custom_group = Group(
    "Custom Options",
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(border_style="red")
    )
)

@app.default
def main(
    opt1: Annotated[str, Parameter(group=simple_group)],
    opt2: Annotated[str, Parameter(group=custom_group)]
):
    pass

See Help Customization <#help-customization> for detailed examples.

sort_key: Any = None

Modifies group-panel display order on the help-page.

  1. If sort_key is a generator, it will be consumed immediately with next() <https://docs.python.org/3/library/functions.html#next> to get the actual value.
  2. If sort_key, or any of it's contents, are Callable, then invoke it sort_key(group) and apply the rules below.
  3. The App default groups (App.group_command, App.group_arguments, App.group_parameters) will be displayed first. If you want to further customize the ordering of these default groups, you can define custom values and they will be treated like any other group:

    from cyclopts import App, Group
    
    app = App(
        group_parameters=Group("Parameters", sort_key=1),
        group_arguments=Group("Arguments", sort_key=2),
        group_commands=Group("Commands", sort_key=3),
    )
    
    
    @app.default
    def main(foo, /, bar):
        pass
    
    
    if __name__ == "__main__":
        app()
    $ python main.py --help
    Usage: main [ARGS] [OPTIONS]
    
    ╭─ Parameters ──────────────────────────────────────────────────────────╮
    │ *  BAR --bar  [required]                                              │
    ╰───────────────────────────────────────────────────────────────────────╯
    ╭─ Arguments ───────────────────────────────────────────────────────────╮
    │ *  FOO  [required]                                                    │
    ╰───────────────────────────────────────────────────────────────────────╯
    ╭─ Commands ────────────────────────────────────────────────────────────╮
    │ --help -h  Display this message and exit.                             │
    │ --version  Display application version.                               │
    ╰───────────────────────────────────────────────────────────────────────╯
  4. For all groups with sort_key!=None, sort them by (sort_key, group.name). That is, sort them by their sort_key, and then break ties alphabetically. It is the user's responsibility that sort_key are comparable.
  5. For all groups with sort_key==None (default value), sort them alphabetically after (4), App.group_commands, App.group_arguments, and App.group_parameters.

Example usage:

from cyclopts import App, Group

app = App()

@app.command(group=Group("4", sort_key=5))
def cmd1():
    pass


@app.command(group=Group("3", sort_key=lambda x: 10))
def cmd2():
    pass


@app.command(group=Group("2", sort_key=lambda x: None))
def cmd3():
    pass


@app.command(group=Group("1"))
def cmd4():
    pass

app()

Resulting help-page:

Usage: app COMMAND

╭─ 4 ────────────────────────────────────────────────────────────────╮
│ cmd1                                                               │
╰────────────────────────────────────────────────────────────────────╯
╭─ 3 ────────────────────────────────────────────────────────────────╮
│ cmd2                                                               │
╰────────────────────────────────────────────────────────────────────╯
╭─ 1 ────────────────────────────────────────────────────────────────╮
│ cmd4                                                               │
╰────────────────────────────────────────────────────────────────────╯
╭─ 2 ────────────────────────────────────────────────────────────────╮
│ cmd3                                                               │
╰────────────────────────────────────────────────────────────────────╯
╭─ Commands ─────────────────────────────────────────────────────────╮
│ --help,-h  Display this message and exit.                          │
│ --version  Display application version.                            │
╰────────────────────────────────────────────────────────────────────╯
default_parameter: Parameter <#cyclopts.Parameter> | None <https://docs.python.org/3/library/constants.html#None> = None

Default Parameter in the parameter-resolution-stack that goes between App.default_parameter and the function signature's Annotated Parameter. The provided Parameter is not allowed to have a group value.

validator: Callable | None <https://docs.python.org/3/library/constants.html#None> = None

A function (or list of functions) that validates an ArgumentCollection.

Example usage:

def validator(argument_collection: ArgumentCollection):
    "Raise an exception if something is invalid."

The ArgumentCollection will contain all arguments that belong to that group. The validator(s) will always be invoked, regardless if any argument within the collection has token(s).

Validators are not invoked for command groups.

classmethod create_ordered(name='', help='', *, show=None, sort_key=None, validator=None, default_parameter=None, help_formatter=None) -> Self <https://docs.python.org/3/library/typing.html#typing.Self>

Create a group with a globally incrementing sort_key.

Used to create a group that will be displayed after a previously instantiated Group.create_ordered() group on the help-page.

Parameters
  • name (str <https://docs.python.org/3/library/stdtypes.html#str>) -- Group name used for the help-page and for group-referenced-by-string. This is a title, so the first character should be capitalized. If a name is not specified, it will not be shown on the help-page.
  • help (str <https://docs.python.org/3/library/stdtypes.html#str>) -- Additional documentation shown on the help-page. This will be displayed inside the group's panel, above the parameters/commands.
  • show (bool <https://docs.python.org/3/library/functions.html#bool> | None) -- Show this group on the help-page. Defaults to None <https://docs.python.org/3/library/constants.html#None>, which will only show the group if a name is provided.
  • sort_key (Any) -- If provided, prepended to the globally incremented counter value (i.e. has priority during sorting).
  • validator (None | Callable[[ArgumentCollection <#cyclopts.ArgumentCollection>], Any] | Iterable[Callable[[ArgumentCollection <#cyclopts.ArgumentCollection>], Any]]) -- Group validator to collectively apply.
  • default_parameter (cyclopts.Parameter <#cyclopts.Parameter> | None) -- Default parameter for elements within the group.
  • help_formatter (cyclopts.help.protocols.HelpFormatter <#cyclopts.help.protocols.HelpFormatter> | None) -- Custom help formatter for this group's help display.
class cyclopts.Token(*, keyword: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, value: str <https://docs.python.org/3/library/stdtypes.html#str> = '', source: str <https://docs.python.org/3/library/stdtypes.html#str> = '', index: int <https://docs.python.org/3/library/functions.html#int> = 0, keys: tuple[str, ...]=(), implicit_value: Any <https://docs.python.org/3/library/typing.html#typing.Any> = <UNSET>)

Tracks how a user supplied a value to the application.

keyword: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Unadulterated user-supplied keyword like --foo or --foo.bar.baz; None when token was pared positionally. Could also be something like tool.project.foo if from non-cli sources.

value: str <https://docs.python.org/3/library/stdtypes.html#str> = ""

The parsed token value (unadulterated).

source: str <https://docs.python.org/3/library/stdtypes.html#str> = ""

Where the token came from; used for error message purposes. Cyclopts uses the string cli for cli-parsed tokens.

index: int <https://docs.python.org/3/library/functions.html#int> = 0

The relative positional index in which the value was provided.

keys: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] = ()

The additional parsed python variable keys from keyword.

Only used for Arguments that take arbitrary keys.

implicit_value: Any = cyclopts.UNSET

Final value that should be used instead of converting from value.

Commonly used for boolean flags.

Ignored if UNSET.

class cyclopts.field_info.FieldInfo(names: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] = (), kind: _ParameterKind = _ParameterKind.POSITIONAL_OR_KEYWORD, *, required: bool <https://docs.python.org/3/library/functions.html#bool> = False, default: Any <https://docs.python.org/3/library/typing.html#typing.Any>, annotation: Any <https://docs.python.org/3/library/typing.html#typing.Any>, help: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None)

Extension of inspect.Parameter <https://docs.python.org/3/library/inspect.html#inspect.Parameter>.

class cyclopts.Argument(*, tokens: list <https://docs.python.org/3/library/stdtypes.html#list>[Token <#cyclopts.Token>] = NOTHING, field_info: FieldInfo <#cyclopts.field_info.FieldInfo> = NOTHING, parameter: Parameter <#cyclopts.Parameter> = NOTHING, hint: Any <https://docs.python.org/3/library/typing.html#typing.Any> = <class 'str'>, index: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, keys: tuple[str, ...]=(), value: Any <https://docs.python.org/3/library/typing.html#typing.Any> = <UNSET>)

Encapsulates functionality and additional contextual information for parsing a parameter.

An argument is defined as anything that would have its own entry in the help page.

tokens: list <https://docs.python.org/3/library/stdtypes.html#list>[Token <#cyclopts.Token>]

List of Token parsed from various sources. Do not directly mutate; see append().

field_info: FieldInfo <#cyclopts.field_info.FieldInfo>

Additional information about the parameter from surrounding python syntax.

parameter: Parameter <#cyclopts.Parameter>

Fully resolved user-provided Parameter.

hint: Any <https://docs.python.org/3/library/typing.html#typing.Any>

The type hint for this argument; may be different from FieldInfo.annotation.

index: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Associated python positional index for argument. If None, then cannot be assigned positionally.

keys: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

Python keys that lead to this leaf.

self.parameter.name and self.keys can naively disagree! For example, a self.parameter.name="--foo.bar.baz" could be aliased to "--fizz". The resulting self.keys would be ("bar", "baz").

This is populated based on type-hints and class-structure, not Parameter.name.

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

app = App()


@dataclass
class User:
    id: int
    name: Annotated[str, Parameter(name="--fullname")]


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


for argument in app.assemble_argument_collection():
    print(f"name: {argument.name:16} hint: {str(argument.hint):16} keys: {str(argument.keys)}")
$ my-script
name: --user.id        hint: <class 'int'>    keys: ('id',)
name: --fullname       hint: <class 'str'>    keys: ('name',)
children: ArgumentCollection <#cyclopts.ArgumentCollection>

Collection of other Argument that eventually culminate into the python variable represented by field_info.

property value

Converted value from last convert() call.

This value may be stale if fields have changed since last convert() call. UNSET if convert() has not yet been called with tokens.

property show_default: bool <https://docs.python.org/3/library/functions.html#bool> | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], str <https://docs.python.org/3/library/stdtypes.html#str>]

Show the default value on the help page.

match(term: str <https://docs.python.org/3/library/stdtypes.html#str> | int <https://docs.python.org/3/library/functions.html#int>, *, transform: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[str <https://docs.python.org/3/library/stdtypes.html#str>], str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None, delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> = '.') -> tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...], Any <https://docs.python.org/3/library/typing.html#typing.Any>]

Match a name search-term, or a positional integer index.

Raises

ValueError <https://docs.python.org/3/library/exceptions.html#ValueError> -- If no match is found.

Returns
  • tuple[str, ...] -- Leftover keys after matching to this argument. Used if this argument accepts_arbitrary_keywords.
  • Any -- Implicit value. UNSET if no implicit value is applicable.
append(token: Token <#cyclopts.Token>)

Safely add a Token.

property has_tokens: bool <https://docs.python.org/3/library/functions.html#bool>

This argument, or a child argument, has at least 1 parsed token.

property children_recursive: ArgumentCollection <#cyclopts.ArgumentCollection>

convert(converter: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable> | None <https://docs.python.org/3/library/constants.html#None> = None)

Converts tokens into value.

Parameters

converter (Callable | None) -- Converter function to use. Overrides self.parameter.converter

Returns

The converted data. Same as value.

Return type

Any

validate(value)

Validates provided value.

Parameters

value -- Value to validate.

Returns

The converted data. Same as value.

Return type

Any

convert_and_validate(converter: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable> | None <https://docs.python.org/3/library/constants.html#None> = None)

Converts and validates tokens into value.

Parameters

converter (Callable | None) -- Converter function to use. Overrides self.parameter.converter

Returns

The converted data. Same as value.

Return type

Any

token_count(keys: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] = ())

The number of string tokens this argument consumes.

Parameters

keys (tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]) -- The python keys into this argument. If provided, returns the number of string tokens that specific data type within the argument consumes.

Returns
property negatives

Negative flags from Parameter.get_negatives().

property name: str <https://docs.python.org/3/library/stdtypes.html#str>

The first provided name this argument goes by.

property names: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

Names the argument goes by (both positive and negative).

env_var_split(value: str <https://docs.python.org/3/library/stdtypes.html#str>, delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None) -> list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>]

Split a given value with Parameter.env_var_split().

property show: bool <https://docs.python.org/3/library/functions.html#bool>

Show this argument on the help page.

If an argument has child arguments, don't show it on the help-page. Returns False for arguments that won't be parsed (including underscore-prefixed params).

property parse: bool <https://docs.python.org/3/library/functions.html#bool>

Whether this argument should be parsed from CLI tokens.

If Parameter.parse is a regex pattern, parse if the pattern matches the field name; otherwise don't parse.

property required: bool <https://docs.python.org/3/library/functions.html#bool>

Whether or not this argument requires a user-provided value.

is_positional_only() -> bool <https://docs.python.org/3/library/functions.html#bool>

is_var_positional() -> bool <https://docs.python.org/3/library/functions.html#bool>

is_flag() -> bool <https://docs.python.org/3/library/functions.html#bool>

Check if this argument is a flag (consumes no CLI tokens).

Flags are arguments that don't consume command-line tokens after the option name. They typically have implicit values (e.g., --verbose for bool, --no-items for list).

Returns

True if the argument consumes zero tokens from the command line.

Return type

bool <https://docs.python.org/3/library/functions.html#bool>

Examples

>>> from cyclopts import Parameter
>>> bool_arg = Argument(hint=bool, parameter=Parameter(name="--verbose"))
>>> bool_arg.is_flag()
True
>>> str_arg = Argument(hint=str, parameter=Parameter(name="--name"))
>>> str_arg.is_flag()
False
get_choices(force: bool <https://docs.python.org/3/library/functions.html#bool> = False) -> tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] | None <https://docs.python.org/3/library/constants.html#None>

Extract completion choices from type hint.

Extracts choices from Literal types, Enum types, and Union types containing them. Respects the Parameter.show_choices setting unless force=True.

Parameters

force (bool <https://docs.python.org/3/library/functions.html#bool>) -- If True, return choices even when show_choices=False. Used by shell completion to always provide choices.

Returns

Tuple of choice strings if choices exist and should be shown, None otherwise.

Return type

tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] | None

Examples

>>> argument = Argument(hint=Literal["dev", "staging", "prod"], parameter=Parameter(show_choices=True))
>>> argument.get_choices()
('dev', 'staging', 'prod')
>>> argument = Argument(hint=Literal["dev", "staging", "prod"], parameter=Parameter(show_choices=False))
>>> argument.get_choices()  # Returns None for help text
>>> argument.get_choices(force=True)  # Returns choices for completion
('dev', 'staging', 'prod')
class cyclopts.ArgumentCollection(*args)

A list-like container for Argument.

copy() -> ArgumentCollection <#cyclopts.ArgumentCollection>

Returns a shallow copy of the ArgumentCollection.

__contains__(item: object <https://docs.python.org/3/library/functions.html#object>, /) -> bool <https://docs.python.org/3/library/functions.html#bool>

Check if an argument or argument name exists in the collection.

Parameters

item (Argument <#cyclopts.Argument> | str <https://docs.python.org/3/library/stdtypes.html#str>) -- Either an Argument object or a string name/alias to search for.

Returns

True if the item is in the collection.

Return type

bool <https://docs.python.org/3/library/functions.html#bool>

Examples

>>> argument_collection = ArgumentCollection(
...     [
...         Argument(parameter=Parameter(name="--foo")),
...         Argument(parameter=Parameter(name=("--bar", "-b"))),
...     ]
... )
>>> "--foo" in argument_collection
True
>>> "-b" in argument_collection  # Alias matching
True
>>> "--baz" in argument_collection
False
get(term: str | int, default: type[<UNSET>] = <UNSET>, *, transform: ~collections.abc.Callable[[str], str] | None = None, delimiter: str = '.') -> Argument <#cyclopts.Argument>
get(term: str <https://docs.python.org/3/library/stdtypes.html#str> | int <https://docs.python.org/3/library/functions.html#int>, default: T, *, transform: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[str <https://docs.python.org/3/library/stdtypes.html#str>], str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None, delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> = '.') -> Argument <#cyclopts.Argument> | T

Get an Argument by name or index.

This is a convenience wrapper around match() that returns just the Argument object instead of a tuple.

Parameters
Returns

The matched Argument, or default if provided and not found.

Return type

Argument <#cyclopts.Argument> | None

Raises

See also:

match()

Returns a tuple of (Argument, keys, value) with more detailed information.

match(term: str <https://docs.python.org/3/library/stdtypes.html#str> | int <https://docs.python.org/3/library/functions.html#int>, *, transform: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[str <https://docs.python.org/3/library/stdtypes.html#str>], str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None> = None, delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> = '.') -> tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[Argument <#cyclopts.Argument>, tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...], Any <https://docs.python.org/3/library/typing.html#typing.Any>]

Matches CLI keyword or index to their Argument.

Parameters

term (str <https://docs.python.org/3/library/stdtypes.html#str> | int <https://docs.python.org/3/library/functions.html#int>) --

One of:

Raises

ValueError <https://docs.python.org/3/library/exceptions.html#ValueError> -- If the provided term doesn't match.

Returns
  • Argument -- Matched Argument.
  • tuple[str, ...] -- Python keys into Argument. Non-empty iff Argument accepts keys.
  • Any -- Implicit value (if a flag). UNSET otherwise.

property groups

filter_by(*, group: Group <#cyclopts.Group> | None <https://docs.python.org/3/library/constants.html#None> = None, has_tokens: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, has_tree_tokens: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, keys_prefix: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] | None <https://docs.python.org/3/library/constants.html#None> = None, kind: _ParameterKind | None <https://docs.python.org/3/library/constants.html#None> = None, parse: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, show: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, value_set: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None) -> ArgumentCollection <#cyclopts.ArgumentCollection>

Filter the ArgumentCollection.

All non-None filters will be applied.

Parameters
class cyclopts.UNSET

Special sentinel value indicating that no data was provided. Do not instantiate.

cyclopts.default_name_transform(s: str <https://docs.python.org/3/library/stdtypes.html#str>) -> str <https://docs.python.org/3/library/stdtypes.html#str>

Converts a python identifier into a CLI token.

Performs the following operations (in order):

  1. Convert PascalCase to snake_case.
  2. Convert the string to all lowercase.
  3. Replace _ with -.
  4. Strip any leading/trailing - (also stripping _, due to point 3).

Intended to be used with App.name_transform and Parameter.name_transform.

Parameters

s (str <https://docs.python.org/3/library/stdtypes.html#str>) -- Input python identifier string.

Returns

Transformed name.

Return type

str <https://docs.python.org/3/library/stdtypes.html#str>

cyclopts.env_var_split(type_: Any <https://docs.python.org/3/library/typing.html#typing.Any>, val: str <https://docs.python.org/3/library/stdtypes.html#str>, *, delimiter: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None) -> list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>]

Type-dependent environment variable value splitting.

Converts a single string into a list of strings. Splits when:

  • The type_ is some variant of Iterable[pathlib.Path] objects. If Windows, split on ;, otherwise split on :.
  • Otherwise, if the type_ is an Iterable, split on whitespace. Leading/trailing whitespace of each output element will be stripped.

This function is the default value for cyclopts.App.env_var_split.

Parameters
Returns

List of individual string tokens.

Return type

list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>]

cyclopts.edit(initial_text: str <https://docs.python.org/3/library/stdtypes.html#str> = '', *, fallback_editors: Sequence <https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence>[str <https://docs.python.org/3/library/stdtypes.html#str>] = ('nano', 'vim', 'notepad', 'gedit'), editor_args: Sequence <https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence>[str <https://docs.python.org/3/library/stdtypes.html#str>] = (), path: str <https://docs.python.org/3/library/stdtypes.html#str> | Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> = '', encoding: str <https://docs.python.org/3/library/stdtypes.html#str> = 'utf-8', save: bool <https://docs.python.org/3/library/functions.html#bool> = True, required: bool <https://docs.python.org/3/library/functions.html#bool> = True) -> str <https://docs.python.org/3/library/stdtypes.html#str>

Get text input from a user by launching their default text editor.

Parameters
Raises
  • EditorError <#cyclopts.EditorError> -- Base editor error exception. Explicitly raised if editor subcommand
       returned a non-zero exit code.
  • EditorNotFoundError <#cyclopts.EditorNotFoundError> -- A suitable text editor could not be found.
  • EditorDidNotSaveError <#cyclopts.EditorDidNotSaveError> -- The user exited the text-editor without saving and save=True.
  • EditorDidNotChangeError <#cyclopts.EditorDidNotChangeError> -- The user did not change the file contents and required=True.
Returns

The resulting text that was saved by the text editor.

Return type

str <https://docs.python.org/3/library/stdtypes.html#str>

cyclopts.run(callable: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], Coroutine <https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine>[None <https://docs.python.org/3/library/constants.html#None>, None <https://docs.python.org/3/library/constants.html#None>, V]], /, *, result_action: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value']) -> V
cyclopts.run(callable: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], V], /, *, result_action: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value']) -> V
cyclopts.run(callable: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], Coroutine <https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine>[None <https://docs.python.org/3/library/constants.html#None>, None <https://docs.python.org/3/library/constants.html#None>, Any <https://docs.python.org/3/library/typing.html#typing.Any>]], /, *, result_action: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>] | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>]] | None <https://docs.python.org/3/library/constants.html#None> = None) -> Any <https://docs.python.org/3/library/typing.html#typing.Any>
cyclopts.run(callable: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[...], Any <https://docs.python.org/3/library/typing.html#typing.Any>], /, *, result_action: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>] | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['return_value', 'call_if_callable', 'print_non_int_return_int_as_exit_code', 'print_str_return_int_as_exit_code', 'print_str_return_zero', 'print_non_none_return_int_as_exit_code', 'print_non_none_return_zero', 'return_int_as_exit_code_else_zero', 'print_non_int_sys_exit', 'sys_exit', 'return_none', 'return_zero', 'print_return_zero', 'sys_exit_zero', 'print_sys_exit_zero'] | Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable>[[Any <https://docs.python.org/3/library/typing.html#typing.Any>], Any <https://docs.python.org/3/library/typing.html#typing.Any>]] | None <https://docs.python.org/3/library/constants.html#None> = None) -> Any <https://docs.python.org/3/library/typing.html#typing.Any>

Run the given callable as a CLI command.

The callable may also be a coroutine function. This function is syntax sugar for very simple use cases, and is roughly equivalent to:

from cyclopts import App

app = App()
app.default(callable)
app()
Parameters
  • callable -- The function to execute as a CLI command.
  • result_action -- How to handle the command's return value. If not specified, uses the default "print_non_int_sys_exit" which calls sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with the appropriate code. Can be set to "return_value" to return the result directly for testing/embedding.
  • usage (Example)
  • code-block: (..) --

    python: import cyclopts

    def main(name: str, age: int):

    print(f"Hello {name}, you are {age} years old.")

    cyclopts.run(main)

class cyclopts.CycloptsPanel(message: Any <https://docs.python.org/3/library/typing.html#typing.Any>, title: str <https://docs.python.org/3/library/stdtypes.html#str> = 'Error', style: str <https://docs.python.org/3/library/stdtypes.html#str> = 'red')

Create a Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel> with a consistent style.

The resulting panel can be displayed using a Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console>.

╭─ Title ──────────────────────────────────╮
│ Message content here.                    │
╰──────────────────────────────────────────╯
Parameters
Returns

Formatted panel object.

Return type

Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>

Validators

Cyclopts has several builtin validators for common CLI inputs.

class cyclopts.validators.LimitedChoice(min: int <https://docs.python.org/3/library/functions.html#int> = 0, max: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, allow_none: bool <https://docs.python.org/3/library/functions.html#bool> = False)

Group validator that limits the number of selections per group.

Commonly used for enforcing mutually-exclusive parameters (default behavior).

Parameters
class cyclopts.validators.MutuallyExclusive

Alias for LimitedChoice to make intentions more obvious.

Only 1 argument in the group can be supplied a value.

cyclopts.validators.mutually_exclusive = <cyclopts.validators._group.MutuallyExclusive object>

Instantiated version of MutuallyExclusive. Can be used directly in group validators:

import cyclopts
from cyclopts import Group

mutually_exclusive_group = Group(validator=cyclopts.validators.mutually_exclusive)
cyclopts.validators.all_or_none = <cyclopts.validators._group.LimitedChoice object>

Group validator that enforces that either all parameters in the group must be supplied an argument, or none of them.

class cyclopts.validators.Number(*, lt: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None> = None, lte: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None> = None, gt: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None> = None, gte: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None> = None, modulo: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None> = None)

Limit input number to a value range.

Example Usage:

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

app = App()


@app.default
def main(age: Annotated[int, Parameter(validator=validators.Number(gte=0, lte=150))]):
    print(f"You are {age} years old.")


app()
$ my-script 100
You are 100 years old.

$ my-script -1
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid value "-1" for "AGE". Must be >= 0.                   │
╰───────────────────────────────────────────────────────────────╯

$ my-script 200
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid value "200" for "AGE". Must be <= 150.                │
╰───────────────────────────────────────────────────────────────╯
lt: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None>

Input value must be less than this value.

lte: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None>

Input value must be less than or equal this value.

gt: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None>

Input value must be greater than this value.

gte: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None>

Input value must be greater than or equal this value.

modulo: int <https://docs.python.org/3/library/functions.html#int> | float <https://docs.python.org/3/library/functions.html#float> | None <https://docs.python.org/3/library/constants.html#None>

Input value must be a multiple of this value.

class cyclopts.validators.Path(*, exists: bool <https://docs.python.org/3/library/functions.html#bool> = False, file_okay: bool <https://docs.python.org/3/library/functions.html#bool> = True, dir_okay: bool <https://docs.python.org/3/library/functions.html#bool> = True, ext: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = None)

Assertions on properties of pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>.

Example Usage:

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

app = App()


@app.default
def main(
    # ``src`` must be a file that exists.
    src: Annotated[Path, Parameter(validator=validators.Path(exists=True, dir_okay=False))],
    # ``dst`` must be a path that does **not** exist.
    dst: Annotated[Path, Parameter(validator=validators.Path(dir_okay=False, file_okay=False))],
):
    "Copies src->dst."
    dst.write_bytes(src.read_bytes())


app()
$ my-script foo.bin bar.bin  # if foo.bin does not exist
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid value "foo.bin" for "SRC". "foo.bin" does not exist.  │
╰───────────────────────────────────────────────────────────────╯

$ my-script foo.bin bar.bin  # if bar.bin exists
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid value "bar.bin" for "DST". "bar.bin" already exists.  │
╰───────────────────────────────────────────────────────────────╯
exists: bool <https://docs.python.org/3/library/functions.html#bool>

If True <https://docs.python.org/3/library/constants.html#True>, specified path must exist. Defaults to False <https://docs.python.org/3/library/constants.html#False>.

file_okay: bool <https://docs.python.org/3/library/functions.html#bool>

If path exists, check it's type:

Defaults to True <https://docs.python.org/3/library/constants.html#True>.

dir_okay: bool <https://docs.python.org/3/library/functions.html#bool>

If path exists, check it's type:

Defaults to True <https://docs.python.org/3/library/constants.html#True>.

ext: str <https://docs.python.org/3/library/stdtypes.html#str> | Sequence <https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence>[str <https://docs.python.org/3/library/stdtypes.html#str>]

Supplied path must have this extension (case insensitive). May or may not include the ".".

Types

Cyclopts has builtin pre-defined annotated-types for common conversion and validation configurations. Most definitions in this section are simply predefined annotations for convenience:

Annotated[..., Parameter(...)]

Custom classes that provide additional functionality beyond simple annotations will be noted.

Due to Cyclopts's advanced Parameter resolution engine, these annotations can themselves be annotated to further configure behavior. E.g:

Annotated[PositiveInt, Parameter(...)]

Path

Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> annotated types for checking existence, type, and performing path-resolution. All of these types will also work on sequence of paths (e.g. tuple[Path, Path] or list[Path]).

class cyclopts.types.StdioPath
Note:

This is a custom class, not a simple Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated> type alias.

Requires Python 3.12+ due to Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> subclassing support.

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> subclass that treats - as stdin (for reading) or stdout (for writing). This follows common Unix convention <https://clig.dev/#arguments-and-flags>.

StdioPath is pre-configured with allow_leading_hyphen=True, so - can be passed as an argument without being interpreted as an option.

STDIO_STRING: str <https://docs.python.org/3/library/stdtypes.html#str> = "-"

Class attribute defining the string that triggers stdio behavior. Override in subclasses to use a different string.

is_stdio: bool <https://docs.python.org/3/library/functions.html#bool>

Returns True <https://docs.python.org/3/library/constants.html#True> if this path represents stdin/stdout (i.e., str(self) == STDIO_STRING). Override this property in subclasses for custom matching logic (e.g., matching multiple strings).

Basic usage:

from cyclopts import App
from cyclopts.types import StdioPath

app = App()

@app.default
def main(input_file: StdioPath):
    data = input_file.read_text()
    print(data.upper())

app()
$ echo "hello" | python my_script.py -
HELLO

$ python my_script.py data.txt
<contents of data.txt uppercased>

To default to stdin/stdout when no argument is provided:

@app.default
def main(input_file: StdioPath = StdioPath("-")):
    data = input_file.read_text()
    print(data.upper())

See Reading/Writing From File or Stdin/Stdout <#reading-writing-from-file-or-stdin-stdout> for more examples.

Subclassing

To use a different trigger string or custom matching logic, subclass StdioPath:

from cyclopts.types import StdioPath

# Simple: different trigger string
class StdinPath(StdioPath):
    STDIO_STRING = "STDIN"

class StdoutPath(StdioPath):
    STDIO_STRING = "STDOUT"

# Advanced: match multiple strings
class MultiStdioPath(StdioPath):
    @property
    def is_stdio(self) -> bool:
        return str(self) in ("-", "STDIN", "STDOUT")
cyclopts.types.ExistingPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file or directory that must exist.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=True, ext=()),))]

cyclopts.types.NonExistentPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file or directory that must not exist.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=()),))]

cyclopts.types.ResolvedPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file or directory. resolve() <https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve> is invoked prior to returning the path.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(converter=<function _path_resolve_converter at 0x7f8742aa9d20>)]

cyclopts.types.ResolvedExistingPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file or directory that must exist. resolve() <https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve> is invoked prior to returning the path.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=True, ext=()),)), Parameter(converter=<function _path_resolve_converter at 0x7f8742aa9d20>)]

cyclopts.types.Directory

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must be a directory (or not exist).

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=True, ext=()),))]

cyclopts.types.ExistingDirectory

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> directory that must exist.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=False, dir_okay=True, ext=()),))]

cyclopts.types.NonExistentDirectory

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> directory that must not exist.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=()),))]

cyclopts.types.ResolvedDirectory

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> directory. resolve() <https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve> is invoked prior to returning the path.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=True, ext=()),)), Parameter(converter=<function _path_resolve_converter at 0x7f8742aa9d20>)]

cyclopts.types.ResolvedExistingDirectory

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> directory that must exist. resolve() <https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve> is invoked prior to returning the path.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=False, dir_okay=True, ext=()),)), Parameter(converter=<function _path_resolve_converter at 0x7f8742aa9d20>)]

cyclopts.types.File

A File that must be a file (or not exist).

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=()),))]

cyclopts.types.ExistingFile

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file that must exist.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=()),))]

cyclopts.types.NonExistentFile

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file that must not exist.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=()),))]

cyclopts.types.ResolvedFile

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file. resolve() <https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve> is invoked prior to returning the path.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=()),)), Parameter(converter=<function _path_resolve_converter at 0x7f8742aa9d20>)]

cyclopts.types.ResolvedExistingFile

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> file that must exist. resolve() <https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve> is invoked prior to returning the path.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=()),)), Parameter(converter=<function _path_resolve_converter at 0x7f8742aa9d20>)]

cyclopts.types.BinPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension bin.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('bin',)),))]

cyclopts.types.ExistingBinPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension bin.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('bin',)),))]

cyclopts.types.NonExistentBinPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension bin.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('bin',)),))]

cyclopts.types.CsvPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension csv.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('csv',)),))]

cyclopts.types.ExistingCsvPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension csv.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('csv',)),))]

cyclopts.types.NonExistentCsvPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension csv.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('csv',)),))]

cyclopts.types.TxtPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension txt.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('txt',)),))]

cyclopts.types.ExistingTxtPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension txt.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('txt',)),))]

cyclopts.types.NonExistentTxtPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension txt.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('txt',)),))]

cyclopts.types.ImagePath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension in {png, jpg, jpeg}.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('png', 'jpg', 'jpeg')),))]

cyclopts.types.ExistingImagePath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension in {png, jpg, jpeg}.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('png', 'jpg', 'jpeg')),))]

cyclopts.types.NonExistentImagePath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension in {png, jpg, jpeg}.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('png', 'jpg', 'jpeg')),))]

cyclopts.types.Mp4Path

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension mp4.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('mp4',)),))]

cyclopts.types.ExistingMp4Path

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension mp4.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('mp4',)),))]

cyclopts.types.NonExistentMp4Path

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension mp4.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('mp4',)),))]

cyclopts.types.JsonPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension json.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('json',)),))]

cyclopts.types.ExistingJsonPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension json.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('json',)),))]

cyclopts.types.NonExistentJsonPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension json.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('json',)),))]

cyclopts.types.TomlPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension toml.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('toml',)),))]

cyclopts.types.ExistingTomlPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension toml.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('toml',)),))]

cyclopts.types.NonExistentTomlPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension toml.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('toml',)),))]

cyclopts.types.YamlPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must have extension yaml.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=True, dir_okay=False, ext=('yaml',)),))]

cyclopts.types.ExistingYamlPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must exist and have extension yaml.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=True, file_okay=True, dir_okay=False, ext=('yaml',)),))]

cyclopts.types.NonExistentYamlPath

A Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> that must not exist and have extension yaml.

alias of Annotated[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>, Parameter(validator=(Path(exists=False, file_okay=False, dir_okay=False, ext=('yaml',)),))]

Number

Annotated types for checking common int/float value constraints. All of these types will also work on sequence of numbers (e.g. tuple[int, int] or list[float]).

cyclopts.types.PositiveFloat

A float that must be >0.

alias of Annotated[float <https://docs.python.org/3/library/functions.html#float>, Parameter(validator=(Number(lt=None, lte=None, gt=0, gte=None, modulo=None),))]

cyclopts.types.NonNegativeFloat

A float that must be >=0.

alias of Annotated[float <https://docs.python.org/3/library/functions.html#float>, Parameter(validator=(Number(lt=None, lte=None, gt=None, gte=0, modulo=None),))]

cyclopts.types.NegativeFloat

A float that must be <0.

alias of Annotated[float <https://docs.python.org/3/library/functions.html#float>, Parameter(validator=(Number(lt=0, lte=None, gt=None, gte=None, modulo=None),))]

cyclopts.types.NonPositiveFloat

A float that must be <=0.

alias of Annotated[float <https://docs.python.org/3/library/functions.html#float>, Parameter(validator=(Number(lt=None, lte=0, gt=None, gte=None, modulo=None),))]

cyclopts.types.PositiveInt

An int that must be >0.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=None, gt=0, gte=None, modulo=None),))]

cyclopts.types.NonNegativeInt

An int that must be >=0.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=None, gt=None, gte=0, modulo=None),))]

cyclopts.types.NegativeInt

An int that must be <0.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=0, lte=None, gt=None, gte=None, modulo=None),))]

cyclopts.types.NonPositiveInt

An int that must be <=0.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=0, gt=None, gte=None, modulo=None),))]

cyclopts.types.UInt8

An unsigned 8-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=255, gt=None, gte=0, modulo=None),))]

cyclopts.types.HexUInt8

An unsigned 8-bit integer who's default value will be displayed as hexadecimal in the help-page.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=255, gt=None, gte=0, modulo=None),)), Parameter(show_default=functools.partial(<function _hex_formatter at 0x7f8742aa9f30>, digits=2))]

cyclopts.types.Int8

A signed 8-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=127, gt=None, gte=-128, modulo=None),))]

cyclopts.types.UInt16

An unsigned 16-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=65535, gt=None, gte=0, modulo=None),))]

cyclopts.types.HexUInt16

An unsigned 16-bit integer who's default value will be displayed as hexadecimal in the help-page.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=65535, gt=None, gte=0, modulo=None),)), Parameter(show_default=functools.partial(<function _hex_formatter at 0x7f8742aa9f30>, digits=4))]

cyclopts.types.Int16

A signed 16-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=32767, gt=None, gte=-32768, modulo=None),))]

cyclopts.types.UInt32

An unsigned 32-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=4294967296, lte=None, gt=None, gte=0, modulo=None),))]

cyclopts.types.HexUInt32

An unsigned 32-bit integer who's default value will be displayed as hexadecimal in the help-page.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=4294967296, lte=None, gt=None, gte=0, modulo=None),)), Parameter(show_default=functools.partial(<function _hex_formatter at 0x7f8742aa9f30>, digits=8))]

cyclopts.types.Int32

A signed 32-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=2147483648, lte=None, gt=None, gte=-2147483648, modulo=None),))]

cyclopts.types.UInt64

An unsigned 64-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=18446744073709551616, lte=None, gt=None, gte=0, modulo=None),))]

cyclopts.types.HexUInt64

An unsigned 64-bit integer who's default value will be displayed as hexadecimal in the help-page.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=18446744073709551616, lte=None, gt=None, gte=0, modulo=None),)), Parameter(show_default=functools.partial(<function _hex_formatter at 0x7f8742aa9f30>, digits=16))]

cyclopts.types.Int64

A signed 64-bit integer.

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=9223372036854775808, lte=None, gt=None, gte=-9223372036854775808, modulo=None),))]

Json

Annotated types for parsing a json-string from the CLI.

cyclopts.types.Json

Parse a json-string from the CLI.

Note: Since Cyclopts v3.6.0, all dataclass-like classes now natively attempt to parse json-strings, so practical use-case of this annotation is limited.

Usage example:

from cyclopts import App, types

app = App()

@app.default
def main(json: types.Json):
    print(json)

app()
$ my-script '{"foo": 1, "bar": 2}'
{'foo': 1, 'bar': 2}

alias of Annotated[Any <https://docs.python.org/3/library/typing.html#typing.Any>, Parameter(converter=<function _json_converter at 0x7f8742aa9a60>)]

Web

Annotated types for common web-related values.

cyclopts.types.Email

An email address string with simple validation.

alias of Annotated[str <https://docs.python.org/3/library/stdtypes.html#str>, Parameter(validator=(<function _email_validator at 0x7f8742aa9b10>,))]

cyclopts.types.Port

An int <https://docs.python.org/3/library/functions.html#int> limited to range [0, 65535].

alias of Annotated[int <https://docs.python.org/3/library/functions.html#int>, Parameter(validator=(Number(lt=None, lte=65535, gt=None, gte=0, modulo=None),))]

cyclopts.types.URL

A str <https://docs.python.org/3/library/stdtypes.html#str> URL string with some simple validation.

alias of Annotated[str <https://docs.python.org/3/library/stdtypes.html#str>, Parameter(validator=(<function _url_validator at 0x7f8742aa96f0>,))]

Help Formatting

Cyclopts provides a flexible help formatting system for customizing the help-page's appearance.

class cyclopts.help.protocols.HelpFormatter(*args, **kwargs)

Protocol for help formatter functions.

It's the Formatter's job to transform a HelpPanel into rendered text on the display.

Implementations may optionally provide the following methods for custom rendering of "usage" and "description". If these methods are not provided, default rendering will be used.

def render_usage(self, console: Console, options: ConsoleOptions, usage: Any) -> None:
    """Render the usage line."""
    ...

def render_description(self, console: Console, options: ConsoleOptions, description: Any) -> None:
    """Render the description."""
    ...
__call__(console: Console, options: ConsoleOptions, panel: HelpPanel <#cyclopts.help.HelpPanel>) -> None <https://docs.python.org/3/library/constants.html#None>

Format and render a single help panel.

Parameters
class cyclopts.help.DefaultFormatter(*, panel_spec: PanelSpec <#cyclopts.help.PanelSpec> | None <https://docs.python.org/3/library/constants.html#None> = None, table_spec: TableSpec <#cyclopts.help.TableSpec> | None <https://docs.python.org/3/library/constants.html#None> = None, column_specs: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[ColumnSpec <#cyclopts.help.ColumnSpec>, ...] | ColumnSpecBuilder <#cyclopts.help.protocols.ColumnSpecBuilder> | None <https://docs.python.org/3/library/constants.html#None> = None)

Default help formatter using Rich library with customizable specs.

Parameters
  • panel_spec (Optional[PanelSpec <#cyclopts.help.PanelSpec>]) -- Panel specification for the outer box/panel styling.
  • table_spec (Optional[TableSpec <#cyclopts.help.TableSpec>]) -- Table specification for table styling (borders, padding, etc).
  • column_specs (Optional[Union[tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[ColumnSpec <#cyclopts.help.ColumnSpec>, ...], ColumnSpecBuilder <#cyclopts.help.protocols.ColumnSpecBuilder>]]) -- Column specifications or builder function for table columns.

Notes

The relationship between these specs can be visualized as:

╭─ Commands ───────────────────────────────────────────────────────╮  ← panel_spec
│ serve     Start the development server                           │     (border, title)
│ --help    Display this message and exit.                         │
╰──────────────────────────────────────────────────────────────────╯
 ↑         ↑
 col[0]    col[1]
 (name)    (description)

╭─ Parameters ─────────────────────────────────────────────────────╮  ← panel_spec
│ *  PORT --port        Server port number [required]              │
│    VERBOSE --verbose  Enable verbose output [default: False]     │
╰──────────────────────────────────────────────────────────────────╯
 ↑  ↑                  ↑
 │  col[1]             col[2]
 │  (name/flags)       (description)
 │
 col[0]
 (required marker)

Where:

  • panel_spec controls the outer panel appearance (border, title, etc.)
  • table_spec controls the inner table styling (no visible borders by default)
  • column_specs defines individual columns (width, style, alignment, etc.)
panel_spec: PanelSpec <#cyclopts.help.PanelSpec> | None <https://docs.python.org/3/library/constants.html#None>

Panel specification for the outer box/panel styling (border, title, padding, etc).

table_spec: TableSpec <#cyclopts.help.TableSpec> | None <https://docs.python.org/3/library/constants.html#None>

Table specification for table styling (borders, padding, column separation, etc).

column_specs: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[ColumnSpec <#cyclopts.help.ColumnSpec>, ...] | ColumnSpecBuilder <#cyclopts.help.protocols.ColumnSpecBuilder> | None <https://docs.python.org/3/library/constants.html#None>

Column specifications or builder function for table columns (width, style, alignment, etc).

classmethod with_newline_metadata(**kwargs)

Create formatter with metadata on separate lines.

Returns a DefaultFormatter configured to display parameter metadata (choices, env vars, defaults) on separate indented lines rather than inline with descriptions.

Parameters

**kwargs -- Additional keyword arguments to pass to DefaultFormatter constructor.

Returns

Configured formatter instance with newline metadata display.

Return type

DefaultFormatter <#cyclopts.help.DefaultFormatter>

Examples

>>> from cyclopts import App
>>> from cyclopts.help import DefaultFormatter
>>> app = App(help_formatter=DefaultFormatter.with_newline_metadata())
__call__(console: Console, options: ConsoleOptions, panel: HelpPanel <#cyclopts.help.HelpPanel>) -> None <https://docs.python.org/3/library/constants.html#None>

Format and render a single help panel using Rich.

Parameters
render_usage(console: Console, options: ConsoleOptions, usage: Any <https://docs.python.org/3/library/typing.html#typing.Any>) -> None <https://docs.python.org/3/library/constants.html#None>

Render the usage line.

Parameters
render_description(console: Console, options: ConsoleOptions, description: Any <https://docs.python.org/3/library/typing.html#typing.Any>) -> None <https://docs.python.org/3/library/constants.html#None>

Render the description.

Parameters
class cyclopts.help.PlainFormatter(indent_width: int <https://docs.python.org/3/library/functions.html#int> = 2, max_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None)

Plain text formatter for improved accessibility.

Parameters
__call__(console: Console, options: ConsoleOptions, panel: HelpPanel <#cyclopts.help.HelpPanel>) -> None <https://docs.python.org/3/library/constants.html#None>

Format and render a single help panel as plain text.

Parameters
render_usage(console: Console, options: ConsoleOptions, usage: Any <https://docs.python.org/3/library/typing.html#typing.Any>) -> None <https://docs.python.org/3/library/constants.html#None>

Render the usage line.

Parameters
render_description(console: Console, options: ConsoleOptions, description: Any <https://docs.python.org/3/library/typing.html#typing.Any>) -> None <https://docs.python.org/3/library/constants.html#None>

Render the description.

Parameters
class cyclopts.help.protocols.ColumnSpecBuilder(*args, **kwargs)

Protocol for ColumnSpecBuilders.

__call__(console: Console, options: ConsoleOptions, entries: list <https://docs.python.org/3/library/stdtypes.html#list>[HelpEntry <#cyclopts.help.HelpEntry>]) -> tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[ColumnSpec <#cyclopts.help.ColumnSpec>, ...]

Build column specifications based on console settings and entries.

Parameters
Returns

Tuple of column specifications for table rendering.

Return type

tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[ColumnSpec <#cyclopts.help.ColumnSpec>, ...]

class cyclopts.help.PanelSpec(title: RenderableType | None <https://docs.python.org/3/library/constants.html#None> = None, subtitle: RenderableType | None <https://docs.python.org/3/library/constants.html#None> = None, title_align: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['left', 'center', 'right'] = 'left', subtitle_align: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['left', 'center', 'right'] = 'center', style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = 'none', border_style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = 'none', box: Box | None <https://docs.python.org/3/library/constants.html#None> = None, padding: PaddingDimensions = (0, 1), expand: bool <https://docs.python.org/3/library/functions.html#bool> = True, width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, height: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, safe_box: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None, highlight: bool <https://docs.python.org/3/library/functions.html#bool> = False)

Specification for panel (outer box) styling.

Used by DefaultFormatter to control the appearance of the outer panel that wraps help sections. This spec defines the panel's border, title, subtitle, and overall styling.

See also:

DefaultFormatter

The formatter that uses these specs.

TableSpec

Specification for the inner table.

ColumnSpec

Specification for individual columns.

title: RenderableType | None <https://docs.python.org/3/library/constants.html#None>

Title text displayed at the top of the panel.

Corresponds to the title parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

subtitle: RenderableType | None <https://docs.python.org/3/library/constants.html#None>

Subtitle text displayed at the bottom of the panel.

Corresponds to the subtitle parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

title_align: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['left', 'center', 'right']

Alignment of the title text within the panel.

Corresponds to the title_align parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

subtitle_align: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['left', 'center', 'right']

Alignment of the subtitle text within the panel.

Corresponds to the subtitle_align parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Style applied to the panel background.

Corresponds to the style parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

border_style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Style applied to the panel border.

Corresponds to the border_style parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

box: Box | None <https://docs.python.org/3/library/constants.html#None>

Box drawing style for the panel border.

Corresponds to the box parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>. See rich.box for available styles. Defaults to rich.box.ROUNDED.

padding: PaddingDimensions

Padding inside the panel (top/bottom, left/right) or (top, right, bottom, left).

Corresponds to the padding parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

expand: bool <https://docs.python.org/3/library/functions.html#bool>

Whether the panel should expand to fill available width.

Corresponds to the expand parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Fixed width for the panel in characters.

Corresponds to the width parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

height: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Fixed height for the panel in lines.

Corresponds to the height parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

safe_box: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None>

Whether to use ASCII-safe box characters for compatibility.

Corresponds to the safe_box parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

highlight: bool <https://docs.python.org/3/library/functions.html#bool>

Enable automatic highlighting of panel contents.

Corresponds to the highlight parameter of Panel <https://rich.readthedocs.io/en/stable/reference/panel.html#rich.panel.Panel>.

build(renderable: RenderableType, **overrides) -> Panel

Create a Panel around renderable. Use kwargs to override spec per render.

copy(**kwargs)

class cyclopts.help.TableSpec(title: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, caption: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = None, border_style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = None, header_style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = None, footer_style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = None, box: Box | None <https://docs.python.org/3/library/constants.html#None> = None, show_header: bool <https://docs.python.org/3/library/functions.html#bool> = False, show_footer: bool <https://docs.python.org/3/library/functions.html#bool> = False, show_lines: bool <https://docs.python.org/3/library/functions.html#bool> = False, show_edge: bool <https://docs.python.org/3/library/functions.html#bool> = True, expand: bool <https://docs.python.org/3/library/functions.html#bool> = False, pad_edge: bool <https://docs.python.org/3/library/functions.html#bool> = False, padding: PaddingDimensions = (0, 2, 0, 0), collapse_padding: bool <https://docs.python.org/3/library/functions.html#bool> = False, width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, min_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, safe_box: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None)

Specification for table layout and styling.

Used by DefaultFormatter to control the appearance of tables that display commands and parameters. This spec defines table-wide properties like borders, headers, and padding.

See also:

DefaultFormatter

The formatter that uses these specs.

ColumnSpec

Specification for individual columns.

PanelSpec

Specification for the outer panel.

title: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None>

Title text displayed above the table.

Corresponds to the title parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

caption: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None>

Caption text displayed below the table.

Corresponds to the caption parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Default style applied to the entire table.

Corresponds to the style parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

border_style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Style applied to table borders.

Corresponds to the border_style parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

header_style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Default style for all table headers (can be overridden per column).

Corresponds to the header_style parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

footer_style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Default style for all table footers (can be overridden per column).

Corresponds to the footer_style parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

box: Box | None <https://docs.python.org/3/library/constants.html#None>

Box drawing style for the table borders.

Corresponds to the box parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>. See rich.box for available styles.

show_header: bool <https://docs.python.org/3/library/functions.html#bool>

Whether to display column headers.

Corresponds to the show_header parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

show_footer: bool <https://docs.python.org/3/library/functions.html#bool>

Whether to display column footers.

Corresponds to the show_footer parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

show_lines: bool <https://docs.python.org/3/library/functions.html#bool>

Whether to show horizontal lines between rows.

Corresponds to the show_lines parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

show_edge: bool <https://docs.python.org/3/library/functions.html#bool>

Whether to draw a box around the outside of the table.

Corresponds to the show_edge parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

expand: bool <https://docs.python.org/3/library/functions.html#bool>

Whether the table should expand to fill available width.

Corresponds to the expand parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

pad_edge: bool <https://docs.python.org/3/library/functions.html#bool>

Whether to add padding to the table edges.

Corresponds to the pad_edge parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

padding: PaddingDimensions

Padding around cell content (top, right, bottom, left).

Corresponds to the padding parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

collapse_padding: bool <https://docs.python.org/3/library/functions.html#bool>

Whether to collapse padding when adjacent cells are empty.

Corresponds to the collapse_padding parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Fixed width for the table in characters.

Corresponds to the width parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

min_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Minimum width for the table in characters.

Corresponds to the min_width parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

safe_box: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None>

Whether to use ASCII-safe box characters for compatibility.

Corresponds to the safe_box parameter of Table <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table>.

build(columns: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[ColumnSpec <#cyclopts.help.ColumnSpec>, ...], entries: Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[HelpEntry <#cyclopts.help.HelpEntry>], **overrides) -> Table

Construct and populate a rich.Table.

Parameters
  • columns (tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[ColumnSpec <#cyclopts.help.ColumnSpec>, ...]) -- Column specifications defining the table structure.
  • entries (Iterable[HelpEntry <#cyclopts.help.HelpEntry>]) -- Table entries to populate the table with.
  • **overrides -- Per-render overrides for table settings.
Returns

A populated Rich Table.

Return type

Table

copy(**kwargs)

class cyclopts.help.ColumnSpec(renderer: str <https://docs.python.org/3/library/stdtypes.html#str> | Renderer, header: str <https://docs.python.org/3/library/stdtypes.html#str> = '', footer: str <https://docs.python.org/3/library/stdtypes.html#str> = '', header_style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = None, footer_style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = None, style: StyleType | None <https://docs.python.org/3/library/constants.html#None> = None, justify: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['default', 'left', 'center', 'right', 'full'] = 'left', vertical: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['top', 'middle', 'bottom'] = 'top', overflow: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['fold', 'crop', 'ellipsis', 'ignore'] = 'ellipsis', width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, min_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, max_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, ratio: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None, no_wrap: bool <https://docs.python.org/3/library/functions.html#bool> = False, highlight: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None> = None)

Specification for a single column in a help table.

Used by DefaultFormatter to define how individual columns are rendered in help tables. Each column can have its own renderer, styling, and layout properties.

See also:

DefaultFormatter

The formatter that uses these specs.

TableSpec

Specification for the entire table.

PanelSpec

Specification for the outer panel.

renderer: str <https://docs.python.org/3/library/stdtypes.html#str> | Renderer

Specifies how to extract and render cell content from a HelpEntry.

Can be either:

  • A string: The attribute name to retrieve from HelpEntry (e.g., 'names', 'description', 'required', 'type'). The string is displayed as-is.
  • A callable: A function matching the Renderer protocol. The function receives a HelpEntry and should return a RenderableType (str, Text <https://rich.readthedocs.io/en/stable/reference/text.html#rich.text.Text>, or other Rich renderable).

Examples:

# String renderer - get attribute directly
ColumnSpec(renderer="description")

# Callable renderer - custom formatting
def format_names(entry: HelpEntry) -> str:
    return ", ".join(entry.names) if entry.names else ""
ColumnSpec(renderer=format_names)
header: str <https://docs.python.org/3/library/stdtypes.html#str>

Column header text displayed at the top of the column.

Example:

header="Options" renders:
┌─────────┬─────────────┐
│ Options │ Description │
├─────────┼─────────────┤
│ --help  │ Show help   │
└─────────┴─────────────┘
footer: str <https://docs.python.org/3/library/stdtypes.html#str>

Column footer text displayed at the bottom of the column.

Example:

footer="Required" renders:
┌──────────┬────────────┐
│ --help   │ Show help  │
├──────────┼────────────┤
│ Required │            │
└──────────┴────────────┘
header_style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Style applied to the column header text.

Corresponds to the header_style parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

footer_style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Style applied to the column footer text.

Corresponds to the footer_style parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

style: StyleType | None <https://docs.python.org/3/library/constants.html#None>

Default style applied to all cells in this column.

Corresponds to the style parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

justify: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['default', 'left', 'center', 'right', 'full']

Text justification within the column.

Corresponds to the justify parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

vertical: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['top', 'middle', 'bottom']

Vertical alignment of text within cells.

Corresponds to the vertical parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

overflow: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['fold', 'crop', 'ellipsis', 'ignore']

How to handle text that exceeds column width.

Corresponds to the overflow parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Fixed width for the column in characters.

Corresponds to the width parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

min_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Minimum width for the column in characters.

Corresponds to the min_width parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

max_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Maximum width for the column in characters.

Corresponds to the max_width parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

ratio: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None>

Relative width ratio compared to other columns.

Corresponds to the ratio parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

no_wrap: bool <https://docs.python.org/3/library/functions.html#bool>

Prevent text wrapping in the column.

Corresponds to the no_wrap parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

highlight: bool <https://docs.python.org/3/library/functions.html#bool> | None <https://docs.python.org/3/library/constants.html#None>

Enable automatic highlighting of text in the column.

Corresponds to the highlight parameter of rich.table.Table.add_column() <https://rich.readthedocs.io/en/stable/reference/table.html#rich.table.Table.add_column>.

copy(**kwargs)

class cyclopts.help.NameRenderer(max_width: int <https://docs.python.org/3/library/functions.html#int> | None <https://docs.python.org/3/library/constants.html#None> = None)

Renderer for parameter/command names with optional text wrapping.

Parameters

max_width (int <https://docs.python.org/3/library/functions.html#int> | None) -- Maximum width for wrapping. If None, no wrapping is applied.

Initialize the renderer with formatting options.

Parameters

max_width (int <https://docs.python.org/3/library/functions.html#int> | None) -- Maximum width for wrapping. If None, no wrapping is applied.

__call__(entry: HelpEntry <#cyclopts.help.HelpEntry>) -> RenderableType

Render the names column with optional text wrapping.

Parameters

entry (HelpEntry <#cyclopts.help.HelpEntry>) -- The table entry to render.

Returns

Combined names and shorts, optionally wrapped. Order: positive_names, positive_shorts, negative_names, negative_shorts

Return type

RenderableType

class cyclopts.help.DescriptionRenderer(newline_metadata: bool <https://docs.python.org/3/library/functions.html#bool> = False)

Renderer for descriptions with configurable metadata formatting.

Parameters

newline_metadata (bool <https://docs.python.org/3/library/functions.html#bool>) -- If True, display metadata (choices, env vars, defaults) on separate lines. If False (default), display metadata inline with the description.

Initialize the renderer with formatting options.

Parameters

newline_metadata (bool <https://docs.python.org/3/library/functions.html#bool>) -- If True, display metadata on separate lines instead of inline.

__call__(entry: HelpEntry <#cyclopts.help.HelpEntry>) -> RenderableType

Render parameter description with metadata annotations.

Enriches the base description with choices, environment variables, default values, and required status.

Parameters

entry (HelpEntry <#cyclopts.help.HelpEntry>) -- The table entry to render.

Returns

Description with appended metadata.

Return type

RenderableType

class cyclopts.help.AsteriskRenderer

Renderer for required parameter asterisk indicator.

A simple renderer that displays an asterisk (*) for required parameters.

__call__(entry: HelpEntry <#cyclopts.help.HelpEntry>) -> RenderableType

Render an asterisk for required parameters.

Parameters

entry (HelpEntry <#cyclopts.help.HelpEntry>) -- The table entry to render.

Returns

An asterisk if the entry is required, empty string otherwise.

Return type

RenderableType

class cyclopts.help.HelpPanel(format: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['command', 'parameter'], title: RenderableType, description=None, entries: list <https://docs.python.org/3/library/stdtypes.html#list>[HelpEntry <#cyclopts.help.HelpEntry>] = NOTHING)

Data container for help panel information.

format: Literal <https://docs.python.org/3/library/typing.html#typing.Literal>['command', 'parameter']

Panel format type.

title: RenderableType

The title text displayed at the top of the help panel.

description: Any <https://docs.python.org/3/library/typing.html#typing.Any>

Optional description text displayed below the title.

Typically a str <https://docs.python.org/3/library/stdtypes.html#str> or a RenderableType <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.RenderableType>

entries: list <https://docs.python.org/3/library/stdtypes.html#list>[HelpEntry <#cyclopts.help.HelpEntry>]

List of help entries to display (in order) in the panel.

copy(**kwargs)

class cyclopts.help.HelpEntry(*, positive_names: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] = (), positive_shorts: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] = (), negative_names: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] = (), negative_shorts: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] = (), description: Any <https://docs.python.org/3/library/typing.html#typing.Any> = None, required: bool <https://docs.python.org/3/library/functions.html#bool> = False, sort_key: Any <https://docs.python.org/3/library/typing.html#typing.Any> = None, type: Any <https://docs.python.org/3/library/typing.html#typing.Any> | None <https://docs.python.org/3/library/constants.html#None> = None, choices: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] | None <https://docs.python.org/3/library/constants.html#None> = None, env_var: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] | None <https://docs.python.org/3/library/constants.html#None> = None, default: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None)

Container for help table entry data.

positive_names: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

Positive long option names (e.g., "--verbose", "--dry-run").

positive_shorts: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

Positive short option names (e.g., "-v", "-n").

negative_names: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

Negative long option names (e.g., "--no-verbose", "--no-dry-run").

negative_shorts: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

Negative short option names (e.g., "-N"). Rarely used.

property names: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

All long option names (positive + negative). For backward compatibility.

property shorts: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

All short option names (positive + negative). For backward compatibility.

property all_options: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...]

positive longs, positive shorts, negative longs, negative shorts.

Type

All options in display order

description: Any <https://docs.python.org/3/library/typing.html#typing.Any>

Help text description for this entry.

Typically a str <https://docs.python.org/3/library/stdtypes.html#str> or a RenderableType <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.RenderableType>

required: bool <https://docs.python.org/3/library/functions.html#bool>

Whether this parameter/command is required.

sort_key: Any <https://docs.python.org/3/library/typing.html#typing.Any>

Custom sorting key for ordering entries.

type: Any <https://docs.python.org/3/library/typing.html#typing.Any> | None <https://docs.python.org/3/library/constants.html#None>

Type annotation of the parameter.

choices: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] | None <https://docs.python.org/3/library/constants.html#None>

Available choices for this parameter.

env_var: tuple <https://docs.python.org/3/library/stdtypes.html#tuple>[str <https://docs.python.org/3/library/stdtypes.html#str>, ...] | None <https://docs.python.org/3/library/constants.html#None>

Environment variable names that can set this parameter.

default: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None>

Default value for this parameter to display. None means no default to show.

copy(**kwargs)

Config

Cyclopts has builtin configuration classes to be used with App.config for loading user-defined defaults in many common scenarios. All Cyclopts builtins index into the configuration file with the following rules:

1.

Apply root_keys (if provided) to enter the project's configuration namespace.

2.

Apply the command name(s) to enter the current command's configuration namespace.

3.

Apply each key/value pair if CLI arguments have not been provided for that parameter.

class cyclopts.config.Toml(path, *, root_keys: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = (), allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False, use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True, source: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, must_exist: bool <https://docs.python.org/3/library/functions.html#bool> = False, search_parents: bool <https://docs.python.org/3/library/functions.html#bool> = False)

Automatically read configuration from Toml file.

path: str <https://docs.python.org/3/library/stdtypes.html#str> | pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>

Path to TOML configuration file.

source: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Identifier for the configuration source, used in error messages. If not provided, defaults to the absolute path of the configuration file.

root_keys: Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = None

The key or sequence of keys that lead to the root configuration structure for this app. For example, if referencing a pyproject.toml, it is common to store all of your projects configuration under:

[tool.myproject]

So, your Cyclopts App should be configured as:

app = cyclopts.App(config=cyclopts.config.Toml("pyproject.toml", root_keys=("tool", "myproject")))
must_exist: bool <https://docs.python.org/3/library/functions.html#bool> = False

The configuration file MUST exist. Raises FileNotFoundError <https://docs.python.org/3/library/exceptions.html#FileNotFoundError> if it does not exist.

search_parents: bool <https://docs.python.org/3/library/functions.html#bool> = False

If path doesn't exist, iteratively search parenting directories for a same-named configuration file. Raises FileNotFoundError <https://docs.python.org/3/library/exceptions.html#FileNotFoundError> if no configuration file is found.

allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False

Allow for unknown keys. Otherwise, if an unknown key is provided, raises UnknownOptionError.

use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True

Use the sequence of commands as keys into the configuration.

For example, the following CLI invocation:

$ python my-script.py my-command

Would search into ["my-command"] for values.

class cyclopts.config.Yaml(path, *, root_keys: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = (), allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False, use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True, source: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, must_exist: bool <https://docs.python.org/3/library/functions.html#bool> = False, search_parents: bool <https://docs.python.org/3/library/functions.html#bool> = False)

Automatically read configuration from YAML file.

path: str <https://docs.python.org/3/library/stdtypes.html#str> | pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>

Path to YAML configuration file.

source: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Identifier for the configuration source, used in error messages. If not provided, defaults to the absolute path of the configuration file.

root_keys: Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = None

The key or sequence of keys that lead to the root configuration structure for this app. For example, if referencing a common config.yaml that is shared with other applications, it is common to store your projects configuration under a key like myproject:.

Your Cyclopts App would be configured as:

app = cyclopts.App(config=cyclopts.config.Yaml("config.yaml", root_keys="myproject"))
must_exist: bool <https://docs.python.org/3/library/functions.html#bool> = False

The configuration file MUST exist. Raises FileNotFoundError <https://docs.python.org/3/library/exceptions.html#FileNotFoundError> if it does not exist.

search_parents: bool <https://docs.python.org/3/library/functions.html#bool> = False

If path doesn't exist, iteratively search parenting directories for a same-named configuration file. Raises FileNotFoundError <https://docs.python.org/3/library/exceptions.html#FileNotFoundError> if no configuration file is found.

allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False

Allow for unknown keys. Otherwise, if an unknown key is provided, raises UnknownOptionError.

use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True

Use the sequence of commands as keys into the configuration.

For example, the following CLI invocation:

$ python my-script.py my-command

Would search into ["my-command"] for values.

class cyclopts.config.Json(path, *, root_keys: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = (), allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False, use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True, source: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, must_exist: bool <https://docs.python.org/3/library/functions.html#bool> = False, search_parents: bool <https://docs.python.org/3/library/functions.html#bool> = False)

Automatically read configuration from Json file.

path: str <https://docs.python.org/3/library/stdtypes.html#str> | pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>

Path to JSON configuration file.

source: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None

Identifier for the configuration source, used in error messages. If not provided, defaults to the absolute path of the configuration file. Can be customized to provide more descriptive error context.

Example:

app = cyclopts.App(config=cyclopts.config.Json("config.json", source="production-config"))
root_keys: Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = None

The key or sequence of keys that lead to the root configuration structure for this app. For example, if referencing a common config.json that is shared with other applications, it is common to store your projects configuration under a key like "myproject":.

Your Cyclopts App would be configured as:

app = cyclopts.App(config=cyclopts.config.Json("config.json", root_keys="myproject"))
must_exist: bool <https://docs.python.org/3/library/functions.html#bool> = False

The configuration file MUST exist. Raises FileNotFoundError <https://docs.python.org/3/library/exceptions.html#FileNotFoundError> if it does not exist.

search_parents: bool <https://docs.python.org/3/library/functions.html#bool> = False

If path doesn't exist, iteratively search parenting directories for a same-named configuration file. Raises FileNotFoundError <https://docs.python.org/3/library/exceptions.html#FileNotFoundError> if no configuration file is found.

allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False

Allow for unknown keys. Otherwise, if an unknown key is provided, raises UnknownOptionError.

use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True

Use the sequence of commands as keys into the configuration.

For example, the following CLI invocation:

$ python my-script.py my-command

Would search into ["my-command"] for values.

class cyclopts.config.Dict(data: dict <https://docs.python.org/3/library/stdtypes.html#dict>[str <https://docs.python.org/3/library/stdtypes.html#str>, Any <https://docs.python.org/3/library/typing.html#typing.Any>], *, root_keys: None <https://docs.python.org/3/library/constants.html#None> | Any <https://docs.python.org/3/library/typing.html#typing.Any> | Iterable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable>[Any <https://docs.python.org/3/library/typing.html#typing.Any>] = (), allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False, use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True, source: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None)

Configuration source from an in-memory dictionary.

Useful for programmatically generated configurations.

Use an in-memory Python dictionary as configuration source.

data: dict <https://docs.python.org/3/library/stdtypes.html#dict>[str <https://docs.python.org/3/library/stdtypes.html#str>, Any]

The configuration dictionary.

source: str <https://docs.python.org/3/library/stdtypes.html#str> = "dict"

Identifier for the configuration source, used in error messages.

root_keys: Iterable[str <https://docs.python.org/3/library/stdtypes.html#str>] = ()

The key or sequence of keys that lead to the root configuration structure for this app.

allow_unknown: bool <https://docs.python.org/3/library/functions.html#bool> = False

Allow for unknown keys. Otherwise, if an unknown key is provided, raises UnknownOptionError.

use_commands_as_keys: bool <https://docs.python.org/3/library/functions.html#bool> = True

Use the sequence of commands as keys into the configuration.

class cyclopts.config.Env(prefix: str <https://docs.python.org/3/library/stdtypes.html#str> = '', *, source: str <https://docs.python.org/3/library/stdtypes.html#str> = 'env', command: bool <https://docs.python.org/3/library/functions.html#bool> = True, show: bool <https://docs.python.org/3/library/functions.html#bool> = True)

Automatically derive environment variable names to read configurations from.

For example, consider the following app:

import cyclopts

app = cyclopts.App(config=cyclopts.config.Env("MY_SCRIPT_"))

@app.command
def my_command(foo, bar):
    print(f"{foo=} {bar=}")

app()

If values for foo and bar are not supplied by the command line, the app will check the environment variables MY_SCRIPT_MY_COMMAND_FOO and MY_SCRIPT_MY_COMMAND_BAR, respectively:

$ python my_script.py my-command 1 2
foo=1 bar=2

$ export MY_SCRIPT_MY_COMMAND_FOO=100
$ python my_script.py my-command --bar=2
foo=100 bar=2
$ python my_script.py my-command 1 2
foo=1 bar=2
prefix: str <https://docs.python.org/3/library/stdtypes.html#str> = ""

String to prepend to all autogenerated environment variable names. Typically ends in _, and is something like MY_APP_.

source: str <https://docs.python.org/3/library/stdtypes.html#str> = "env"

Identifier for the configuration source, used in error messages and token tracking. Defaults to "env".

command: bool <https://docs.python.org/3/library/functions.html#bool> = True

If True <https://docs.python.org/3/library/constants.html#True>, add the command's name (uppercase) after prefix.

show: bool <https://docs.python.org/3/library/functions.html#bool> = True

If True <https://docs.python.org/3/library/constants.html#True>, then show the environment variables on the help-page.

Exceptions

exception cyclopts.CycloptsError

Bases: Exception <https://docs.python.org/3/library/exceptions.html#Exception>

Root exception for runtime errors.

As CycloptsErrors bubble up the Cyclopts call-stack, more information is added to it.

msg: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None>

If set, override automatic message generation.

verbose: bool <https://docs.python.org/3/library/functions.html#bool>

More verbose error messages; aimed towards developers debugging their Cyclopts app. Defaults to False.

root_input_tokens: list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None>

The parsed CLI tokens that were initially fed into the App.

unused_tokens: list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None>

Leftover tokens after parsing is complete.

target: Callable <https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable> | None <https://docs.python.org/3/library/constants.html#None>

The python function associated with the command being parsed.

argument: Argument <#cyclopts.Argument> | None <https://docs.python.org/3/library/constants.html#None>

Argument that was matched.

command_chain: Sequence <https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence>[str <https://docs.python.org/3/library/stdtypes.html#str>] | None <https://docs.python.org/3/library/constants.html#None>

List of command that lead to target.

app: App <#cyclopts.App> | None <https://docs.python.org/3/library/constants.html#None>

The Cyclopts application itself.

console: Console | None <https://docs.python.org/3/library/constants.html#None>

Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> to display runtime errors.

exception cyclopts.ValidationError

Bases: CycloptsError

Validator function raised an exception.

exception_message: str <https://docs.python.org/3/library/stdtypes.html#str>

Parenting Assertion/Value/Type Error message.

group: Group <#cyclopts.Group> | None <https://docs.python.org/3/library/constants.html#None>

If a group validator caused the exception.

value: Any <https://docs.python.org/3/library/typing.html#typing.Any>

Converted value that failed validation.

exception cyclopts.UnknownOptionError

Bases: CycloptsError

Unknown/unregistered option provided by the cli.

A nearest-neighbor parameter suggestion may be printed.

token: Token <#cyclopts.Token>

Token without a matching parameter.

argument_collection: ArgumentCollection <#cyclopts.ArgumentCollection>

Argument collection of plausible options.

exception cyclopts.CoercionError

Bases: CycloptsError

There was an error performing automatic type coercion.

token: Token <#cyclopts.Token> | None <https://docs.python.org/3/library/constants.html#None>

Input token that couldn't be coerced.

target_type: type <#cyclopts.help.HelpEntry.type> | None <https://docs.python.org/3/library/constants.html#None>

Intended type to coerce into.

exception cyclopts.UnknownCommandError

Bases: CycloptsError

CLI token combination did not yield a valid command.

msg

If set, override automatic message generation.

verbose

More verbose error messages; aimed towards developers debugging their Cyclopts app. Defaults to False.

root_input_tokens

The parsed CLI tokens that were initially fed into the App.

unused_tokens

Leftover tokens after parsing is complete.

target

The python function associated with the command being parsed.

argument

Argument that was matched.

command_chain

List of command that lead to target.

app

The Cyclopts application itself.

console

Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> to display runtime errors.

exception cyclopts.UnusedCliTokensError

Bases: CycloptsError

Not all CLI tokens were used as expected.

exception cyclopts.MissingArgumentError

Bases: CycloptsError

A required argument was not provided.

tokens_so_far: list <https://docs.python.org/3/library/stdtypes.html#list>[str <https://docs.python.org/3/library/stdtypes.html#str>]

If the matched parameter requires multiple tokens, these are the ones we have parsed so far.

keyword: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None>

The keyword that was used when the error was raised (e.g., '-o' instead of '--option').

exception cyclopts.RequiresEqualsError

Bases: CycloptsError

A long option requires = to assign a value (e.g., --option=value).

keyword: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None>

The keyword that was used (e.g., '--name').

exception cyclopts.RepeatArgumentError

Bases: CycloptsError

The same parameter has erroneously been specified multiple times.

token: Token <#cyclopts.Token>

The repeated token.

exception cyclopts.MixedArgumentError

Bases: CycloptsError

Cannot supply keywords and non-keywords to the same argument.

class cyclopts.CommandCollisionError

Bases: Exception <https://docs.python.org/3/library/exceptions.html#Exception>

A command with the same name has already been registered to the app.

exception cyclopts.CombinedShortOptionError

Bases: CycloptsError

Cannot combine short, token-consuming options with short flags.

class cyclopts.EditorError

Bases: Exception <https://docs.python.org/3/library/exceptions.html#Exception>

Root editor-related error.

Root exception raised by all exceptions in edit().

class cyclopts.EditorNotFoundError

Bases: EditorError

Could not find a valid text editor for :func`.edit`.

class cyclopts.EditorDidNotSaveError

Bases: EditorError

User did not save upon exiting edit().

class cyclopts.EditorDidNotChangeError

Bases: EditorError

User did not edit file contents in edit().

CLI Reference

The cyclopts package includes a command-line interface for various development tasks.

cyclopts --install-completion

cyclopts --install-completion [OPTIONS]

Register shell-completion for the cyclopts CLI itself.

Parameters:

--shell

Shell type for completion. If not specified, attempts to auto-detect current shell. [Choices: zsh, bash, fish]

--output,  -o

Output path for the completion script. If not specified, uses shell-specific default.

cyclopts generate-docs

cyclopts generate-docs [OPTIONS] SCRIPT [ARGS]

Generate documentation for a Cyclopts application.

Parameters:

SCRIPT, --script

Python script path, optionally with ':app_object' notation to specify the App object. If not specified, will search for App objects in the script's global namespace. [Required]

OUTPUT, --output, -o

Output file path. If not specified, prints to stdout.

--format, -f

Output format for documentation. If not specified, inferred from output file extension. [Choices: markdown, md, html, htm, rst, rest, restructuredtext]

--include-hidden

Include hidden commands in documentation. [Default: False]

--heading-level

Starting heading level for markdown format. [Default: 1]

cyclopts run

cyclopts run SCRIPT [ARGS...]

Run a Cyclopts application from a Python script with dynamic shell completion.

All arguments after the script path are passed to the loaded application.

Shell completion is available. Run once to install (persistent): cyclopts --install-completion

Arguments:

SCRIPT

Python script path with optional ':app_object' notation. [Required]

ARGS

Arguments to pass to the loaded application.

Known Issues

This document intends to record any known long-standing issues/limitations with Cyclopts. While this document should always be up to date, please also visit the github-issues page <https://github.com/BrianPugh/cyclopts/issues> for more information & discussion.

from __future__ import annotations

Due to quirks in the python-typing system, Cyclopts can only support some scenarios surrounding PEP-0563 <https://peps.python.org/563>, the strinigization of type hints via from __future__ import annotations. Notably, this can also sometimes break dataclass definitions when inheritance from multiple python modules is involved. Attempts have been made to improve Cyclopts support, but there are the following blockers:

  1. CPython has some bugs <https://github.com/python/cpython/issues/89687> around typing.get_type_hints() <https://docs.python.org/3/library/typing.html#typing.get_type_hints>. It is outside the scope of Cyclopts to compensate for the complex task of type-hint scoping and resolution.
  2. Particularly with dataclasses, it looks like they will be fixing these bugs, but it would only be backported to 3.13 and 3.14 <https://github.com/python/cpython/issues/133956#issuecomment-2883646533>. This limitation to very modern python versions makes a lot of PEP-0563 moot.
  3. PEP-0649 <https://peps.python.org/649> and PEP-0749 <https://peps.python.org/749> deprecate the usage of from __future__ import annotations. This suggests that it is not worth the long-term maintenance of supporting the complications of this feature.

Original discussion on GitHub. <https://github.com/BrianPugh/cyclopts/issues/439>

Lazy Loading

Lazy loading allows you to register commands using import path strings instead of direct function references. This defers importing command modules until they are actually executed, which could significantly improve CLI startup time for large applications that have expensive per-command imports.

Basic Usage

Instead of importing and registering a function directly:

from cyclopts import App
from myapp.commands.users import create, delete, list_users  # Imported immediately

app = App()
user_app = App(name="user")

user_app.command(create)
user_app.command(delete)
user_app.command(list_users, name="list")

app.command(user_app)

Use an import path string:

from cyclopts import App

app = App()
user_app = App(name="user")

# No imports! Modules loaded only when commands execute
user_app.command("myapp.commands.users:create")
user_app.command("myapp.commands.users:delete")
user_app.command("myapp.commands.users:list_users", name="list")

app.command(user_app)

The import path format is "module.path:function_name", similar to setuptools entry points.

Lazy commands are resolved/imported in these situations:

  • Command Execution - When the user runs that specific command
  • Help Generation - When displaying help that includes the command
  • Direct Access - When accessing via app["command_name"]

In order to benefit from lazy loading, you have to make sure that the files are not imported by other means when your CLI starts up.

Import Path Format

The import path string has two parts separated by a colon (:):

Module Path (before the :)

The Python module to import, using dot notation (e.g., myapp.commands.users).

Attribute Name (after the :)

The function or App to get from the module using getattr() <https://docs.python.org/3/library/functions.html#getattr>.

Examples:

# Simple function in a module
app.command("myapp.commands:create_user")

# Nested module path
app.command("myapp.admin.database.operations:migrate")

# Import an App instance, exposed to the CLI as "admin"
app.command("myapp.admin:admin_app", name="admin")
Note:

The attribute name (after :) is the actual Python name, not the CLI command name. Use the name parameter to specify the CLI command name.

Name vs Function Name

The name parameter specifies how the command appears in the CLI, while the import path specifies what code to execute. They can be completely different:

from cyclopts import App

user_app = App(name="user")

# Function name: "list_users"
# CLI command name: "list"
user_app.command("myapp.commands.users:list_users", name="list")

# Function name: "delete"
# CLI command name: "remove"
user_app.command("myapp.commands.users:delete", name="remove")
$ myapp user list --limit 10
# Imports and runs myapp.commands.users:list_users

$ myapp user remove --username alice
# Imports and runs myapp.commands.users:delete

If name is not specified, Cyclopts derives it from the function name with App.name_transform <#cyclopts.App.name_transform> applied (typically converting underscores to hyphens).

Error Handling

If an import path/configuration is invalid, the error occurs when the command is executed, not when it's registered:

from cyclopts import App

app = App()

# This won't error immediately - registration succeeds
app.command("nonexistent.module:func")

app()
$ myapp func
# Now the error occurs:
ImportError: Cannot import module 'nonexistent.module'

To catch import errors early, you can access the command during testing:

import pytest
from cyclopts import App

def test_lazy_commands_are_importable():
    app = App()
    app.command("myapp.commands:create")

    # This will trigger the import and fail if path is wrong
    resolved = app["create"]
    assert resolved is not None

Groups and Lazy Loading

Tip:

TL;DR: Define Group <#cyclopts.Group> objects used by commands in your main CLI module, NOT in lazy-loaded modules.

Group <#cyclopts.Group> objects defined in unresolved lazy modules won't be available until those modules are explicitly imported. To avoid this, define Group <#cyclopts.Group> objects in non-lazy modules.

# myapp/cli.py (always imported)
from cyclopts import App, Group

# Define Group objects here
admin_group = Group("Admin Commands", validator=require_admin_role)
db_group = Group("Database", default_parameter=Parameter(envvar_prefix="DB_"))

app = App()

# Lazy commands can reference the Group objects
app.command("myapp.admin:create_user", group=admin_group)
app.command("myapp.admin:delete_user", group=admin_group)
app.command("myapp.db:migrate", group=db_group)

What to avoid: Defining Group objects inside lazy-loaded modules:

# myapp/admin.py (lazy-loaded)
from cyclopts import App, Group

# BAD: This Group won't be available to other commands until this module is imported
admin_group = Group("Admin Commands", validator=require_admin_role)

def create_user():
    ...

If you reference a group by string (e.g., group="Admin Commands") and the Group <#cyclopts.Group> object with that name is only defined in an unresolved lazy module, the group won't be available until that lazy module is imported. This means that:

  • Validators defined on the lazy-loaded Group <#cyclopts.Group> won't be applied to commands in other modules.
  • Group.default_parameter <#cyclopts.Group.default_parameter> and other settings won't be inherited by commands referencing the group by string.

Once the lazy module is imported (e.g., by executing one of its commands), the Group <#cyclopts.Group> object becomes available and subsequent operations will use it correctly.

Help Customization

Cyclopts provides extensive customization options for help screen appearance and formatting through the help_formatter parameter available on both App <#cyclopts.App.help_formatter> and Group <#cyclopts.Group.help_formatter>. These parameters accept formatters that follow the HelpFormatter <#cyclopts.help.protocols.HelpFormatter> protocol.

Setting Help Formatters

App-Level Formatting

The App <#cyclopts.App> class accepts a help_formatter parameter that controls the default formatting for all help output:

from cyclopts import App
from cyclopts.help import DefaultFormatter, PlainFormatter

# Use a built-in formatter by name: {"default", "plain"}
app = App(help_formatter="plain")

# Or pass a formatter instance with custom configuration
app = App(
    help_formatter=DefaultFormatter(
        # Custom configuration options
    )
)

# Or use a completely custom formatter; see HelpFormatter protocol.
app = App(help_formatter=MyCustomFormatter())

Group-Level Formatting

Individual Group <#cyclopts.Group> instances can have their own help_formatter that overrides the app-level default:

from cyclopts import App, Group
from cyclopts.help import DefaultFormatter, PanelSpec
from rich.box import DOUBLE

# Create a group with custom formatting
advanced_group = Group(
    "Advanced Options",
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            border_style="red",
            box=DOUBLE,
        )
    )
)

# The app can have a different default formatter
app = App(help_formatter="plain")

# Parameters in advanced_group will use the group's formatter,
# while other parameters use the app's formatter

Built-in Formatters

DefaultFormatter

The DefaultFormatter <#cyclopts.help.DefaultFormatter> is the default help formatter that uses Rich <https://github.com/Textualize/rich> for beautiful terminal output with colors, borders, and structured layouts.

from cyclopts import App

# Explicitly use the default formatter (same as not specifying)
app = App(help_formatter="default")

@app.default
def main(name: str, count: int = 1):
    """A simple greeting application.

    Parameters
    ----------
    name : str
        Person to greet.
    count : int
        Number of times to greet.
    """
    for _ in range(count):
        print(f"Hello, {name}!")

if __name__ == "__main__":
    app()

Output:

PlainFormatter

The PlainFormatter <#cyclopts.help.PlainFormatter> provides accessibility-focused plain text output without colors or special characters, ideal for screen readers and simpler terminals.

from cyclopts import App

# Use plain text formatter for accessibility
app = App(help_formatter="plain")

@app.default
def main(name: str, count: int = 1):
    """A simple greeting application.

    Parameters
    ----------
    name : str
        Person to greet.
    count : int
        Number of times to greet.
    """
    for _ in range(count):
        print(f"Hello, {name}!")

if __name__ == "__main__":
    app()

Output:

Usage: demo.py [ARGS] [OPTIONS]

A simple greeting application.

Commands:
--help, -h: Display this message and exit.
--version: Display application version.

Parameters:
NAME, --name: Person to greet.
COUNT, --count: Number of times to greet.

Basic Customization

The DefaultFormatter <#cyclopts.help.DefaultFormatter> accepts several customization options through its initialization parameters.

Panel Customization

The PanelSpec <#cyclopts.help.PanelSpec> controls the outer panel appearance:

from cyclopts import App
from cyclopts.help import DefaultFormatter, PanelSpec
from rich.box import DOUBLE

app = App(
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=DOUBLE,              # Use double-line borders
            border_style="blue",     # Blue border color
            padding=(1, 2),         # (vertical, horizontal) padding
            expand=True,            # Expand to full terminal width
        )
    )
)

@app.default
def main(path: str, verbose: bool = False):
    """Process a file with custom panel styling."""
    print(f"Processing {path}")

if __name__ == "__main__":
    app()

Output:

Table Customization

The TableSpec <#cyclopts.help.TableSpec> controls the table styling within panels:

from cyclopts import App
from cyclopts.help import DefaultFormatter, TableSpec

app = App(
    help_formatter=DefaultFormatter(
        table_spec=TableSpec(
            show_header=True,  # Show column headers
            show_lines=True,  # Show lines between rows
            show_edge=False,  # Remove outer table border
            border_style="green",  # Green table elements
            padding=(0, 2, 0, 0),  # Extra right padding
            box=SQUARE,  # otherwise we won't see the lines
        )
    )
)

@app.default
def main(path: str, verbose: bool = False):
    """Process a file with custom table styling."""
    print(f"Processing {path}")

if __name__ == "__main__":
    app()

Output:

Combining Customizations

You can combine both panel and table specifications:

from cyclopts import App
from cyclopts.help import DefaultFormatter, PanelSpec, TableSpec
from rich.box import ROUNDED

app = App(
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=ROUNDED,
            border_style="cyan",
            padding=(0, 1),
        ),
        table_spec=TableSpec(
            show_header=False,
            show_lines=False,
            padding=(0, 1),
        )
    )
)

@app.default
def main(path: str, verbose: bool = False):
    """Process a file with combined customizations."""
    print(f"Processing {path}")

if __name__ == "__main__":
    app()

Output:

Group-Level Formatting

Different parameter groups can have different formatting styles, allowing you to visually distinguish between different types of options:

from cyclopts import App, Group, Parameter
from cyclopts.help import DefaultFormatter, PanelSpec
from rich.box import DOUBLE, MINIMAL
from typing import Annotated

# Create groups with different styles
required_group = Group(
    "Required Options",
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=DOUBLE,
            border_style="red bold",
        )
    )
)

optional_group = Group(
    "Optional Settings",
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=MINIMAL,
            border_style="green",
        )
    )
)

app = App()

@app.default
def main(
    # Required parameters with red double border
    input_file: Annotated[str, Parameter(group=required_group)],
    output_dir: Annotated[str, Parameter(group=required_group)],

    # Optional parameters with green minimal border
    verbose: Annotated[bool, Parameter(group=optional_group)] = False,
    threads: Annotated[int, Parameter(group=optional_group)] = 4,
):
    """Process files with styled help groups."""
    print(f"Processing {input_file} -> {output_dir}")
    if verbose:
        print(f"Using {threads} threads")

if __name__ == "__main__":
    app()

Output:

Custom Column Layout

For complete control over the help table layout, you can define custom columns using ColumnSpec <#cyclopts.help.ColumnSpec>:

from cyclopts import App, Group, Parameter
from cyclopts.help import DefaultFormatter, ColumnSpec, TableSpec
from typing import Annotated

# Define custom column renderers
def names_renderer(entry):
    """Combine parameter names and shorts."""
    names = " ".join(entry.names) if entry.names else ""
    shorts = " ".join(entry.shorts) if entry.shorts else ""
    return f"{names} {shorts}".strip()

def type_renderer(entry):
    """Show the parameter type."""
    from cyclopts.annotations import get_hint_name
    return get_hint_name(entry.type) if entry.type else ""

# Create custom columns
custom_group = Group(
    "Custom Layout",
    help_formatter=DefaultFormatter(
        table_spec=TableSpec(show_header=True),
        column_specs=(
            ColumnSpec(
                renderer=lambda e: "★" if e.required else " ",
                header="",
                width=2,
                style="yellow bold",
            ),
            ColumnSpec(
                renderer=names_renderer,
                header="Option",
                style="cyan",
                max_width=30,
            ),
            ColumnSpec(
                renderer=type_renderer,
                header="Type",
                style="magenta",
                justify="center",
            ),
            ColumnSpec(
                renderer="description",  # Use attribute name
                header="Description",
                overflow="fold",
            ),
        )
    )
)

app = App()

@app.default
def main(
    input_path: Annotated[str, Parameter(group=custom_group, help="Input file path")],
    output_path: Annotated[str, Parameter(group=custom_group, help="Output file path")],
    count: Annotated[int, Parameter(group=custom_group, help="Number of iterations")] = 1,
):
    """Demo custom column layout."""
    print(f"Processing {input_path} -> {output_path} ({count} times)")

if __name__ == "__main__":
    app()

Output:

Dynamic Column Builders

For even more flexibility, you can create columns dynamically based on runtime conditions:

from cyclopts import App, Parameter
from cyclopts.help import DefaultFormatter, ColumnSpec
from typing import Annotated

def dynamic_columns(console, options, entries):
    """Build columns based on console width and entries."""
    columns = []

    # Only show required indicator if there are required params
    if any(e.required for e in entries):
        columns.append(ColumnSpec(
            renderer=lambda e: "*" if e.required else "",
            width=2,
            style="red",
        ))

    # Adjust name column width based on console size
    max_width = min(40, int(console.width * 0.3))
    columns.append(ColumnSpec(
        renderer=lambda e: " ".join(e.names + e.shorts),
        header="Option",
        max_width=max_width,
        style="cyan",
    ))

    # Always include description
    columns.append(ColumnSpec(
        renderer="description",
        header="Description",
        overflow="fold",
    ))

    return tuple(columns)

app = App(
    help_formatter=DefaultFormatter(
        column_specs=dynamic_columns
    )
)

@app.default
def main(
    input_file: str,
    output_file: str,
    verbose: bool = False,
):
    """Process files with dynamic columns."""
    print(f"Processing {input_file} -> {output_file}")

if __name__ == "__main__":
    app()

Output (adjusts based on terminal width):

Creating Custom Formatters

For complete control, you can implement your own formatter by following the HelpFormatter <#cyclopts.help.protocols.HelpFormatter> protocol. The formatter methods receive the console and options first, followed by the content to render:

from cyclopts import App
from cyclopts.help import HelpPanel
from rich.console import Console, ConsoleOptions
from rich.table import Table
from rich.panel import Panel

class MyCustomFormatter:
    """A custom formatter with unique styling."""

    def __call__(self, console: Console, options: ConsoleOptions, panel: HelpPanel) -> None:
        """Render a help panel with custom styling."""
        if not panel.entries:
            return

        # Create a custom table
        table = Table(show_header=True, header_style="bold magenta")
        table.add_column("Option", style="cyan", no_wrap=True)
        table.add_column("Description", style="white")

        for entry in panel.entries:
            name = " ".join(entry.names + entry.shorts)
            # Extract plain text from description (handles InlineText, etc)
            desc = ""
            if entry.description:
                if hasattr(entry.description, 'plain'):
                    desc = entry.description.plain
                elif hasattr(entry.description, '__rich_console__'):
                    # Render to plain text without styles
                    with console.capture() as capture:
                        console.print(entry.description, end="")
                    desc = capture.get()
                else:
                    desc = str(entry.description)
            table.add_row(name, desc)

        # Wrap in a custom panel
        panel_title = panel.title or "Options"
        styled_panel = Panel(
            table,
            title=f"[bold blue]{panel_title}[/bold blue]",
            border_style="blue",
        )

        console.print(styled_panel)

    def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
        """Render the usage line."""
        if usage:
            console.print(f"[bold green]Usage:[/bold green] {usage}")

    def render_description(self, console: Console, options: ConsoleOptions, description) -> None:
        """Render the description."""
        if description:
            console.print(f"\n[italic]{description}[/italic]\n")

# Use the custom formatter
app = App(help_formatter=MyCustomFormatter())

@app.default
def main(input_file: str, output_file: str, verbose: bool = False):
    """Process files with custom formatter."""
    print(f"Processing {input_file} -> {output_file}")

if __name__ == "__main__":
    app()

Output:

Reference

For complete API documentation of help formatting components, see:

  • cyclopts.help.DefaultFormatter <#cyclopts.help.DefaultFormatter> - Rich-based formatter with full customization
  • cyclopts.help.PlainFormatter <#cyclopts.help.PlainFormatter> - Plain text formatter for accessibility
  • cyclopts.help.PanelSpec <#cyclopts.help.PanelSpec> - Panel appearance specification
  • cyclopts.help.TableSpec <#cyclopts.help.TableSpec> - Table styling specification
  • cyclopts.help.ColumnSpec <#cyclopts.help.ColumnSpec> - Column definition and rendering
  • cyclopts.help.protocols.HelpFormatter <#cyclopts.help.protocols.HelpFormatter> - Protocol for custom formatters

See also:

User Classes

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

Basic Example

As an example, let's consider using the builtin dataclass <https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass> to make a CLI that manages a movie collection.

from cyclopts import App
from dataclasses import dataclass

app = App(name="movie-maintainer")

@dataclass
class Movie:
   title: str
   year: int

@app.command
def add(movie: Movie):
   print(f"Adding movie: {movie}")

app()
$ movie-maintainer add --help
Usage: movie-maintainer add [ARGS] [OPTIONS]

╭─ Parameters ────────────────────────────────────────────────╮
│ *  MOVIE.TITLE              [required]                      │
│      --movie.title                                          │
│ *  MOVIE.YEAR --movie.year  [required]                      │
╰─────────────────────────────────────────────────────────────╯

$ movie-maintainer add 'Mad Max: Fury Road' 2015
Adding movie: Movie(title='Mad Max: Fury Road', year=2015)

$ movie-maintainer add --movie.title 'Furiosa: A Mad Max Saga' --movie.year 2024
Adding movie: Movie(title='Furiosa: A Mad Max Saga', year=2024)

In most circumstances, Cyclopts will also parse a json-string for a dataclass-like parameter:

$ movie-maintainer add --movie='{"title": "Mad Max: Fury Road", "year": 2024}'
Adding movie: Movie(title='Mad Max: Fury Road', year=2024)

JSON Dict Parsing

JSON dict parsing will be performed when:

  1. The parameter is specified as a keyword option; e.g. --movie.
  2. The referenced parameter type has various sub-arguments (is dataclass-like).
  3. The referenced parameter is not union'd with a str.
  4. The first character is a {.

This behavior can be configured via Parameter.json_dict <#cyclopts.Parameter.json_dict>.

from cyclopts import App
from dataclasses import dataclass

app = App(name="movie-manager")

@dataclass
class Movie:
   title: str
   year: int
   rating: float = 8.0

@app.command
def add(movie: Movie):
   print(f"Adding: {movie}")

app()
$ movie-manager add --movie '{"title": "Mad Max: Fury Road", "year": 2015, "rating": 8.1}'
Adding: Movie(title='Mad Max: Fury Road', year=2015, rating=8.1)

$ movie-manager add --movie '{"title": "Furiosa", "year": 2024}'
Adding: Movie(title='Furiosa', year=2024, rating=8.0)

Note that JSON parsing only works when using the keyword option format (--movie). The traditional positional argument format still works with individual fields:

$ movie-manager add --movie.title "Dune" --movie.year 2021 --movie.rating 8.5
Adding: Movie(title='Dune', year=2021, rating=8.5)

JSON List Parsing

Cyclopts also supports JSON parsing for lists of dataclasses. This allows you to pass multiple structured objects via JSON:

from cyclopts import App
from dataclasses import dataclass

app = App(name="movie-collection")

@dataclass
class Movie:
   title: str
   year: int

@app.command
def add_batch(movies: list[Movie]):
   for movie in movies:
       print(f"Adding: {movie}")

app()

You can provide the list in several ways:

  1. JSON Array - Multiple objects in a single argument:

    $ movie-collection add-batch --movies '[{"title": "Mad Max", "year": 2015}, {"title": "Furiosa", "year": 2024}]'
    Adding: Movie(title='Mad Max', year=2015)
    Adding: Movie(title='Furiosa', year=2024)
  2. Individual JSON - Each object as a separate argument:

    $ movie-collection add-batch --movies '{"title": "Mad Max", "year": 2015}' --movies '{"title": "Furiosa", "year": 2024}'
    Adding: Movie(title='Mad Max', year=2015)
    Adding: Movie(title='Furiosa', year=2024)
  3. Mixed - Combining arrays and individual objects:

    $ movie-collection add-batch --movies '{"title": "Mad Max", "year": 2015}' --movies '[{"title": "Furiosa", "year": 2024}, {"title": "Dune", "year": 2021}]'
    Adding: Movie(title='Mad Max', year=2015)
    Adding: Movie(title='Furiosa', year=2024)
    Adding: Movie(title='Dune', year=2021)

JSON list parsing is automatically enabled for list types containing dataclasses. The same rules apply as for dict parsing:

  • The element type cannot be union'd with str
  • JSON objects must start with { or be arrays starting with [

This behavior can be configured via Parameter.json_list <#cyclopts.Parameter.json_list>.

Namespace Flattening

It is likely that the actual movie class/object is not important to the CLI user, and the parameter names like --movie.title are unnecessarily verbose. We can remove movie from the name by giving the Movie type annotation the special name "*".

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

app = App(name="movie-maintainer")

@dataclass
class Movie:
   title: str
   year: int

@app.command
def add(movie: Annotated[Movie, Parameter(name="*")]):
   print(f"Adding movie: {movie}")

app()
$ movie-maintainer add --help
Usage: movie-maintainer add [ARGS] [OPTIONS]

╭─ Parameters ────────────────────────────────────────────────╮
│ *  TITLE --title  [required]                                │
│ *  YEAR --year    [required]                                │
╰─────────────────────────────────────────────────────────────╯

An alternative way of supplying the Parameter <#cyclopts.Parameter> configuration is via a decorator. This way can be cleaner and terser in many scenarios. The Parameter <#cyclopts.Parameter> configuration will also be inherited by subclasses.

from cyclopts import App, Parameter
from dataclasses import dataclass

app = App(name="movie-maintainer")

@Parameter(name="*")
@dataclass
class Movie:
   title: str
   year: int

@app.command
def add(movie: Movie):
   print(f"Adding movie: {movie}")

app()

Sharing Parameters

A flattened dataclass provides a natural way of easily sharing a set of parameters between commands.

from cyclopts import App, Parameter
from dataclasses import dataclass

app = App(name="movie-maintainer")

@Parameter(name="*")
@dataclass
class Config:
   user: str
   server: str = "media.sqlite"

@dataclass
class Movie:
   title: str
   year: int

@app.command
def add(movie: Movie, *, config: Config):
   print(f"Config: {config}")
   print(f"Adding movie: {movie}")

@app.command
def remove(movie: Movie, *, config: Config):
   print(f"Config: {config}")
   print(f"Removing movie: {movie}")

app()
$ movie-maintainer remove --help
Usage: movie-maintainer remove [ARGS] [OPTIONS]

╭─ Parameters ────────────────────────────────────────────────╮
│ *  MOVIE.TITLE              [required]                      │
│      --movie.title                                          │
│ *  MOVIE.YEAR --movie.year  [required]                      │
│ *  --user                   [required]                      │
│    --server                 [default: media.sqlite]         │
╰─────────────────────────────────────────────────────────────╯

$ movie-maintainer remove 'Mad Max: Fury Road' 2015 --user Guido
Config: Config(user='Guido', server='media.sqlite')
Removing movie: Movie(title='Mad Max: Fury Road', year=2015)

Config File

Having the user specify --user every single call is a bit cumbersome, especially if they're always going to provide the same value. We can have Cyclopts fallback to a toml configuration file <#config-files>.

Consider the following toml data saved to config.toml:

# config.toml
user = "Guido"

We can update our app to fill in missing CLI parameters from this file:

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

app = App(
   name="movie-maintainer",
   config=config.Toml("config.toml", use_commands_as_keys=False),
)

@Parameter(name="*")
@dataclass
class Config:
   user: str
   server: str = "media.sqlite"

@dataclass
class Movie:
   title: str
   year: int

@app.command
def add(movie: Movie, *, config: Config):
   print(f"Config: {config}")
   print(f"Adding movie: {movie}")

app()
$ movie-maintainer add 'Mad Max: Fury Road' 2015
Config: Config(user='Guido', server='media.sqlite')
Adding movie: Movie(title='Mad Max: Fury Road', year=2015)

Args & Kwargs

In python, a function can consume a variable number of positional and keyword arguments:

def foo(normal_required_variable, *args, **kwargs):
    pass

There is nothing special about the names args and kwargs; the functionality is derived from the leading * and **. args and kwargs are the defacto standard names for these variables. In this document, we'll usually just refer to them as *args and **kwargs.

Cyclopts commands may consume a variable number of positional and keyword arguments. The priority ruleset is as follows:

  1. --keyword CLI arguments first get matched to normal variable parameters.
  2. Unmatched keywords get consumed by **kwargs, if specified.
  3. All remaining tokens get consumed by *args, if specified. A prevalant use-case is in a typical Meta App <#meta-app>.

Args (Variable Positional)

A variable number of positional arguments consume all remaining positional arguments from the command-line. Individual elements are converted to the annotated type.

from cyclopts import App

app = App()

@app.command
def foo(name: str, *favorite_numbers: int):
    print(f"{name}'s favorite numbers are: {favorite_numbers}")

app()
$ my-script foo Brian
Brian's favorite numbers are: ()

$ my-script foo Brian 777
Brian's favorite numbers are: (777,)

$ my-script foo Brian 777 2
Brian's favorite numbers are: (777, 2)

Kwargs (Variable Keywords)

A variable number of keyword arguments consume all remaining CLI tokens starting with --. Individual values are converted to the annotated type.

from cyclopts import App

app = App()

@app.command
def add(**country_to_capitols):
    for country, capitol in country_to_capitols.items():
        print(f"Adding {country} with capitol {capitol}.")

app()
$ my-script add --united-states="Washington, D.C." --canada=Ottawa
Adding united-states with capitol Washington, D.C..
Adding canada with capitol Ottawa.

Config Files

For more complicated CLI applications, it is common to have an external user configuration file. For example, the popular python tools poetry, ruff, and pytest are all configurable from a pyproject.toml file. The App.config <#cyclopts.App.config> attribute accepts a callable <https://docs.python.org/3/glossary.html#term-callable> (or list of callables) that add (or remove) values to the parsed CLI tokens. The provided callable must have signature:

def config(app: "App", commands: Tuple[str, ...], arguments: ArgumentCollection):
    """Modifies the argument collection inplace with some injected values.

    Parameters
    ----------
    app: App
       The current command app being executed.
    commands: Tuple[str, ...]
       The CLI strings that led to the current command function.
    arguments: ArgumentCollection
       Complete ArgumentCollection for the app.
       Modify this collection inplace to influence values provided to the function.
    """
    ...

The provided config does not have to be a function; all the Cyclopts builtin configs are classes that implement the __call__ method. The Cyclopts builtins offer good standard functionality for common configuration files like yaml or toml.

TOML Example

In this example, we create a small CLI tool that counts the number of times a given character occurs in a file.

# character-counter.py
import cyclopts
from cyclopts import App
from pathlib import Path

app = App(
    name="character-counter",
    config=cyclopts.config.Toml(
        "pyproject.toml",  # Name of the TOML File
        root_keys=["tool", "character-counter"],  # The project's namespace in the TOML.
        # If "pyproject.toml" is not found in the current directory,
        # then iteratively search parenting directories until found.
        search_parents=True,
    ),
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

if __name__ == "__main__":
    app()

Running this code without a pyproject.toml present:

$ python character-counter.py count README.md
70
$ python character-counter.py count README.md --character=t
380

We can have the new default character be t by adding the following to pyproject.toml:

[tool.character-counter.count]
character = "t"

Rerunning the app without a specified --character will result in using the toml-provided value:

$ python character-counter.py count README.md
380

User-Specified Config File

Extending the above TOML Example, what if we want to allow the user to specify the toml configuration file? This can be accomplished via a Meta App <#meta-app>.

# character-counter.py
from pathlib import Path
from typing import Annotated

import cyclopts
from cyclopts import App, Parameter

app = App(name="character-counter")

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

@app.meta.default
def meta(
    *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
    config: Path = Path("pyproject.toml"),
):
    app.config = cyclopts.config.Toml(
        config,
        root_keys=["tool", "character-counter"],
        search_parents=True,
    )

    app(tokens)

if __name__ == "__main__":
    app.meta()

Environment Variable Example

To automatically derive and read appropriate environment variables, use the cyclopts.config.Env <#cyclopts.config.Env> class. Continuing the above TOML example:

# character-counter.py
import cyclopts
from pathlib import Path

app = cyclopts.App(
    name="character-counter",
    config=cyclopts.config.Env(
        "CHAR_COUNTER_",  # Every environment variable will begin with this.
    ),
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

app()

Env <#cyclopts.config.Env> assembles the environment variable name by joining the following components (in-order):

  1. The provided prefix. In this case, it is "CHAR_COUNTER_".
  2. The command and subcommand(s) that lead up to the function being executed.
  3. The parameter's CLI name, with the leading -- stripped, and hyphens - replaced with underscores _.

Running this code without a specified --character results in counting the default - character.

$ python character-counter.py count README.md
70

By exporting a value to CHAR_COUNTER_COUNT_CHARACTER, that value will now be used as the default:

$ export CHAR_COUNTER_COUNT_CHARACTER=t
$ python character-counter.py count README.md
380
$ python character-counter.py count README.md --character=q
3

In-Memory Dict

For configurations that come from sources other than files, use cyclopts.config.Dict <#cyclopts.config.Dict>.

# character-counter.py
import json
import cyclopts
from pathlib import Path

def fetch_config():
    """Simulate fetching configuration from an API."""
    return {"count": {"character": "e"}}

config_data = fetch_config_from_api()

app = cyclopts.App(
    name="character-counter",
    config=cyclopts.config.Dict(
        fetch_config,
        # Optional: provide custom source identifier for better error messages
        source="api",
    ),
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

if __name__ == "__main__":
    app()

Combining Multiple Config Sources

You can combine multiple config sources in a single application by passing a sequence to App.config <#cyclopts.App.config>. Each configuration is applied sequentially.

In the following example, we combine a TOML file and environment variables, allowing environment variables to override TOML settings:

# character-counter.py
import cyclopts
from pathlib import Path

app = cyclopts.App(
    name="character-counter",
    config=[
        # Since Env comes before Toml, it has priority.
        cyclopts.config.Env("CHAR_COUNTER_"),
        cyclopts.config.Toml(
            "pyproject.toml",
            root_keys=["tool", "character-counter"],
            search_parents=True,
        ),
    ],
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

if __name__ == "__main__":
    app()

With this setup, the configuration is resolved in the following order:

  1. CLI arguments (if provided) override everything else
  2. Environment variables (prefixed with CHAR_COUNTER_) can override TOML values
  3. TOML file (pyproject.toml) provides the base configuration
  4. Python default the default value - in the python code.

For example, with pyproject.toml containing:

[tool.character-counter.count]
character = "t"

You can override it via environment variable:

$ CHAR_COUNTER_COUNT_CHARACTER=a python character-counter.py count README.md

Or via CLI argument:

$ python character-counter.py count README.md --character=x

Sphinx Integration

Cyclopts provides builtin Sphinx <https://www.sphinx-doc.org/> support.

Table of Contents

  • Quick Start
  • Directive Usage

    • Basic Syntax
    • Module Path Formats
  • Directive Options

    • :heading-level: - Heading Level
    • :max-heading-level: - Maximum Heading Level
    • :no-recursive: - Exclude Subcommands
    • :include-hidden: - Show Hidden Commands
    • :flatten-commands: - Generate Flat Command Hierarchy
    • :code-block-title: - Render Titles as Inline Code
    • :commands: - Filter Specific Commands
    • :exclude-commands: - Exclude Specific Commands
    • :skip-preamble: - Skip Description and Usage
    • Automatic Reference Labels
  • Complete Example

    • CLI Application (myapp/cli.py):
    • Sphinx Configuration (docs/conf.py):
    • Documentation File (docs/cli.rst):
  • Advanced Usage

    • Using Distinct Command Headings
    • Selective Command Documentation
  • Output Formats
  • See Also

Quick Start

  1. Add the extension to your Sphinx configuration (docs/conf.py):

    extensions = [
        'cyclopts.sphinx_ext',  # Add this line
        # ... your other extensions
    ]
  2. Use the directive in your RST files:

    .. cyclopts:: mypackage.cli:app

Directive Usage

Basic Syntax

The cyclopts directive accepts a module path to your Cyclopts App object:

.. cyclopts:: mypackage.cli:app

Module Path Formats

The directive accepts two module path formats:

  1. Explicit format (module.path:app_name):

    .. cyclopts:: mypackage.cli:app
    .. cyclopts:: myapp.commands:main_app
    .. cyclopts:: src.cli:cli

    This explicitly specifies which App object to document.

  2. Automatic discovery (module.path):

    .. cyclopts:: mypackage.cli
    .. cyclopts:: myapp.main

    The extension will search the module for an App instance, looking for common names like app, cli, or main.

Directive Options

The directive supports several options to customize the generated documentation:

heading-level: - Heading Level

Set the starting heading level for the generated documentation (1-6, default: 2):

.. cyclopts:: mypackage.cli:app
   :heading-level: 3

This is useful when you need to adjust the heading hierarchy. The default of 2 works well for most cases where the directive is placed under a page title.

max-heading-level: - Maximum Heading Level

Set the maximum heading level to use (1-6, default: 6):

.. cyclopts:: mypackage.cli:app
   :max-heading-level: 4

Headings deeper than this level will be capped at this value. This is useful for deeply nested command hierarchies where you want to prevent headings from becoming too small.

no-recursive: - Exclude Subcommands

Disable recursive documentation of subcommands (by default, subcommands are included):

.. cyclopts:: mypackage.cli:app
   :no-recursive:

When this flag is present, only the top-level commands are documented.

include-hidden: - Show Hidden Commands

Include commands marked with show=False in the documentation:

.. cyclopts:: mypackage.cli:app
   :include-hidden: true

By default, hidden commands are not included in the generated documentation.

flatten-commands: - Generate Flat Command Hierarchy

Generate all commands at the same heading level instead of nested hierarchy:

.. cyclopts:: mypackage.cli:app
   :flatten-commands:

This creates distinct, equally-weighted headings for each command and subcommand, making them easier to reference and navigate in the documentation. Without this option, subcommands are nested with incrementing heading levels.

code-block-title: - Render Titles as Inline Code

Render command titles with inline code formatting instead of plain text:

.. cyclopts:: mypackage.cli:app
   :code-block-title:

When this flag is present, command titles are rendered with monospace formatting, which can be useful for certain documentation themes or to make command names stand out visually.

commands: - Filter Specific Commands

Document only specific commands from your CLI application:

.. cyclopts:: mypackage.cli:app
   :commands: init, build, deploy

This will only document the specified commands. You can also use nested command paths with dot notation:

.. cyclopts:: mypackage.cli:app
   :commands: db.migrate, db.backup, api
  • db.migrate - Documents only the migrate subcommand under db
  • db.backup - Documents only the backup subcommand under db
  • api - Documents the api command and all its subcommands

You can use either underscore or dash notation in command names - they will be normalized automatically.

exclude-commands: - Exclude Specific Commands

Exclude specific commands from the documentation:

.. cyclopts:: mypackage.cli:app
   :exclude-commands: debug, internal-test

This is useful for hiding internal or debug commands from user-facing documentation. Like :commands:, this also supports nested command paths with dot notation.

skip-preamble: - Skip Description and Usage

Skip the description and usage sections for the target command when filtering to a single command:

.. cyclopts:: mypackage.cli:app
   :commands: deploy
   :skip-preamble:

When you filter to a single command using :commands: and provide your own section heading in the RST, you may not want the directive to generate the command's description and usage block. Adding :skip-preamble: suppresses these sections while still generating the command's parameters and subcommands.

This is useful when you want to write your own introduction for a command section:

Deployment
==========

Deploy your application to production with these commands.

.. cyclopts:: mypackage.cli:app
   :commands: deploy
   :skip-preamble:

Without :skip-preamble:, the output would include both your introduction and the command's docstring description, which can be redundant.

Automatic Reference Labels

The Sphinx directive automatically generates RST reference labels for all commands, enabling cross-referencing throughout your documentation. The anchor format is cyclopts-{app-name}-{command-path}, which prevents naming conflicts when documenting multiple CLIs.

For example: - Root application: cyclopts-myapp - Subcommand: cyclopts-myapp-deploy - Nested subcommand: cyclopts-myapp-deploy-production

You can reference these commands elsewhere in your documentation using :ref:`cyclopts-myapp-deploy`.

Complete Example

Here's a complete example showing a CLI application and its Sphinx documentation:

CLI Application (myapp/cli.py)

from pathlib import Path
from typing import Optional
from cyclopts import App

app = App(
    name="myapp",
    help="My awesome CLI application",
    version="1.0.0"
)

@app.command
def init(path: Path = Path("."), template: str = "default"):
    """Initialize a new project.

    Parameters
    ----------
    path : Path
        Directory where the project will be created
    template : str
        Project template to use
    """
    print(f"Initializing project at {path}")

@app.command
def build(source: Path, output: Optional[Path] = None, *, minify: bool = False):
    """Build the project.

    Parameters
    ----------
    source : Path
        Source directory
    output : Path, optional
        Output directory (defaults to source/dist)
    minify : bool
        Minify the output files
    """
    output = output or source / "dist"
    print(f"Building from {source} to {output}")

if __name__ == "__main__":
    app()

Sphinx Configuration (docs/conf.py)

import sys
from pathlib import Path

# Add your package to the path
sys.path.insert(0, str(Path(__file__).parent.parent))

# Extensions
extensions = [
    'cyclopts.sphinx_ext',
    'sphinx.ext.autodoc',  # For API docs
    'sphinx.ext.napoleon',  # For NumPy-style docstrings
]

# Project info
project = 'MyApp'
author = 'Your Name'
version = '1.0.0'

# HTML theme
html_theme = 'sphinx_rtd_theme'

Documentation File (docs/cli.rst)

CLI Reference
=============

This section documents all available CLI commands.

.. cyclopts:: myapp.cli:app

The above directive will automatically generate documentation for all
commands, including their parameters, types, defaults, and help text.

Advanced Usage

Using Distinct Command Headings

When you want each command to have its own distinct heading for better navigation and referencing:

CLI Command Reference
=====================

.. cyclopts:: myapp.cli:app
   :flatten-commands:
   :code-block-title:

This generates:

- All commands at the same heading level (not nested)
- Command titles with monospace formatting
- Automatic reference labels for cross-linking

You can then reference specific commands:

See :ref:`cyclopts-myapp-deploy` for deployment options.
The :ref:`cyclopts-myapp-init` command sets up your project.

Selective Command Documentation

Split your CLI documentation across multiple sections or pages:

Database Commands
=================

The following commands manage database operations:

.. cyclopts:: myapp.cli:app
   :commands: db

API Management
==============

Commands for controlling the API server:

.. cyclopts:: myapp.cli:app
   :commands: api

Development Tools
=================

Utilities for development (excluding internal debug commands):

.. cyclopts:: myapp.cli:app
   :commands: dev
   :exclude-commands: dev.debug, dev.internal

This approach allows you to:

  • Organize large CLI applications into logical sections
  • Document different command groups on separate pages
  • Exclude internal or debug commands from user documentation
  • Create targeted documentation for different audiences

Output Formats

While the Sphinx directive uses RST internally, you can also generate documentation programmatically in multiple formats:

from myapp.cli import app

# Generate reStructuredText
rst_docs = app.generate_docs(output_format="rst")

# Generate Markdown
md_docs = app.generate_docs(output_format="markdown")

# Generate HTML
html_docs = app.generate_docs(output_format="html")

This is useful for generating documentation outside of Sphinx, such as for GitHub README files or other documentation systems.

See Also

  • Help <> - Customizing help output
  • Commands <> - Creating commands and subcommands
  • Parameters <> - Parameter types and validation
  • Sphinx Documentation <https://www.sphinx-doc.org/> - Official Sphinx documentation

Mkdocs Integration

Cyclopts provides builtin MkDocs <https://www.mkdocs.org/> support.

Warning:

The MkDocs plugin is experimental and may have breaking changes in future releases. If you encounter any issues or have feedback, please report them on GitHub <https://github.com/BrianPugh/cyclopts/issues>.

Table of Contents

  • Quick Start
  • Directive Usage

    • Basic Syntax
    • Module Path Formats
  • Directive Options

    • module - Module Path (Required)
    • heading_level - Heading Level
    • max_heading_level - Maximum Heading Level
    • recursive - Include Subcommands
    • include_hidden - Show Hidden Commands
    • flatten_commands - Generate Flat Command Hierarchy
    • generate_toc - Generate Table of Contents
    • code_block_title - Render Titles as Inline Code
    • commands - Filter Specific Commands
    • exclude_commands - Exclude Specific Commands
    • skip_preamble - Skip Description and Usage
  • Complete Example

    • CLI Application (myapp/cli.py):
    • MkDocs Configuration (mkdocs.yml):
    • Documentation File (docs/cli-reference.md):
  • Advanced Usage

    • Using Flat Command Structure
    • Selective Command Documentation
  • See Also

Quick Start

  1. Install Cyclopts with MkDocs support:

    pip install cyclopts[mkdocs]
  2. Add the plugin to your MkDocs configuration (mkdocs.yml):

    plugins:
      - cyclopts
  3. Use the directive in your Markdown files:

    ::: cyclopts
        module: mypackage.cli:app

Directive Usage

Basic Syntax

The ::: cyclopts directive uses YAML format and accepts a module path to your Cyclopts App object:

::: cyclopts
    module: mypackage.cli:app

Module Path Formats

The directive accepts two module path formats:

  1. Explicit format (module.path:app_name):

    ::: cyclopts
        module: mypackage.cli:app
    
    ::: cyclopts
        module: myapp.commands:main_app
    
    ::: cyclopts
        module: src.cli:cli

    This explicitly specifies which App object to document.

  2. Automatic discovery (module.path):

    ::: cyclopts
        module: mypackage.cli
    
    ::: cyclopts
        module: myapp.main

    The plugin will search the module for an App instance, looking for common names like app, cli, or main.

Directive Options

The directive supports several options to customize the generated documentation. All options use standard YAML syntax:

module - Module Path (Required)

The module path to your Cyclopts App instance:

::: cyclopts
    module: mypackage.cli:app

This is the only required option.

heading_level - Heading Level

Set the starting heading level for the generated documentation (1-6, default: 2):

::: cyclopts
    module: mypackage.cli:app
    heading_level: 3

This is useful when you need to adjust the heading hierarchy. The default of 2 works well for most cases where the directive is placed under a page title.

max_heading_level - Maximum Heading Level

Set the maximum heading level to use (1-6, default: 6):

::: cyclopts
    module: mypackage.cli:app
    max_heading_level: 4

Headings deeper than this level will be capped at this value. This is useful for deeply nested command hierarchies where you want to prevent headings from becoming too small.

recursive - Include Subcommands

Control whether to document subcommands recursively (default: true):

::: cyclopts
    module: mypackage.cli:app
    recursive: false

Set to false to only document the top-level commands.

include_hidden - Show Hidden Commands

Include commands marked with show=False in the documentation:

::: cyclopts
    module: mypackage.cli:app
    include_hidden: true

By default, hidden commands are not included in the generated documentation.

flatten_commands - Generate Flat Command Hierarchy

Generate all commands at the same heading level instead of nested hierarchy:

::: cyclopts
    module: mypackage.cli:app
    flatten_commands: true

This creates distinct, equally-weighted headings for each command and subcommand, making them easier to reference and navigate in the documentation. Without this option, subcommands are nested with incrementing heading levels.

generate_toc - Generate Table of Contents

Control whether to generate a table of contents for multi-command apps (default: true):

::: cyclopts
    module: mypackage.cli:app
    generate_toc: false

This is useful when you want to suppress the automatic table of contents, especially when using multiple directives on the same page or when you have your own navigation structure.

code_block_title - Render Titles as Inline Code

Render command titles with inline code formatting (backticks) instead of plain text:

::: cyclopts
    module: mypackage.cli:app
    code_block_title: true

When enabled, command titles are rendered as #### `command-name` instead of #### command-name. This makes command names appear with monospace formatting, which can be useful for certain documentation themes or to make command names stand out visually.

commands - Filter Specific Commands

Document only specific commands from your CLI application:

::: cyclopts
    module: mypackage.cli:app
    commands:
      - init
      - build
      - deploy

This will only document the specified commands. You can also use nested command paths with dot notation:

::: cyclopts
    module: mypackage.cli:app
    commands:
      - db.migrate
      - db.backup
      - api

Or use inline YAML list syntax:

::: cyclopts
    module: mypackage.cli:app
    commands: [db.migrate, db.backup, api]
  • db.migrate - Documents only the migrate subcommand under db
  • db.backup - Documents only the backup subcommand under db
  • api - Documents the api command and all its subcommands

You can use either underscore or dash notation in command names - they will be normalized automatically.

exclude_commands - Exclude Specific Commands

Exclude specific commands from the documentation:

::: cyclopts
    module: mypackage.cli:app
    exclude_commands:
      - debug
      - internal-test

This is useful for hiding internal or debug commands from user-facing documentation. Like commands, this also supports nested command paths with dot notation and inline YAML list syntax.

skip_preamble - Skip Description and Usage

Skip the description and usage sections for the target command when filtering to a single command:

::: cyclopts
    module: mypackage.cli:app
    commands: [deploy]
    skip_preamble: true

When you filter to a single command using commands and provide your own section heading in the Markdown, you may not want the plugin to generate the command's description and usage block. Setting skip_preamble: true suppresses these sections while still generating the command's parameters and subcommands.

This is useful when you want to write your own introduction for a command section:

## Deployment

Deploy your application to production with these commands.

::: cyclopts
    module: mypackage.cli:app
    commands: [deploy]
    skip_preamble: true

Without skip_preamble, the output would include both your introduction and the command's docstring description, which can be redundant.

Complete Example

Here's a complete example showing a CLI application and its MkDocs documentation:

CLI Application (myapp/cli.py)

from pathlib import Path
from typing import Optional
from cyclopts import App

app = App(
    name="myapp",
    help="My awesome CLI application",
    version="1.0.0"
)

@app.command
def init(path: Path = Path("."), template: str = "default"):
    """Initialize a new project.

    Parameters
    ----------
    path : Path
        Directory where the project will be created
    template : str
        Project template to use
    """
    print(f"Initializing project at {path}")

@app.command
def build(source: Path, output: Optional[Path] = None, *, minify: bool = False):
    """Build the project.

    Parameters
    ----------
    source : Path
        Source directory
    output : Path, optional
        Output directory (defaults to source/dist)
    minify : bool
        Minify the output files
    """
    output = output or source / "dist"
    print(f"Building from {source} to {output}")

if __name__ == "__main__":
    app()

MkDocs Configuration (mkdocs.yml)

site_name: MyApp Documentation
site_description: Documentation for MyApp CLI

theme:
  name: readthedocs

plugins:
  - search
  - cyclopts

nav:
  - Home: index.md
  - CLI Reference: cli-reference.md
  - User Guide: guide.md

markdown_extensions:
  - admonition
  - codehilite
  - toc:
      permalink: true

Documentation File (docs/cli-reference.md)

# CLI Reference

This section documents all available CLI commands.

::: cyclopts
    module: myapp.cli:app
    heading_level: 2
    recursive: true

The above directive will automatically generate documentation for all
commands, including their parameters, types, defaults, and help text.

Advanced Usage

Using Flat Command Structure

When you want each command to have its own distinct heading for better navigation:

# CLI Command Reference

::: cyclopts
    module: myapp.cli:app
    flatten_commands: true

This generates all commands at the same heading level (not nested),
making it easier to navigate and reference specific commands.

Selective Command Documentation

Split your CLI documentation across multiple sections or pages:

## Database Commands

The following commands manage database operations:

::: cyclopts
    module: myapp.cli:app
    commands: [db]
    recursive: true

## API Management

Commands for controlling the API server:

::: cyclopts
    module: myapp.cli:app
    commands: [api]
    recursive: true

## Development Tools

Utilities for development (excluding internal debug commands):

::: cyclopts
    module: myapp.cli:app
    commands: [dev]
    exclude_commands: [dev.debug, dev.internal]
    recursive: true

This approach allows you to:

  • Organize large CLI applications into logical sections
  • Document different command groups on separate pages
  • Exclude internal or debug commands from user documentation
  • Create targeted documentation for different audiences

See Also

  • Sphinx Integration <> - Sphinx integration (similar functionality)
  • Help <> - Customizing help output
  • Commands <> - Creating commands and subcommands
  • Parameters <> - Parameter types and validation
  • MkDocs Documentation <https://www.mkdocs.org/> - Official MkDocs documentation

Packaging

Packaging is bundling up your python library so that it can be easily pip install by others.

Typically this involves:

  1. Bundling the code into a Built Distribution (wheel) and/or Source Distribution (sdist).
  2. Uploading (publishing) the distribution(s) to python package repository, like PyPI.

This section is a brief bootcamp on package configuration for a CLI application. This is not intended to be a complete tutorial on python packaging and publishing. In this tutorial, replace all instances of mypackage with your own project name.

__main__.py

In python, if you have a module mypackage/__main__.py, it will be executed with the bash command python -m mypackage.

A pretty bare-bones Cyclopts mypackage/__main__.py will look like:

# mypackage/__main__.py

import cyclopts

app = cyclopts.App()

@app.command
def foo(name: str):
    print(f"Hello {name}!")

if __name__ == "__main__":
    app()
$ python -m mypackage World
Hello World!

Entrypoints

If you want your application to be callable like a standard bash executable (i.e. my-package instead of python -m mypackage), we'll need to add an entrypoint <https://setuptools.pypa.io/en/latest/userguide/entry_point.html#entry-points>.

Modern Python projects typically use pyproject.toml for configuration. The standard way to define console scripts is:

# pyproject.toml
[project.scripts]
my-package = "mypackage.__main__:app"

This creates an executable named my-package that executes the callable app object (from the right of the colon) from the python module mypackage.__main__. Note that this configuration is independent of any special naming, like __main__ or app.

Legacy Configurations

For older projects, you may encounter these alternative formats:

setup.py:

# setup.py
from setuptools import setup

setup(
    # There should be a lot more fields populated here.
    entry_points={
        "console_scripts": [
            "my-package = mypackage.__main__:app",
        ]
    },
)

setup.cfg:

# setup.cfg
[options.entry_points]
console_scripts =
    my-package = mypackage.__main__:app

Poetry:

# pyproject.toml
[tool.poetry.scripts]
my-package = "mypackage.__main__:app"

The setuptools entrypoint <https://setuptools.pypa.io/en/latest/userguide/entry_point.html#entry-points> documentation goes into further detail.

Result Action

When using Cyclopts as a CLI application, command return values are automatically handled appropriately. By default, App <#cyclopts.App> uses "print_non_int_sys_exit" mode, which calls sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with the appropriate exit code:

This default behavior makes Cyclopts applications work consistently whether run directly as scripts or installed via console_scripts entry points <https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts>. The result_action <#cyclopts.App.result_action> can be customized if different behavior is needed:

App Calling & Return Values

In this section, we'll take a closer look at the App.__call__() <#cyclopts.App.__call__> method.

Input Command

Typically, a Cyclopts app looks something like:

from cyclopts import App

app = App()

@app.command
def foo(a: int, b: int, c: int):
    print(a + b + c)

app()
$ my-script 1 2 3
6

App.__call__() <#cyclopts.App.__call__> takes in an optional input that it parses into an action. If not specified, Cyclopts defaults to sys.argv[1:] <https://docs.python.org/3/library/sys.html#sys.argv>, i.e. the list of command line arguments. An explicit string or list of strings can instead be passed in.

app("foo 1 2 3")
# 6
app(["foo", "1", "2", "3"])
# 6

If a string is passed in, it will be internally converted into a list using shlex.split <https://docs.python.org/3/library/shlex.html#shlex.split>.

Return Value

The app invocation processes the command's return value based on App.result_action <#cyclopts.App.result_action>. By default, Cyclopts calls sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with an appropriate exit code:

from cyclopts import App

app = App()  # Default result_action="print_non_int_sys_exit"

@app.command
def success():
    return 0  # Exit code for success

@app.command
def greet(name: str) -> str:
    return f"Hello {name}!"  # Prints and exits with 0

if __name__ == "__main__":
    app()  # Will call sys.exit with the returned 0 error code (success).

Installed scripts <https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts> call sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with the returned value of the entry point. So the default Cyclopts App.result_action <#cyclopts.App.result_action> will have consistent behavior for standalone scripts and installed apps.

For embedding Cyclopts in other Python code or testing, use result_action="return_value" to get the raw command return value without calling sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit>:

from cyclopts import App

app = App(result_action="return_value")

@app.command
def foo(a: int, b: int, c: int):
    return a + b + c

return_value = app("foo 1 2 3")  # no longer exits!
print(f"The return value was: {return_value}.")
# The return value was: 6.

See Result Action <#result-action> for all available modes and detailed behavior.

Exception Handling and Exiting

For the most part, Cyclopts is hands-off when it comes to handling exceptions and exiting the application. However, by default, if there is a Cyclopts runtime error, like CoercionError <#cyclopts.CoercionError> or a ValidationError <#cyclopts.ValidationError>, then Cyclopts will perform a sys.exit(1) <https://docs.python.org/3/library/sys.html#sys.exit>. This is to avoid displaying the unformatted, uncaught exception to the CLI user.

These behaviors can be controlled via App <#cyclopts.App> attributes or method parameters:

These attributes are inherited by child apps and can be overridden by providing parameters to method calls.

Note:

Cyclopts separates normal output from error messages using two different consoles:

  • App.console <#cyclopts.App.console> - Used for normal output like help messages and version information (defaults to stdout)
  • App.error_console <#cyclopts.App.error_console> - Used for error messages like parsing errors and exceptions (defaults to stderr)

Setting at App Level:

# Configure error handling at the app level
app = App(
    exit_on_error=False,  # Don't exit on errors
    print_error=False,    # Don't print formatted errors
)

# Child apps inherit these settings
child_app = App(name="child")
app.command(child_app)

Method-Level Override:

app("this-is-not-a-registered-command")
print("this will not be printed since cyclopts exited above.")
# ╭─ Error ─────────────────────────────────────────────────────────────╮
# │ Unknown command "this-is-not-a-registered-command".                 │
# ╰─────────────────────────────────────────────────────────────────────╯

app("this-is-not-a-registered-command", exit_on_error=False, print_error=False)
# Traceback (most recent call last):
#   File "/cyclopts/scratch.py", line 9, in <module>
#     app("this-is-not-a-registered-command", exit_on_error=False, print_error=False)
#   File "/cyclopts/cyclopts/core.py", line 1102, in __call__
#     command, bound, _ = self.parse_args(
#   File "/cyclopts/cyclopts/core.py", line 1037, in parse_args
#     command, bound, unused_tokens, ignored, argument_collection = self._parse_known_args(
#   File "/cyclopts/cyclopts/core.py", line 966, in _parse_known_args
#     raise UnknownCommandError(unused_tokens=unused_tokens)
# cyclopts.exceptions.UnknownCommandError: Unknown command "this-is-not-a-registered-command".

try:
    app("this-is-not-a-registered-command", exit_on_error=False, print_error=False)
except CycloptsError:
    pass
print("Execution continues since we caught the exception.")

With exit_on_error=False, the UnknownCommandError is raised the same as a normal python exception.

Meta App

What if you want more control over the application launch process? Cyclopts provides the option of launching an app from an app; a meta app!

Meta Sub App

Typically, a Cyclopts application is launched by calling the App <#cyclopts.App> object:

from cyclopts import App

app = App()
# Register some commands here (not shown)
app()  # Run the app

To change how the primary app is run, you can use the meta-app feature of Cyclopts. The meta app is a special App <#cyclopts.App> that inherits configuration from its parent and has its help-page merged with the parent app's help.

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

app = App()
# Rename the meta's "Parameter" -> "Session Parameters".
# Set sort_key so it will be drawn higher up the help-page.
app.meta.group_parameters = Group("Session Parameters", sort_key=0)

@app.command
def foo(loops: int):
    for i in range(loops):
        print(f"Looping! {i}")

@app.meta.default
def my_app_launcher(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], user: str):
    print(f"Hello {user}")
    app(tokens)

app.meta()
$ my-script --user=Bob foo 3
Hello Bob
Looping! 0
Looping! 1
Looping! 2

The variable positional *tokens will aggregate all remaining tokens, including those starting with a hyphen (typically options). We can then pass them along to the primary app.

The meta app inherits many configuration values from its parent app and is additionally scanned when generating help screens. *tokens is annotated with show=False since we do not want this variable to show up in the help screen.

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

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

Meta Commands

If you want a command to circumvent my_app_launcher, add it as you would any other command to the meta app.

@app.meta.command
def info():
    print("CLI didn't have to provide --user to call this.")
$ my-script info
CLI didn't have to provide --user to call this.

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

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

Just like a standard application, the parsed command executes instead of default.

Custom Command Invocation

The core logic of App.__call__() <#cyclopts.App.__call__> method is the following:

def __call__(self, tokens=None, **kwargs):
    command, bound, ignored = self.parse_args(tokens, **kwargs)
    return command(*bound.args, **bound.kwargs)

Knowing this, we can easily customize how we actually invoke actions with Cyclopts. Let's imagine that we want to instantiate an object, User in our meta app, and pass it to subsequent commands that need it. This might be useful to share an expensive-to-create object amongst commands in a single session; see Command Chaining <#command-chaining>.

from cyclopts import App, Parameter
from typing import Annotated

app = App()

class User:
    def __init__(self, name):
        self.name = name

@app.command
def create(
    age: int,
    *,
    user_obj: Annotated[User, Parameter(parse=False)],
):
    print(f"Creating user {user_obj.name} with age {age}.")

@app.meta.default
def launcher(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], user: str):
    additional_kwargs = {}
    command, bound, ignored = app.parse_args(tokens)
    # "ignored" is a dict mapping python-variable-name to it's type annotation for parameters with "parse=False".
    if "user_obj" in ignored:
        # 'ignored["user_obj"]' is the class "User"
        additional_kwargs["user_obj"] = ignored["user_obj"](user)
    return command(*bound.args, **bound.kwargs, **additional_kwargs)

if __name__ == "__main__":
    app.meta()
$ my-script create --user Alice 30
Creating user Alice with age 30.

The parse=False configuration tells Cyclopts to not try and bind arguments to this parameter. Cyclopts will pass it along to ignored to make custom meta-app logic easier. The annotated parameter must be a keyword-only parameter.

Tip:

For app-wide control over which parameters are parsed, parse <#cyclopts.Parameter.parse> can also accept a regex pattern. This can be useful for automatically skipping all "private" parameters (e.g., those prefixed with _) with the regex pattern "^(?!_)". See Skipping Private Parameters <#skipping-private-parameters> for details.

Command Chaining

Cyclopts does not natively support command chaining. This is because Cyclopts opted for more flexible and robust CLI parsing, rather than a compromised, inconsistent parsing experience. With that said, Cyclopts gives you the tools to create your own command chaining experience. In this example, we will use a special delimiter token (e.g. "AND") to separate commands.

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

app = App()

@app.command
def foo(val: int):
    print(f"FOO {val=}")

@app.command
def bar(flag: bool):
    print(f"BAR {flag=}")

@app.meta.default
def main(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)]):
    # tokens is ``["foo", "123", "AND", "foo", "456", "AND", "bar", "--flag"]``
    delimiter = "AND"

    groups = [list(group) for key, group in itertools.groupby(tokens, lambda x: x == delimiter) if not key] or [[]]
    # groups is ``[['foo', '123'], ['foo', '456'], ['bar', '--flag']]``

    for group in groups:
        # Execute each group
        app(group)

if __name__ == "__main__":
    app.meta(["foo", "123", "AND", "foo", "456", "AND", "bar", "--flag"])
    # FOO val=123
    # FOO val=456
    # BAR flag=True

Autoregistry

AutoRegistry <https://github.com/BrianPugh/autoregistry> is a python library that automatically creates string-to-functionality mappings, making it trivial to instantiate classes or invoke functions from CLI parameters.

Lets consider the following program that can download a file from either a GCP, AWS, or Azure bucket (without worrying about the implementation):

import cyclopts
from pathlib import Path
from typing import Literal

def _download_gcp(bucket: str, key: str, dst: Path):
    print("Downloading data from Google.")

def _download_s3(bucket: str, key: str, dst: Path):
    print("Downloading data from Amazon.")

def _download_azure(bucket: str, key: str, dst: Path):
    print("Downloading data from Azure.")

_downloaders = {
    "gcp": _download_gcp,
    "s3": _download_s3,
    "azure": _download_azure,
}

app = cyclopts.App()

@app.command
def download(bucket: str, key: str, dst: Path, provider: Literal[tuple(_downloaders)] = "gcp"):
    downloader = _downloaders[provider]
    downloader(bucket, key, dst)

app()
$ my-script download --help
╭─ Parameters ────────────────────────────────────────────────────────────╮
│ *  BUCKET,--bucket      [required]                                      │
│ *  KEY,--key            [required]                                      │
│ *  DST,--dst            [required]                                      │
│    PROVIDER,--provider  [choices: gcp,s3,azure] [default: gcp]          │
╰─────────────────────────────────────────────────────────────────────────╯

$ my-script my-bucket my-key local.bin --provider=s3
Downloading data from Amazon.

Not bad, but let's see how this would look with AutoRegistry.

import cyclopts
from autoregistry import Registry
from pathlib import Path
from typing import Literal

_downloaders = Registry(prefix="_download_")

@_downloaders
def _download_gcp(bucket: str, key: str, dst: Path):
    print("Downloading data from Google.")

@_downloaders
def _download_s3(bucket: str, key: str, dst: Path):
    print("Downloading data from Amazon.")

@_downloaders
def _download_azure(bucket: str, key: str, dst: Path):
    print("Downloading data from Azure.")

app = cyclopts.App()

@app.command
def download(bucket: str, key: str, dst: Path, provider: Literal[tuple(_downloaders)] = "gcp"):
    downloader = _downloaders[provider]
    downloader(bucket, key, dst)

app()
$ my-script download --help
╭─ Parameters ────────────────────────────────────────────────────────────╮
│ *  BUCKET,--bucket      [required]                                      │
│ *  KEY,--key            [required]                                      │
│ *  DST,--dst            [required]                                      │
│    PROVIDER,--provider  [choices: gcp,s3,azure] [default: gcp]          │
╰─────────────────────────────────────────────────────────────────────────╯

$ my-script my-bucket my-key local.bin --provider=s3
Downloading data from Amazon.

Exactly the same functionality, but more terse and organized. With Autoregistry, the download providers are much more self-contained, do not require changes in other code locations, and reduce duplication.

App Upgrade

It's best practice for users to install python-based CLIs via pipx <https://github.com/pypa/pipx>, where each application gets it's own python virtual environment. Whether done via pipx or standard pip, updating your application can be done via the upgrade command. i.e.:

$ pipx upgrade mypackage

If you would like your CLI application to be able to upgrade itself, you can add the following command to your application:

import mypackage
import subprocess
import sys
from cyclopts import App

app = App()

@app.command
def upgrade():
    """Update mypackage to latest stable version."""
    old_version = mypackage.__version__
    subprocess.check_output([sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
    subprocess.check_output([sys.executable, "-m", "pip", "install", "--upgrade", "mypackage"])
    res = subprocess.run([sys.executable, "-m", "mypackage", "--version"], stdout=subprocess.PIPE, check=True)
    new_version = res.stdout.decode().strip()
    if old_version == new_version:
        print(f"mypackage up-to-date (v{new_version}).")
    else:
        print(f"mypackage updated from v{old_version} to v{new_version}.")

app()

sys.executable <https://docs.python.org/3/library/sys.html#sys.executable> points to the currently used python interpreter's path; if your package was installed via pipx <https://github.com/pypa/pipx>, then it points to the python interpreter in it's respective virtual environment.

Dataclass Commands

An alternative command syntax is to use dataclasses with a __call__ method. To support this pattern, Cyclopts provides the "call_if_callable" result action, which can be composed with other result actions.

Basic Example

Here's a simple example using the dataclass command pattern:

# greeter.py
from dataclasses import dataclass, KW_ONLY
from cyclopts import App

app = App(result_action=["call_if_callable", "print_non_int_sys_exit"])

@app.command
@dataclass
class Greet:
    """Greet someone with a message."""

    name: str = "World"
    _: KW_ONLY
    formal: bool = False

    def __call__(self):
        greeting = "Hello" if self.formal else "Hey"
        return f"{greeting} {self.name}."

if __name__ == "__main__":
    app()

Running this application:

$ python greeter.py greet
Hey World.

$ python greeter.py greet Alice
Hey Alice.

$ python greeter.py greet Bob --formal
Hello Bob.

How It Works

The result_action=["call_if_callable", "print_non_int_sys_exit"] creates a pipeline:

  1. call_if_callable: After parsing, Cyclopts creates an instance of the Greet dataclass. This action checks if the result is callable (it is, because of __call__), and calls it with no arguments.
  2. print_non_int_sys_exit: Takes the string returned by __call__ and prints it, then exits.

Without "call_if_callable", the app would try to print the dataclass instance itself instead of calling it and printing the result.

Interactive Shell & Help

Cyclopts has a builtin interactive shell-like feature <../api.html#cyclopts.App.interactive_shell>:

from cyclopts import App

app = App()

@app.command
def foo(p1):
    """Foo Docstring.

    Parameters
    ----------
    p1: str
        Foo's first parameter.
    """
    print(f"foo {p1}")

@app.command
def bar(p1):
    """Bar Docstring.

    Parameters
    ----------
    p1: str
        Bar's first parameter.
    """
    print(f"bar {p1}")

# A blocking call, launching an interactive shell.
app.interactive_shell(prompt="cyclopts> ")

To make the application still work as-expected from the CLI, it is more appropriate to set a command (or @app.default) to launch the shell:

@app.command
def shell():
    app.interactive_shell()

if __name__ == "__main__":
    app()  # Don't call ``app.interactive_shell()`` here.

Special flags like --help and --version work in the shell, but could be a bit awkward for the root-help:

$ python interactive-shell-demo.py
Interactive shell. Press Ctrl-D to exit.
cyclopts> --help
Usage: interactive-shell-demo.py COMMAND

╭─ Parameters ──────────────────────────────────────────────────╮
│ --version      Display application version.                   │
│ --help     -h  Display this message and exit.                 │
╰───────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────╮
│ bar  Bar Docstring.                                           │
│ foo  Foo Docstring.                                           │
╰───────────────────────────────────────────────────────────────╯
cyclopts> foo --help
Usage: interactive-shell-demo.py foo [ARGS] [OPTIONS]

Foo Docstring

╭─ Parameters ──────────────────────────────────────────────────╮
│ *  P1,--p1  Foo's first parameter. [required]                 │
╰───────────────────────────────────────────────────────────────╯
cyclopts>

To resolve this, we can explicitly add a help command:

@app.command
def help():
    """Display the help screen."""
    app.help_print()
$ python interactive-shell-demo.py
Interactive shell. Press Ctrl-D to exit.
cyclopts> help
Usage: interactive-shell-demo.py COMMAND

╭─ Parameters ──────────────────────────────────────────────────╮
│ --version      Display application version.                   │
│ --help     -h  Display this message and exit.                 │
╰───────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────╮
│ bar   Bar Docstring.                                          │
│ foo   Foo Docstring.                                          │
│ help  Display the help screen.                                │
╰───────────────────────────────────────────────────────────────╯

Rich Formatted Exceptions

Tracebacks of uncaught exceptions provide valuable feedback for debugging. This guide demonstrates how to enhance your error messages using rich formatting.

Standard Python Traceback

Consider the following example:

from cyclopts import App

app = App()

@app.default
def main(name: str):
    print(name + 3)

if __name__ == "__main__":
    app()

Running this script will produce a standard Python traceback:

$ python my-script.py foo
Traceback (most recent call last):
  File "/cyclopts/my-script.py", line 12, in <module>
    app()
  File "/cyclopts/cyclopts/core.py", line 903, in __call__
    return command(*bound.args, **bound.kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cyclopts/my-script.py", line 8, in main
    print(name + 3)
          ~~~~~^~~
TypeError: can only concatenate str (not "int") to str

Rich Formatted Traceback

To create a more visually appealing and informative traceback, you can use the Rich library's traceback handler <https://rich.readthedocs.io/en/stable/traceback.html#printing-tracebacks>. Here's how to modify your script:

import sys
from cyclopts import App
from rich.console import Console
from rich.traceback import install as install_rich_traceback

error_console = Console(stderr=True)
app = App(console=console, error_console=error_console)

# Install rich traceback handler using the error console
install_rich_traceback(console=error_console)

@app.default
def main(name: str):
    print(name + 3)

if __name__ == "__main__":
    app()

Now, running the updated script will display a rich-formatted traceback:

$ python my-script.py foo
╭──────────────── Traceback (most recent call last) ─────────────────╮
│ /cyclopts/my-script.py:16 in <module>                              │
│                                                                    │
│   13                                                               │
│   14 if __name__ == "__main__":                                    │
│   15 │   try:                                                      │
│ ❱ 16 │   │   app()                                                 │
│   17 │   except Exception:                                         │
│   18 │   │   console.print_exception(width=70)                     │
│   19                                                               │
│                                                                    │
│ /cyclopts/cyclopts/core.py:903 in __call__                         │
│                                                                    │
│    900 │   │   │   │                                               │
│    901 │   │   │   │   return asyncio.run(command(*bound.args, **b │
│    902 │   │   │   else:                                           │
│ ❱  903 │   │   │   │   return command(*bound.args, **bound.kwargs) │
│    904 │   │   except Exception as e:                              │
│    905 │   │   │   try:                                            │
│    906 │   │   │   │   from pydantic import ValidationError as Pyd │
│                                                                    │
│ /cyclopts/my-script.py:11 in main                                  │
│                                                                    │
│    8                                                               │
│    9 @app.default                                                  │
│   10 def main(name: str):                                          │
│ ❱ 11 │   print(name + 3)                                           │
│   12                                                               │
│   13                                                               │
│   14 if __name__ == "__main__":                                    │
╰────────────────────────────────────────────────────────────────────╯

This rich-formatted traceback provides a more readable and visually appealing representation of the error, but may make copy/pasting for sharing a bit more cumbersome.

Sharing Parameters

Many subcommands within a CLI may take the same parameters. For example, all commands for a CLI that deals with a remote server might need a url and port number. Furthermore, there might be common setup required, such as connecting to the remote server. If you are familiar with Click <https://click.palletsprojects.com>, this would be accomplished with contexts <https://click.palletsprojects.com/en/stable/complex/>. In Cyclopts, there are 2 ways to accomplish this:

  1. With a meta app <#meta-app>. While powerful, it's admittantly a bit heavy-handed and clunky.
  2. Via a common dataclass that is passed to each command. While less powerful than using a meta-app, it still accomplishes many of the same goals with simpler, terser code.

In this section, we'll be investigating option (2) by constructing an example application that has 2 commands:

  1. create - Connect to a server and send a POST command to it.
  2. info - Connect to a server and GET information about a user.

    # demo.py
    from cyclopts import App, Parameter
    from cyclopts.types import UInt16
    from dataclasses import dataclass
    from functools import cached_property
    from httpx import Client
    
    @Parameter(name="*")  # Flatten the namespace; i.e. option will be "--url" instead of "--common.url"
    @dataclass
    class Common:
        url: str = "http://cyclopts.readthedocs.io"
        "URL of remote server."
    
        port: UInt16  = 8080  # an "int" that is limited to range [0, 65535]
        "Port of remote server."
    
        verbose: bool = False
        "Increased printing verbosity."
    
        def __post_init__(self):
           # dataclasses call this method after calling the auto-generated __init__.
           if self.verbose:
              print(f"Server: {self.base_url}")
    
        @property
        def base_url(self) -> str:
           return f"{self.url}:{self.port}"
    
        @cached_property
        def client(self) -> Client:
            return Client(base_url=self.base_url)
    
    app = App()
    
    @app.command
    def create(name: str, age: int, *, common: Common | None = None):
        """Create a user on remote server.
    
        Parameters
        ----------
        name: str
           Name of the user to create.
        age: int
           Age of the user in years.
        """
        if common is None:
           common = Common()
        json = {"name": name, "age": age}
        if common.verbose:
            print(f"Creating user: {json}")
        common.client.post("/users", json=json)
        # TODO: in a real application, we should error-check the response here.
    
    @app.command
    def info(name: str, *, common: Common | None = None):
        """List a user on remote server.
    
        Parameters
        ----------
        name: str
           Name of the user to get info about.
        """
        if common is None:
           common = Common()
        response = common.client.get("/users", params={"name": name})
        user = response.json()
        print(f"User: {user}")
    
    if __name__ == "__main__":
        app()

From the root help-page, we can see our two commands:

$ python demo.py --help
Usage: demo.py COMMAND

╭─ Commands ─────────────────────────────────────────────────────────────────╮
│ create     Create a user on remote server.                                 │
│ info       List a user on remote server.                                   │
│ --help -h  Display this message and exit.                                  │
│ --version  Display application version.                                    │
╰────────────────────────────────────────────────────────────────────────────╯

From the create help-page, we can see all of our parameters:

$ python demo.py create --help
Usage: demo.py create [ARGS] [OPTIONS]

Create a user on remote server.

╭─ Parameters ───────────────────────────────────────────────────────────────╮
│ *  NAME --name             Name of the user to create. [required]          │
│ *  AGE --age               Age of the user in years. [required]            │
│    --url                   URL of remote server. [default:                 │
│                            http://cyclopts.readthedocs.io]                 │
│    --port                  Port of remote server. [default: 8080]          │
│    --verbose --no-verbose  Increased printing verbosity. [default: False]  │
╰────────────────────────────────────────────────────────────────────────────╯

Some example command-line invocations:

$ python demo.py create Alice 42
# No response from the CLI.

$ python demo.py create Alice 42 --verbose
Creating user: {'name': 'Alice', 'age': 42}

By organizing the code this way, we can centralize shared parameters and logic between many commands.

Unit Testing

It is important to have unit-tests to verify that your CLI is behaving correctly. For unit-testing, we will be using the defacto-standard python unit-testing library, pytest <https://docs.pytest.org/en/stable/>. This section demonstrates some common scenarios you may encounter when unit-testing your CLI app.

Lets make a small application that checks PyPI <https://pypi.org> if a library name is available:

# pypi_checker.py
import sys
import urllib.error
import urllib.request
import cyclopts

def _check_pypi_name_available(name):
    try:
        urllib.request.urlopen(f"https://pypi.org/pypi/{name}/json")
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return True  # Package does not exist (name is available)
    return False  # Package exists (name is not available)

app = cyclopts.App(
      config=[
         cyclopts.config.Env("PYPI_CHECKER_"),
         cyclopts.config.Json("config.json"),
      ],
)

@app.default
def pypi_checker(name: str, *, silent: bool = False) -> bool:
    """Check if a package name is available on PyPI.

    Returns True if available; False otherwise.

    Parameters
    ----------
    name: str
        Name of the package to check.
    silent: bool
        Do not print anything to stdout.
    """
    is_available = _check_pypi_name_available(name)
    if not silent:
        if is_available:
            print(f"{name} is available.")
        else:
            print(f"{name} is not available.")
    return is_available

if __name__ == "__main__":
    app()

Running the app from the console:

$ python pypi_checker.py --help
Usage: pypi_checker COMMAND [ARGS] [OPTIONS]

Check if a package name is available on PyPI.

Returns True if available; False otherwise.

╭─ Commands ────────────────────────────────────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                                                         │
│ --version  Display application version.                                                           │
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ──────────────────────────────────────────────────────────────────────────────────────╮
│ *  NAME --name           Name of the package to check. [env var: PYPI_CHECKER_NAME] [required]    │
│    --silent --no-silent  Do not print anything to stdout. [env var: PYPI_CHECKER_SILENT]          │
│                          [default: False]                                                         │
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯

$ python pypi_checker.py cyclopts
cyclopts is not available.

$ python pypi_checker.py cyclopts --silent
$ echo $?  # Check the exit code of the previous command.
1

$ python pypi_checker.py the-next-big-project
the-next-big-project is available.
$ echo $?  # Check the exit code of the previous command.
0

We will slowly introduce unit-testing concepts and build up a fairly comprehensive set of unit-tests for this application.

Mocking

First off, it's good code-hygiene to separate "business logic" from "user interface." In this example, that means putting all the actual logic of determining whether or not a package name is available into the _check_pypi_name_available function, and putting all of the CLI logic (like printing to stdout and exit-codes) in the Cyclopts-decorated function pypi_checker. This makes it easier to unit-test the app because it allows us to mock <https://docs.python.org/3/library/unittest.mock.html> out portions of our app, allowing us to isolate our CLI unit-tests to just the CLI components.

We can use pytest-mock <https://pytest-mock.readthedocs.io/en/latest/> to simplify mocking _check_pypi_name_available. Let's define a fixture <https://docs.pytest.org/en/stable/explanation/fixtures.html> that declares this mock.

# test.py
import pytest
from pypi_checker import app

@pytest.fixture
def mock_check_pypi_name_available(mocker):
    return mocker.patch("pypi_checker._check_pypi_name_available")

Unit tests that use this fixture can define it's return value, as well as check the arguments it was called with. This will be demonstrated in the next section.

Exit Codes

Our command function returns a boolean. By default, Cyclopts uses result_action <#cyclopts.App.result_action> of "print_non_int_sys_exit", which calls sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> with the appropriate code: True <https://docs.python.org/3/library/constants.html#True> → 0 (success), False <https://docs.python.org/3/library/constants.html#False> → 1 (failure).

import pytest

def test_unavailable_name_cli_behavior(mock_check_pypi_name_available):
    # Set the mock return_value to False (i.e. the name is NOT available).
    mock_check_pypi_name_available.return_value = False
    with pytest.raises(SystemExit) as exc_info:
        app("foo")  # Default result_action calls sys.exit
    mock_check_pypi_name_available.assert_called_once_with("foo")
    assert exc_info.value.code == 1  # Package unavailable exits with code 1

We can then run pytest on this file:

$ pytest test.py
============================== test session starts ==============================
platform darwin -- Python 3.13.0, pytest-8.3.4, pluggy-1.5.0
rootdir: /cyclopts-demo
configfile: pyproject.toml
plugins: cov-6.0.0, anyio-4.8.0, mock-3.14.0
collected 1 item

test.py .                                                                 [100%]

=============================== 1 passed in 0.05s ===============================

Checking stdout

We also want to make sure that our message is displayed to the user. The built-in capsys <https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html#accessing-captured-output-from-a-test-function> fixture gives us access to our application's stdout. We can use this to confirm our app prints the correct statement.

By passing result_action="return_value" to the app call, we can get the return value directly without sys.exit() <https://docs.python.org/3/library/sys.html#sys.exit> being called:

# test.py - continued from "Exit Codes"
def test_unavailable_name_with_output(capsys, mock_check_pypi_name_available):
    mock_check_pypi_name_available.return_value = False
    is_available = app("foo", result_action="return_value")
    mock_check_pypi_name_available.assert_called_once_with("foo")
    assert is_available is False
    assert capsys.readouterr().out == "foo is not available.\n"
Note:

Normal output goes to console <#cyclopts.App.console> (stdout), while errors go to error_console <#cyclopts.App.error_console> (stderr). Use capsys.readouterr().err to check error messages, or provide a custom error_console to capture both streams together.

Environment Variables

Because we configured our App <#cyclopts.App> with cyclopts.config.Env <#cyclopts.config.Env>, we can pass arguments into our application via environment variables. The pytest monkeypatch fixture <https://docs.pytest.org/en/stable/how-to/monkeypatch.html> allows us to modify environment variables within the context of a unit-test.

In this test, we only want to test if our environment variable is being passed in correctly. We will use App.parse_args() <#cyclopts.App.parse_args>, which performs all the parsing, but doesn't actually invoke the command.

# test.py
def test_name_env_var(monkeypatch):
    from pypi_checker import pypi_checker
    monkeypatch.setenv("PYPI_CHECKER_NAME", "foo")
    command, bound, _ = app.parse_args([])  # An empty list - no CLI arguments passed in.
    assert command == pypi_checker
    assert bound.arguments['name'] == "foo"
Warning:

A common mistake is accidentally calling app() or app.parse_args() with the intent of providing no arguments. Calling these methods with no arguments will read from sys.argv <https://docs.python.org/3/library/sys.html#sys.argv>, the same as in a typical application. This is rarely the intention in a unit-test, and Cyclopts will produce a warning. For example, this code in a unit test:

app()  # Wrong: will produce a warning

Will generate this warning:

=============================== warnings summary ================================
test.py::test_no_args
  /my_project/test.py:64: UserWarning: Cyclopts application invoked without tokens
  under unit-test framework "pytest". Did you mean "app([])"?
    app()

The proper way to specify no CLI arguments is to provide an empty string or list:

app([])

File Config

To explicitly test that configurations from the Cyclopts configuration system <#config-files> are loading properly, we can create a configuration file in a temporary directory and change our current-working-directory (cwd) to that temporary directory. The pytest built-in tmp_path fixture gives us a temporary directory, and the monkeypatch fixture allows us to change the cwd. We have to change the cwd because typically configuration files are discovered relative to the directory where the CLI was invoked. If your CLI searches other locations (such as the home directory), you will need to modify this example appropriately.

# test.py
import json
from pypi_checker import pypi_checker

@pytest.fixture(autouse=True)
def chdir_to_tmp_path(tmp_path, monkeypatch):
    "Automatically change current directory to tmp_path"
    monkeypatch.chdir(tmp_path)

@pytest.fixture
def config_path(tmp_path):
    "Path to JSON configuration file in tmp_path"
    return tmp_path / "config.json"  # same name that was provided to cyclopts.config.Json

def test_config(config_path):
    with config_path.open("w") as f:
       json.dump({"name": "bar"}, f)
    command, bound, _ = app.parse_args([])  # An empty list - no CLI arguments passed in.
    assert command == pypi_checker
    assert bound.arguments['name'] == "bar"

Help Page

Cyclopts uses Rich <https://rich.readthedocs.io/en/stable/> to pretty-print messages to the console. Rich interprets the console environment, and can change how it displays text depending on the terminal's capabilities. For unit testing, we will explicitly set a lot of these parameters in a pytest fixture to make it easier to compare against known good values:

@pytest.fixture
def console():
    from rich.console import Console
    return Console(width=70, force_terminal=True, highlight=False, color_system=None, legacy_windows=False)

Since the help-page is just printed to stdout, we will be using the capsys <https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html#accessing-captured-output-from-a-test-function> fixture again.

import pytest
from textwrap import dedent

def test_help_page(capsys, console):
    with pytest.raises(SystemExit):
        app("--help", console=console)
    actual = capsys.readouterr().out
    assert actual == dedent(
        """\
        Usage: pypi_checker COMMAND [ARGS] [OPTIONS]

        Check if a package name is available on PyPI.

        Returns True if available; False otherwise.

        ╭─ Commands ─────────────────────────────────────────────────────────╮
        │ --help -h  Display this message and exit.                          │
        │ --version  Display application version.                            │
        ╰────────────────────────────────────────────────────────────────────╯
        ╭─ Parameters ───────────────────────────────────────────────────────╮
        │ *  NAME --name           Name of the package to check. [required]  │
        │    --silent --no-silent  Do not print anything to stdout.          │
        │                          [default: False]                          │
        ╰────────────────────────────────────────────────────────────────────╯
        """
    )

The textwrap.dedent() <https://docs.python.org/3/library/textwrap.html#textwrap.dedent> function allows us to have our expected-help-string nicely indented within our code. Alternatively, we could have used the rich.console.Console.capture() <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console.capture> context manager to directly capture the rich.console.Console <https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console> output.

Note:

Unit-testing the help-page is probably overkill for most projects (and may get in the way more often than it helps!).

Reading/Writing from File or Stdin/Stdout

In many CLI applications, it's common to be able to read from a file or stdin, and write to a file or stdout. This allows for the chaining of many CLI applications via pipes |.

StdioPath

Note:

StdioPath <#cyclopts.types.StdioPath> requires Python 3.12+. For older Python versions, see Alternative Approach (Python < 3.12) below.

The recommended approach is to use StdioPath <#cyclopts.types.StdioPath>, a Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> subclass that treats - as stdin (for reading) or stdout (for writing). This follows common Unix convention <https://clig.dev/#arguments-and-flags> used by many command-line tools.

from cyclopts import App
from cyclopts.types import StdioPath

app = App()

@app.default
def scream(input_: StdioPath, output: StdioPath):
    """Uppercase all input data.

    Parameters
    ----------
    input_:
        Input file path, or "-" for stdin.
    output:
        Output file path, or "-" for stdout.
    """
    data = input_.read_text()
    output.write_text(data.upper())

if __name__ == "__main__":
    app()
$ echo "hello cyclopts users." > demo.txt

$ python scream.py demo.txt -
HELLO CYCLOPTS USERS.

$ python scream.py demo.txt output.txt && cat output.txt
HELLO CYCLOPTS USERS.

$ echo "foo" | python scream.py - -
FOO

StdioPath <#cyclopts.types.StdioPath> is pre-configured with allow_leading_hyphen=True, so - can be passed as an argument without being interpreted as an option.

Defaulting to Stdin/Stdout

To make stdin/stdout the default when no argument is provided, use StdioPath("-") as the default value:

from cyclopts import App
from cyclopts.types import StdioPath

app = App()

@app.default
def scream(input_: StdioPath = StdioPath("-"), output: StdioPath = StdioPath("-")):
    """Uppercase all input data.

    Parameters
    ----------
    input_:
        Input file path. Defaults to stdin if not provided.
    output:
        Output file path. Defaults to stdout if not provided.
    """
    data = input_.read_text()
    output.write_text(data.upper())

if __name__ == "__main__":
    app()
$ echo "hello cyclopts users." > demo.txt

$ python scream.py demo.txt
HELLO CYCLOPTS USERS.

$ python scream.py demo.txt output.txt && cat output.txt
HELLO CYCLOPTS USERS.

$ echo "foo" | python scream.py
FOO

Binary Data

StdioPath also supports binary reading and writing:

@app.default
def process_binary(input_: StdioPath = StdioPath("-"), output: StdioPath = StdioPath("-")):
    data = input_.read_bytes()
    output.write_bytes(data)

Or using the context manager interface:

@app.default
def process_binary(input_: StdioPath = StdioPath("-"), output: StdioPath = StdioPath("-")):
    with input_.open("rb") as f_in, output.open("wb") as f_out:
        f_out.write(f_in.read())

Alternative Approach (Python < 3.12)

For Python versions before 3.12, or when you prefer an Optional[Path] pattern where None indicates stdin/stdout, you can use helper functions:

import sys
from cyclopts import App
from pathlib import Path
from typing import Optional

def read_str(input_: Optional[Path]) -> str:
    return sys.stdin.read() if input_ is None else input_.read_text()

def write_str(output: Optional[Path], data: str):
    sys.stdout.write(data) if output is None else output.write_text(data)

def read_bytes(input_: Optional[Path]) -> bytes:
    return sys.stdin.buffer.read() if input_ is None else input_.read_bytes()

def write_bytes(output: Optional[Path], data: bytes):
    sys.stdout.buffer.write(data) if output is None else output.write_bytes(data)

app = App()

@app.default
def scream(input_: Optional[Path] = None, output_: Optional[Path] = None):
    """Uppercase all input data.

    Parameters
    ----------
    input_ : Optional[Path]
        If provided, read data from file. If not provided, read from stdin.
    output_ : Optional[Path]
        If provided, write data to file. If not provided, write to stdout.
    """
    data = read_str(input_)
    processed = data.upper()
    write_str(output_, processed)

if __name__ == "__main__":
    app()
$ echo "hello cyclopts users." > demo.txt
$ python scream.py demo.txt
HELLO CYCLOPTS USERS.
$ python scream.py demo.txt output.txt
$ cat output.txt
HELLO CYCLOPTS USERS.
$ echo "foo" | python scream.py
FOO

Migrating from Typer

Much of Cyclopts's syntax is Typer <https://typer.tiangolo.com>-inspired. Migrating from Typer should be pretty straightforward; it is recommended to first read the Getting Started <#getting-started> and Commands <#commands> sections. The below table offers a jumping off point for translating the various portions of the APIs. The Typer Comparison <#typer-comparison> page also provides many examples comparing the APIs.

Typer-to-Cyclopts API Reference

TyperCycloptsNotes
typer.Typer()cyclopts.App() <#cyclopts.App>
Same/similar fields:
  • App.name <#cyclopts.App.name> - Optional name of application or sub-command.
Cyclopts has more user-friendly default features:
  • Equivalent no_args_is_help=True.
  • Equivalent pretty_exceptions_enable=False.
@app.command()@app.command() <#cyclopts.App.command>In Cyclopts, @app.command always results in a command. <#typer-default-command> To define an action when no command is provided, see @app.default <#cyclopts.App.default>.
app.add_typer(...)()app.command(...)()Sub applications and commands are registered the same way in Cyclopts.
@app.callback()@app.default() <#cyclopts.App.default>

@app.meta.default() <#cyclopts.App.default>
Typer's callback always executes before executing an app. If used to provide functionality when no command was specified from the CLI, then use @app.default() <#cyclopts.App.default>. Otherwise, checkout Cyclopt's Meta App <#meta-app>.
Annotated[..., typer.Argument(...)]

Annotated[..., typer.Option(...)]
Annotated[..., cyclopts.Parameter(...)] <#cyclopts.Parameter>In Cyclopts, Positional/Keyword arguments are determined from the function signature. <#typer-argument-vs-option> Some of Typer's validation fields, like exists for Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path> types are handled in Cyclopts by explicit validators. <#parameter-validators>

Cyclopts and Typer mostly handle type-hints the same way, but there are a few notable exceptions:

Typer-to-Cyclopts Type-Hints

Type AnnotationNotes
Enum <https://docs.python.org/3/library/enum.html#enum.Enum>Compared to Typer, Cyclopts handles Enum <https://docs.python.org/3/library/enum.html#enum.Enum> lookups in the reverse direction. <#typer-choices> Frequently, Literal <https://docs.python.org/3/library/typing.html#typing.Literal> offers a more terse, intuitive choice option. <#coercion-rules-literal>
Union <https://docs.python.org/3/library/typing.html#typing.Union>Typer does not support type unions. Cyclopts does. <#coercion-rules-union>

General Steps

  1. Add the following import: from cyclopts import App, Parameter.
  2. Change app = Typer(...) to just app = App(). Revisit more advanced configuration later.
  3. Remove all @app.callback stuff. Cyclopts already provides a good --version handler for you.
  4. Replace all Annotated[..., Argument/Option] type-hints with Annotated[..., Parameter()] <#cyclopts.Parameter>. If only supplying a help <#cyclopts.Parameter.help> string, it's better to supply it via docstring. <#typer-docstring-parsing>
  5. Cyclopts has similar boolean-flag handling as Typer, but has different configuration parameters. <#typer-flag-negation>

    #########
    # Typer #
    #########
    # Overriding the name results in no "False" flag generation.
    my_flag: Annotated[bool, Option("--my-custom-flag")]
    # However, it can be custom specified:
    my_flag: Annotated[bool, Option("--my-custom-flag/--disable-my-custom-flag")]
    
    ############
    # Cyclopts #
    ############
    # Overriding the name still results in "False" flag generation:
    #    --my-custom-flag --no-my-custom-flag
    my_flag: Annotated[bool, Parameter("--my-custom-flag")]
    # Negative flag generation can be disabled:
    #    --my-custom-flag
    my_flag: Annotated[bool, Parameter("--my-custom-flag", negative="")]
    # Or the prefix can be changed:
    #    --my-custom-flag --disable-my-custom-flag
    my_flag: Annotated[bool, Parameter("--my-custom-flag", negative_bool="--disable-")]

After the basic migration is done, it is recommended to read through the rest of Cyclopts's documentation to learn about some of the better functionality it has, which could result in cleaner, terser code.

Typer Comparison

Much of Cyclopts was inspired by the excellent Typer <https://typer.tiangolo.com> library. Despite its popularity, Typer has some traits that I (and others) find less than ideal. Part of this stems from Typer's age, with its first release in late 2019, soon after Python 3.8's release. Because of this, most of its API was initially designed around assigning proxy default values to function parameters. This made the decorated command functions difficult to use outside of Typer. With the introduction of Annotated <https://docs.python.org/3/library/typing.html#typing.Annotated> in python3.9, type-hints were able to be directly annotated, allowing for the removal of these proxy defaults.

Additionally, Typer is built on top of Click <https://click.palletsprojects.com>. This makes it difficult for newcomers to figure out which elements are Typer-related and which elements are click-related. It's also hard to tell whether the following criticisms stem from Typer, or the underlying Click. For better-or-worse, Cyclopts uses its own internal parsing strategy, gaining complete control over the process.

This section was originally written about Typer v0.9.0 (May 2023). Some criticisms have been addressed in later Typer versions; updates are noted in the respective sections below.

Argument vs Option

In Typer, there are two primary classes for providing CLI parameter configuration:

  • Argument results in a positional CLI argument.
  • Option results in a keyword CLI argument, preceded with a --.

With more modern python type annotations, this distinction is unnecessary, because parameters (positional or keyword) can be determined directly from the function signature.

Consider the following function signatures:

def pos_or_keyword(a, b):
    pass

def pos_only(a, b, /):
    pass

def keyword_only(*, a, b=2):
    pass

def mixture(a, /, b, *, c=3):
    pass

If you aren't familiar with these declarations, refer to the official PEP570 <https://peps.python.org/pep-0570/>, or a more user-friendly tutorial <https://realpython.com/lessons/positional-only-arguments/>.

From these function signatures, we can deduce:

  1. Which parameters are position-only, keyword-only, or both.
  2. Which parameters are required, by their lack of defaults.

Because of these builtin python mechanisms, Cyclopts has a single Parameter <#cyclopts.Parameter> class used for providing additional parameter metadata.

I believe that Typer's separate Argument and Option classes are a relic from when they must be supplied as a parameter's proxy default value.

app = typer.Typer()

@app.command()
def foo(a=Argument(), b=Option(default=2)):
    pass

When used as such, we lose the ability to define the function signature with position-only or keyword-only markers. We also lose the ability to directly inspect which parameters are optional by having "real" defaults and which ones are required.

Positional or Keyword Arguments

A limitation of Typer is that a parameter cannot be both positional and keyword.

For example, lets say we want to implement a mv-like program that takes in a source path, and a destination path:

typer_app = typer.Typer()

@typer_app.command()
def mv(src, dst):
    print(f"Moving {src} -> {dst}")

typer_app(["foo", "bar"], standalone_mode=False)
# Moving foo -> bar

The code works when supplying the inputs as positional arguments, but fails when trying to specify them as keywords.

print("Typer keyword:")
typer_app(["--src", "foo", "--dst", "bar"], standalone_mode=False)
# No such option: --src

Cyclopts handles both situations:

cyclopts_app = cyclopts.App()

@cyclopts_app.default()
def mv(src, dst):
    print(f"Moving {src} -> {dst}")

cyclopts_app(["foo", "bar"])
# Moving foo -> bar
cyclopts_app(["--src", "foo", "--dst", "bar"])
# Moving foo -> bar

Choices

Enum

Frequently, a CLI will want to limit values provided to a parameter to a specific set of choices. With Typer, this is accomplished via declaring an Enum <https://docs.python.org/3/library/enum.html#enum.Enum>.

import typer
from enum import Enum

class Environment(str, Enum):
    # Values end in "_value" to avoid confusion in this example.
    DEV = "dev_value"
    STAGING = "staging_value"
    PROD = "prod_value"

typer_app = typer.Typer()

@typer_app.command
def foo(env: Environment = Environment.DEV):
    print(f"Using: {env.name}")

print("Typer (Enum):")
typer_app(["--env", "staging_value"])
# Using: STAGING

Typer looks for the CLI-provided value, and supplies the function with the enum member. IMHO, this is backwards; typically the enum name (e.g. DEV) is intended to be more human-friendly, while the value (e.g. dev_value) more frequently has a programmatic-meaning. When using enums, Cyclopts will do the opposite of Typer, performing a case-insensitive lookup by name.

import cyclopts

cyclopts_app = cyclopts.App()

@cyclopts_app.default
def foo(env: Environment = Environment.DEV):
    print(f"Using: {env.name}")

print("Cyclopts (Enum):")
cyclopts_app(["--env", "staging"])
# Using: STAGING

Literal

Enums don't work well with everyone's workflow. Many people prefer to directly use strings for their functions' options. The much more intuitive, convenient method of doing this is with the Literal <https://docs.python.org/3/library/typing.html#typing.Literal> type annotation.

Note:

Typer added support for Literal <https://docs.python.org/3/library/typing.html#typing.Literal> in version 0.19.0 (September 2025), resolving a feature request from early 2020 <https://github.com/tiangolo/typer/issues/76>.

Cyclopts has builtin support for Literal <https://docs.python.org/3/library/typing.html#typing.Literal>, see Coercion Rules - Literal <#coercion-rules-literal>.

import cyclopts
from typing import Literal

cyclopts_app = cyclopts.App()

@cyclopts_app.default
def foo(env: Literal["dev", "staging", "prod"] = "staging"):
    print(f"Using: {env}")


print("Cyclopts (Literal):")
cmd = ["--env", "staging"]
print(cmd)
cyclopts_app(cmd)
# Using: staging

Default Command

Typer has an annoying design quirk where if you register a single command, it won't expect you to provide the command name in the CLI. For example:

import typer

typer_app = typer.Typer()

@typer_app.command()
def foo():
    print("FOO")

typer_app([], standalone_mode=False)
# FOO
typer_app(["foo"], standalone_mode=False)
# raises exception: Got unexpected extra argument (foo)

Once you add a second command, then the CLI expects the command to be provided:

typer_app(["foo"], standalone_mode=False)
# FOO
typer_app(["bar"], standalone_mode=False)
# BAR

This behavior catches many people off guard. <https://github.com/tiangolo/typer/issues/315> If you want a single command, you have to unintuitively declare a callback. Github user ajlive's callback solution <https://github.com/tiangolo/typer/issues/315#issuecomment-1142593959> is copied below.

@app.callback()
def dummy_to_force_subcommand() -> None:
    """
    This function exists because Typer won't let you force a single subcommand.
    Since we know we will add other subcommands in the future and don't want to
    break the interface, we have to use this workaround.

    Delete this when a second subcommand is added.
    """
    pass

To avoid this confusion, Cyclopts has two ways of registering a function:

  1. @app.command <#cyclopts.App.command> - Register a function as a command.
  2. @app.default <#cyclopts.App.default> - Invoked if no registered command can be parsed from the CLI.

    import cyclopts
    
    cyclopts_app = cyclopts.App()
    
    @cyclopts_app.command
    def foo():
        print("FOO")
    
    cyclopts_app(["foo"])
    # FOO

Docstring Parsing

Typer performs no docstring parsing. Frequently, Typer's Argument/Option is only used to provide a help string. However, this help string commonly mirrors the function's docstring.

Consider the following Typer program:

import typer

typer_app = typer.Typer()

@typer_app.callback()
def dummy():
    # So that ``foo`` is considered a command.
    pass

@typer_app.command()
def foo(bar):
    """Foo Docstring.

    Parameters
    ----------
    bar: str
        Bar parameter docstring.
    """

typer_app()
$ my-script --help
╭─ Commands ────────────────────────────────────────────────────────────╮
│ foo                 Foo Docstring.                                    │
╰───────────────────────────────────────────────────────────────────────╯

$ my-script foo --help
Foo Docstring.
Parameters ---------- bar: str     Bar parameter docstring.

╭─ Arguments ───────────────────────────────────────────────────────────╮
│ *    bar      TEXT  [default: None] [required]                        │
╰───────────────────────────────────────────────────────────────────────╯

The foo command's short description was properly parsed from the docstring. However, it mangles the Numpy-style docstring (or any docstring format for that matter) and doesn't correctly display bar's help. Typer just displays the entire docstring.

To achieve the desired result with Typer, we have to explicitly annotate the parameter bar:

@typer_app.command()
def foo(bar: Annotated[str, Argument(help="Bar parameter docstring.")]):
    ...

For any serious application, this means that every function parameter must be annotated this way, significantly bloating the function signature.

Compare this to Cyclopts:

import cyclopts

cyclopts_app = cyclopts.App()

@cyclopts_app.command()
def foo(bar):
    """Foo Docstring.

    Parameters
    ----------
    bar: str
        Bar parameter docstring.
    """

cyclopts_app()
$ my-script --help
╭─ Commands ────────────────────────────────────────────────────────────╮
│ foo  Foo Docstring.                                                   │
╰───────────────────────────────────────────────────────────────────────╯

$ my-script foo --help

Foo Docstring.

╭─ Parameters ──────────────────────────────────────────────────────────╮
│ *  BAR,--bar  Bar parameter docstring. [required]                     │
╰───────────────────────────────────────────────────────────────────────╯

Cyclopts did not mangle the docstring into the long description, and it correctly parsed bar's help. This ends up significantly simplifying function signatures in the common situation where just a help string needs to be added. The common case in Cyclopts does not require the lengthy Annotated[str, Parameter(help="Bar parameter docstring")].

Internally, Cyclopts uses the excellent docstring_parser <https://github.com/rr-/docstring_parser> library for parsing docstrings. Check their project out!

Decorator Parentheses

A minor nitpick, but all of Typer's decorators require parentheses.

import typer

typer_app = typer.Typer()

# This doesn't work! Missing ()
@typer_app.command
def foo():
    pass

Cyclopts works with and without parentheses.

import cyclopts

cyclopts_app = cyclopts.App()

# This works! Missing ()
@cyclopts_app.command
def foo():
    pass

# This also works.
@cyclopts_app.command()
def bar():
    pass

Optional Lists

Note:

This issue has been addressed in Typer v0.10.0 <https://github.com/tiangolo/typer/releases/tag/0.10.0>.

Typer does not handle optional lists particularly well. In Typer, if a list argument is not provided via the CLI, an empty list is passed to the command by default. While this might be acceptable in some scenarios, it can be unexpected and differs semantically from the default value. Because lists are mutable, and setting mutable defaults is strongly discouraged <https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments>, setting list parameters' default to None <https://docs.python.org/3/library/constants.html#None> is common practice. This approach can also help differentiate between the intention of using a default list and explicitly requesting an empty list.

Consider the following Typer example:

import typer

typer_app = typer.Typer()

@typer_app.command()
def foo(favorite_numbers: Optional[list[int]] = None):
    if favorite_numbers is None:
        favorite_numbers = [1, 2, 3]
    print(f"My favorite numbers are: {favorite_numbers}")

typer_app(["--favorite-numbers", "100", "--favorite-numbers", "200"], standalone_mode=False)
# My favorite numbers are: [100, 200]
typer_app([], standalone_mode=False)
# My favorite numbers are: []

In this example, we expect the default list [1, 2, 3] to be used when no input is provided. However, Typer supplies an empty list instead of None <https://docs.python.org/3/library/constants.html#None>.

Cyclopts has a more intuitive solution. If no CLI option is specified, no argument is bound, so the parameter's default value None <https://docs.python.org/3/library/constants.html#None> is used. If we wish to pass an empty iterable (e.g. set <https://docs.python.org/3/library/stdtypes.html#set> or list <https://docs.python.org/3/library/stdtypes.html#list>), Cyclopts provides an --empty-* flag for each iterable parameter. This feature is configurable via Parameter.negative_iterable <#cyclopts.Parameter.negative_iterable>.

import cyclopts

cyclopts_app = cyclopts.App()

@cyclopts_app.default()
def foo(favorite_numbers: Optional[list[int]] = None):
    if favorite_numbers is None:
        favorite_numbers = [1, 2, 3]
    print(f"My favorite numbers are: {favorite_numbers}")

cyclopts_app(["--favorite-numbers", "100", "--favorite-numbers", "200"])
# My favorite numbers are: [100, 200]
cyclopts_app([])
# My favorite numbers are: [1, 2, 3]
cyclopts_app(["--empty-favorite-numbers"])
# My favorite numbers are: []

Keyword Multiple Values

In some applications, it is desirable to supply multiple values to a keyword argument. For example, lets consider an application where we want to specify multiple input files. We want our application to look like the following:

$ my-program output.bin --input input1.bin input2.bin input3.bin

Interpreted as:

output=PosixPath('output.bin')
input=[PosixPath('input1.bin'), PosixPath('input2.bin'), PosixPath('input3.bin')]

In Typer, it is impossible to accomplish this <https://github.com/pallets/click/issues/484>. With Typer, the keyword must be specified before each value:

$ my-program output.bin --input input1.bin --input input2.bin --input input3.bin

By default, Cyclopts behavior mimics Typer, where a single element worth of CLI tokens are consumed. However, by setting Parameter.consume_multiple <#cyclopts.Parameter.consume_multiple> to True <https://docs.python.org/3/library/constants.html#True>, multiple elements worth of CLI tokens will be consumed. Consider the following example program with a single output path, and multiple input paths.

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

app = App()

@app.default
def main(output: Path, input: Annotated[list[Path], Parameter(consume_multiple=True)]):
   print(f"{input=} {output=}")

if __name__ == "__main__":
   app()

All of the following invocations are equivalent:

$ my-program output.bin input1.bin input2.bin input3.bin                         # Supplying arguments positionally.
$ my-program output.bin --input input1.bin --input input2.bin --input input3.bin # Supplying input arguments via multiple keywords.
$ my-program output.bin --input input1.bin input2.bin input3.bin                 # Supplying input arguments via a single keyword.
$ my-program --input input1.bin input2.bin input3.bin --output output.bin        # Supplying all arguments via keywords.
$ my-program --input input1.bin input2.bin input3.bin -- output.bin              # Using the POSIX convention to indicate the end of keywords

To set this configuration for your entire application, supply it to your root App.default_parameter <#cyclopts.App.default_parameter>:

from cyclopts import App, Parameter

app = App(default_parameter=Parameter(consume_multiple=True))

Flag Negation

For boolean parameters, Typer adds a --no-MY-FLAG-NAME to specify a False argument.

import typer

typer_app = typer.Typer()

@typer_app.command()
def foo(my_flag: bool = False):
    print(f"{my_flag=}")

typer_app(["--my-flag"], standalone_mode=False)
# my_flag=True
typer_app(["--no-my-flag"], standalone_mode=False)
# my_flag=False

Overriding the option's name will disable Typer's negative-flag generation logic:

import typer
from typing import Annotated

typer_app = typer.Typer()

@typer_app.command()
def foo(my_flag: Annotated[bool, Option("--my-flag")] = False):
    print(f"{my_flag=}")

typer_app(["--my-flag"], standalone_mode=False)
# my_flag=True
typer_app(["--no-my-flag"], standalone_mode=False)
# NoSuchOption: No such option: --no-my-flag

This is not the worst, but there is a tiny bit of duplication. To use a different negative flag, you can supply the name after a slash in your option-name-string.

import typer

typer_app = typer.Typer()

@typer_app.command()
def foo(my_flag: Annotated[bool, Option("--my-flag/--your-flag")] = False):
    print(f"{my_flag=}")

typer_app(["--my-flag"], standalone_mode=False)
# my_flag=True
typer_app(["--your-flag"], standalone_mode=False)
# my_flag=False

Cyclopts's Parameter <#cyclopts.Parameter> takes in an optional negative <#cyclopts.Parameter.negative> flag. To suppress the negative-flag generation, set this argument to either an empty string or list.

import cyclopts
from typing import Annotated

cyclopts_app = cyclopts.App()

@cyclopts_app.default
def foo(my_flag: Annotated[bool, cyclopts.Parameter(negative="")] = False):
    print(f"{my_flag=}")

print("Cyclopts:")
cyclopts_app(["--my-flag"])
# my_flag=True
cyclopts_app(["--your-flag"], exit_on_error=False)
# ╭─ Error ─────────────────────────────────────────────────────────────────────╮
# │ Error converting value "--your-flag" to <class 'bool'> for "--my-flag".     │
# ╰─────────────────────────────────────────────────────────────────────────────╯
# CoercionError: Error converting value "--your-flag" to <class 'bool'> for "--my-flag".

To define your own custom negative flag, just provide it as a string or list of strings.

@cyclopts_app.default
def foo(my_flag: Annotated[bool, cyclopts.Parameter(negative="--your-flag")] = False):
    print(f"{my_flag=}")

print("Cyclopts:")
cyclopts_app(["--my-flag"])
# my_flag=True
cyclopts_app(["--your-flag"])
# my_flag=False

The default --no- negation prefix can also be customized with negative_bool <#cyclopts.Parameter.negative_bool>.

@cyclopts_app.default
def foo(my_flag: Annotated[bool, cyclopts.Parameter(negative_bool="--disable-")] = False):
    print(f"{my_flag=}")

print("Cyclopts:")
cyclopts_app(["--my-flag"])
# my_flag=True
cyclopts_app(["--disable-my-flag"])
# my_flag=False

Help Defaults

In Typer's --help display, default values are unhelpfully shown for required arguments.

Note:

This was fixed in Typer 0.17.2 (August 2025) when using Rich for help display.

import typer

typer_app = typer.Typer()

@typer_app.command()
def compress(
    src: Annotated[Path, typer.Argument(help="File to compress.")],
    dst: Annotated[Path, typer.Argument(help="Path to save compressed data to.")] = Path("out.zip"),
):
    print(f"Compressing data from {src} to {dst}")

print("Typer positional:")
typer_app(["--help"], standalone_mode=False)
# ╭─ Arguments ───────────────────────────────────────────────────────────────╮
# │ *    src      PATH   File to compress. [default: None] [required]         │
# │      dst      [DST]  Path to save compressed data to. [default: out.zip]  │
# ╰───────────────────────────────────────────────────────────────────────────╯

It doesn't make any sense to show a default for a parameter that is required and has no default. Cyclopts fixes this:

import cyclopts

cyclopts_app = cyclopts.App()

@cyclopts_app.default()
def compress(
    src: Annotated[Path, cyclopts.Parameter(help="File to compress.")],
    dst: Annotated[Path, cyclopts.Parameter(help="Path to save compressed data to.")] = Path("out.zip"),
):
    print(f"Compressing data from {src} to {dst}")

cyclopts_app(["--help"])
# ╭─ Parameters ───────────────────────────────────────────────────────╮
# │ *  SRC,--src  File to compress. [required]                         │
# │    DST,--dst  Path to save compressed data to. [default: out.zip]  │
# ╰────────────────────────────────────────────────────────────────────╯

Additionally, if the default value is None <https://docs.python.org/3/library/constants.html#None>, cyclopts's default configuration will not display [default: None]. Doing so doesn't convey much meaning to the end-user. Typically None <https://docs.python.org/3/library/constants.html#None> is a sentinel value who's true value gets set inside the function.

Additionally, the cleaner, docstring-centric way of writing this program with Cyclopts would be:

import cyclopts
from pathlib import Path

cyclopts_app = cyclopts.App()

@cyclopts_app.default()
def compress(src: Path, dst: Path = Path("out.zip")):
    """Compress a file.

    Parameters
    ----------
    src: Path
       File to compress.
    dst: Path
       Path to save compressed data to.
    """
    print(f"Compressing data from {src} to {dst}")

cyclopts_app(["--help"])
# ╭─ Parameters ───────────────────────────────────────────────────────╮
# │ *  SRC,--src  File to compress. [required]                         │
# │    DST,--dst  Path to save compressed data to. [default: out.zip]  │
# ╰────────────────────────────────────────────────────────────────────╯

Validation

Typer has builtin argument validation for certain type annotations.

import typer

typer_app = typer.Typer()

@typer_app.command()
def foo(age: Annotated[int, typer.Argument(min=0)]):
    pass

This works for a select few builtins, but the Typer solution doesn't abstract out validation properly. Why does the generic typer.Argument have fields that only have meaning if the annotated type is a number? The typer.Argument signature has a ridiculous number of fields that only apply for certain types.

def Argument(
    # Parameter
    default: Optional[Any] = ...,
    *,
    callback: Optional[Callable[..., Any]] = None,
    metavar: Optional[str] = None,
    expose_value: bool = True,
    is_eager: bool = False,
    envvar: Optional[Union[str, List[str]]] = None,
    shell_complete: Optional[
        Callable[
            [click.Context, click.Parameter, str],
            Union[List["click.shell_completion.CompletionItem"], List[str]],
        ]
    ] = None,
    autocompletion: Optional[Callable[..., Any]] = None,
    # Custom type
    parser: Optional[Callable[[str], Any]] = None,
    # TyperArgument
    show_default: Union[bool, str] = True,
    show_choices: bool = True,
    show_envvar: bool = True,
    help: Optional[str] = None,
    hidden: bool = False,
    # Choice
    case_sensitive: bool = True,
    # Numbers
    min: Optional[Union[int, float]] = None,
    max: Optional[Union[int, float]] = None,
    clamp: bool = False,
    # DateTime
    formats: Optional[List[str]] = None,
    # File
    mode: Optional[str] = None,
    encoding: Optional[str] = None,
    errors: Optional[str] = "strict",
    lazy: Optional[bool] = None,
    atomic: bool = False,
    # Path
    exists: bool = False,
    file_okay: bool = True,
    dir_okay: bool = True,
    writable: bool = False,
    readable: bool = True,
    resolve_path: bool = False,
    allow_dash: bool = False,
    path_type: Union[None, Type[str], Type[bytes]] = None,
    # Rich settings
    rich_help_panel: Union[str, None] = None,
) -> Any:
    ...

Cyclopts has an explicit validator <#cyclopts.Parameter.validator> field that accepts a function:

from cyclopts import App, parameter
from typing import Annotated

cyclopts_app = App()

def age_validator(type_, value: int):
    if value < 0:
        raise ValueError

@cyclopts_app.command()
def foo(age: Annotated[int, Parameter(validator=age_validator)]):
    pass

 cyclopts_app()

This solution is similar to how other libraries, like Attrs <https://www.attrs.org/en/stable/examples.html#validators> or Pydantic <https://docs.pydantic.dev/latest/concepts/validators/>, perform validation.

Cyclopts has builtin validators for common use-cases.

# Typer
typer.Argument(file_okay=True, exists=True)

# Cyclopts
cyclopts.Parameter(validator=cyclopts.validators.Path(file_okay=True, exists=True))

Union/Optional Support

Currently, Typer does not support Union <https://docs.python.org/3/library/typing.html#typing.Union> type annotations.

import typer

typer_app = typer.Typer()

@typer_app.command()
def foo(value: Union[int, str] = "default_str"):
    print(f"{type(value)=} {value=}")

typer_app(["123"])
# AssertionError: Typer Currently doesn't support Union types

Cyclopts fully supports Union <https://docs.python.org/3/library/typing.html#typing.Union> annotations. Cyclopt's Coercion Rules <#coercion-rules-union> iterate left-to-right over the unioned types until a coercion can be performed without error.

import cyclopts

cyclopts_app = cyclopts.App()

@cyclopts_app.default
def foo(value: Union[int, str] = "default_str"):
    print(f"{type(value)=} {value=}")

print("Cyclopts:")
cyclopts_app(["123"])
# type(value)=<class 'int'> value=123
cyclopts_app(["bar"])
# type(value)=<class 'str'> value='bar'

Naturally, Cyclopts also supports Optional <https://docs.python.org/3/library/typing.html#typing.Optional> types, since Optional <https://docs.python.org/3/library/typing.html#typing.Optional> is syntactic sugar for Union[..., None].

Adding a Version Flag

It's common to check a CLI app's version via a --version flag.

Concretely, we want the following behavior:

$ mypackage --version
1.2.3

To achieve this in Typer, we need the following bulky implementation <https://github.com/tiangolo/typer/issues/52>:

import typer
from typing import Annotated

typer_app = typer.Typer()

def version_callback(value: bool):
    if value:
        print("1.2.3")
        raise typer.Exit()

@typer_app.callback()
def common(
    version: Annotated[
        bool,
        typer.Option(
            "--version",
            callback=version_callback,
            help="Print version.",
        ),
    ] = False,
):
    print("Callback body executed.")

print("Typer:")
typer_app(["--version"])
# 1.2.3

Not only is this a lot of boilerplate, but it also has some nasty side-effects, such as impacting whether or not you need to specify the command in a single-command program. <../default_command/README.html> On top of that, it's not very intuitive. Would you expect "Callback body executed." to be printed? When does version_callback get called? What is value?

With Cyclopts, the version is automatically detected by checking the version of the package instantiating App <#cyclopts.App>. If you prefer explicitness, version <#cyclopts.App.version> can also be explicitly supplied to App <#cyclopts.App>.

import cyclopts

cyclopts_app = cyclopts.App(version="1.2.3")
cyclopts_app(["--version"])
# 1.2.3

Documentation

Documentation is a major component of any library.

Typer's documentation contains many good tutorials and demonstrations on how to use the library, but has very little information on the API itself.

Frequently the only way to discover options and behavior is to dive into the source code. This becomes further confusing as the lines of where Typer ends and Click begins is quite blurred.

Cyclopts has a full API <#api> page, containing all the configurable options and defined behaviors in a single place.

Fire Comparison

Fire <https://github.com/google/python-fire> is a CLI parsing library by Google that attempts to generate a CLI from any Python object. To that end, I think Fire definitely achieves its goal. However, I think Fire has too much magic, and not enough control.

From the Fire documentation <https://github.com/google/python-fire/blob/master/docs/guide.md#argument-parsing>:

The types of the arguments are determined by their values, rather than by the function signature where they're used. You can pass any Python literal from the command line: numbers, strings, tuples, lists, dictionaries, (sets are only supported in some versions of Python). You can also nest the collections arbitrarily as long as they only contain literals.

Essentially, Fire ignores type hints and parses CLI parameters as if they were python expressions.

import fire


def hello(name: str = "World"):
    print(f"{name=} {type(name)=}")


if __name__ == "__main__":
    fire.Fire(hello)
$ my-script foo
name='foo' type(name)=<class 'str'>

$ my-script 100
name=100 type(name)=<class 'int'>

$ my-script true
name='true' type(name)=<class 'str'>

$ my-script True
name=True type(name)=<class 'bool'>

The equivalent in Cyclopts:

import cyclopts


app = cyclopts.App()


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


if __name__ == "__main__":
    app()
$ my-script foo
name='foo' type(name)=<class 'str'>

$ my-script 100
name='100' type(name)=<class 'str'>

$ my-script true
name='true' type(name)=<class 'str'>

$ my-script True
name='True' type(name)=<class 'str'>

Fire is fine for some quick prototyping, but is not suitable for a serious CLI. Therefore, I wouldn't say Fire is a direct competitor to Cyclopts.

Arguably Comparison

Arguably <https://treykeown.github.io/arguably/> is another Typer-inspired type-annotation-based CLI library. Arguably was created in response to the overly intrusive nature of Typer, with the goal of minimizing clutter and maintaining code simplicity. Like Cyclopts, Arguably mostly skirts using Annotated by interpreting as much data as possible from the function docstring. Unlike the Typer comparison <#typer-comparison>, many of the topics in this section are simply comparing/contrasting with Arguably, rather than claiming to be strictly better.

Global State

Unlike Cyclopts or Typer, with arguably you directly jump into decorating functions:

import arguably


@arguably.command
def some_function(required, not_required=2, *others: int, option: float = 3.14):
    """
    this function is on the command line!

    Args:
        required: a required argument
        not_required: this one isn't required, since it has a default value
        *others: all the other positional arguments go here
        option: [-x] keyword-only args are options, short name is in brackets
    """
    print(f"{required=}, {not_required=}, {others=}, {option=}")


if __name__ == "__main__":
    arguably.run()

With Arguably, no application object is created. This immediately becomes an issue if you use a library that uses arguably on import.

Lets consider the following file:

# library_using_arguably.py
import arguably


@arguably.command
def some_library_function(name):
    print(f"{name=}")


if __name__ == "__main__":
    arguably.run()
$ python library_using_arguably.py foo
name='foo'

So this by itself works fine, but lets create another script that imports this library:

import arguably
import library_using_arguably


@arguably.command
def my_function(name):
    print(f"{name=}")


if __name__ == "__main__":
    arguably.run()

Now, lets check the help screen:

$ python my-script.py --help
usage: my-script.py [-h] command ...

positional arguments:
  command
    some-library-function
    my-function

options:
  -h, --help               show this help message and exit

The two CLI applications got combined into one, making Arguably dangerous for CLIs that are also libraries.

Subcommands

Arguably parses the command tree based on __ delimited function names.

import arguably


@arguably.command
def ec2__start_instances(*instances):
    """Start instances.

    Args:
        *instances: {instance}s to start
    """
    for inst in instances:
        print(f"Starting {inst}")


@arguably.command
def ec2__stop_instances(*instances):
    """Stop instances.

    Args:
        *instances: {instance}s to stop
    """
    for inst in instances:
        print(f"Stopping {inst}")


if __name__ == "__main__":
    arguably.run()
$ python main.py ec2 --help
positional arguments:
   command
     start-instances  start instances.
     stop-instances   stop instances.

Cyclopts handles the command tree by creating and registering recursive App <#cyclopts.App> objects:

from cyclopts import App

app = App()
ec2 = app.command(App(name="ec2"))


@ec2.command
def start_instances(*instances):
    """Start instances.

    Args:
        *instances: {instance}s to start
    """
    for inst in instances:
        print(f"Starting {inst}")


@ec2.command
def stop_instances(*instances):
    """Stop instances.

    Args:
        *instances: {instance}s to stop
    """
    for inst in instances:
        print(f"Stopping {inst}")


if __name__ == "__main__":
    app()
$ python main.py ec2 --help
╭─ Commands ───────────────────────────────────────────────────────────╮
│ start-instances  start instances.                                    │
│ stop-instances   stop instances.                                     │
╰──────────────────────────────────────────────────────────────────────╯

Author

Brian Pugh

Info

Mar 05, 2026 4.7.0