CLI tool
BrianPugh/cyclopts avatar
BrianPugh/cyclopts

Cyclopts reads your docstring and its numpy-style Parameters block to write the help page

Intuitive, easy CLIs based on python type hints.

1,281 stars64 forksPythonApache-2.0

At a glance

What is it?
Cyclopts is a Python CLI framework that derives everything from type hints and docstrings, so a 29-line function with a numpy-style Parameters section produces a help page, a choices list, a default and a version flag with no extra code. It requires Python 3.11, and the README makes its case against Typer with a side-by-side 29-line versus 41-line implementation of the same command.
Who is it for?
Cyclopts fits a Python team writing a CLI with structured data where the docstring already documents the parameters, since the help output is then free and the type hints do the validation work. Do not adopt it on Python 3.10 or older, because the declared floor is Python 3.11.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole quick start is a decorated function

The shortest useful program in the README has no parser object, no argument object and no help text assembly.

python
from cyclopts import run

def foo(loops: int):
    for i in range(loops):
        print(f"Looping! {i}")

run(foo)

And it runs:

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

The parameter `loops` is annotated `int`, becomes a positional argument, and the integer 3 is parsed from the command line. There is no `--help` written by hand because there is nothing to write.

The moment you need more than one command, the surface grows by two decorators rather than by a class hierarchy. `cyclopts.App` is the application, `@app.command` registers a subcommand, and `@app.default` registers what happens when no subcommand is given:

python
from cyclopts import App

app = App()

@app.command
def foo(loops: int):
    for i in range(loops):
        print(f"Looping! {i}")

@app.default
def default_action():
    print("Hello world! This runs when no command is specified.")

app()

Running it with no arguments takes the default branch, and running `demo.py foo 3` dispatches to the command. That is the entire structural difference between a one-function script and a multi-command CLI, which is the ergonomic claim made concrete.

The description calls it an intuitive and easy-to-use CLI framework aiming for an intuitive and efficient developer experience, and the topic list is `argument-parser`, `cli`, `python`, `shell` and `typehints`, which is a fair summary of where it sits.

Where the help page actually comes from

The claim in the feature list is that help pages are generated automatically from docstrings and other contextual data, and the demonstration that makes it concrete is the deploy example.

The complete application is described as 29 lines; its deploy function includes this docstring:

python
@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.
    """

Two details in that signature produce most of what you see in the output. The `Literal` on `env` produces a choices list, and the union `int | Literal["default", "performance"]` with a default of `"default"` produces a parameter that accepts either a number or one of two words.

The generated help is:

console
โ•ญโ”€ 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]                                                                โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

So the description text comes from the docstring's Parameters section, the choices from the Literal, the default from the signature, and the required marker from the absence of a default. Nothing about that output is written twice.

The docstring format is numpy style, with a `Parameters` header and a dashed underline. The dependency `docstring-parser` at `>=0.15,<4.0` is what reads it, and `rich-rst` and `rich` are what draw the box-drawing output. If your project documents functions in Google style, the parser is capable of more than one convention but the README only demonstrates this one, and the help you get depends on which you wrote.

The same example shows the error path, which reports the conversion that failed and names both the offending value and the target type:

console
โ”‚ Error converting value "nonexistent-env" to typing.Literal['dev', 'staging', 'prod'] for "--env".  โ”‚

That is a genuinely useful error message, and it costs nothing to get because the type annotation was already there.

The Typer comparison is a union type, and 12 lines

The README's comparison section opens with a sentence rather than a table: Cyclopts is what you thought Typer was.

The stated differences are that Cyclopts includes information from docstrings, supports more complex types including Unions, and includes proper validation support. A complete comparison is linked in the documentation.

The worked example makes the point concretely. The README states that in its current state this application would be impossible to implement in Typer, because Typer does not support the union type hint of `replicas`.

The Typer version is given at 41 lines against Cyclopts's 29, and the extra lines are almost entirely machinery. There is a `replica_parser` function that branches on the string to convert it to 10, 20 or an integer, because a parser has to be written by hand for a type Typer cannot express. The Typer example defines a `_version_callback` that prints the version and raises `typer.Exit()`, and wraps both parameters in `Annotated` with explicit `typer.Argument` calls carrying `help=` text.

Compare the resulting help output. Typer's version shows `env` and `replicas` under an `Arguments` heading with no choices, and puts `--help` under a separate `Options` heading. Cyclopts's version shows a single `Parameters` panel with choices, defaults and descriptions inline, and it also handles `--version` for free, printing `0.0.0`.

Two observations worth keeping separate. The union support is a genuine capability difference rather than a matter of taste. The help presentation is a matter of taste, and a team that has standardised on Typer's layout is not going to switch for boxes.

The honest framing is that Cyclopts is a younger project making a narrower set of assumptions, and the comparison is written by the Cyclopts author.

Python 3.11 is the floor, and four dependencies carry it

The manifest states `requires-python = ">=3.11"`, and that is a hard gate rather than a recommendation.

The deploy example uses the PEP 604 union syntax in `replicas: int | Literal["default", "performance"]`, while the manifest independently sets the minimum supported Python version to 3.11.

Classifiers cover 3.11, 3.12, 3.13 and 3.14, and the OS classifier is Independent.

The four runtime dependencies explain what the library does. `attrs>=23.1.0` is how it handles attribute classes and, together with the feature list's mention of Attrs, how it accepts user-specified types. `rich>=13.6.0` draws the help output, which is the box-drawing panel you see rather than plain text. `rich-rst>=2.0.1,<3` is also listed as a runtime dependency. And `docstring-parser>=0.15,<4.0` reads the docstrings, with the upper bound suggesting the parser API has changed across majors.

That is a modest dependency list for a CLI framework, and notably it does not list Click or Typer as runtime dependencies.

The optional extras are worth scanning because they tell you what the integrations cost. `trio` adds trio support, `yaml` adds pyyaml, `docs` pins a wide set of Sphinx extensions with upper bounds, and `mkdocs` is a separate extra with mkdocs and pymdown-extensions. There are two extras beyond that which are unusual: an entry point registering `cyclopts.ext.mkdocs:CycloptsPlugin` as an `mkdocs.plugins` plugin, which means Cyclopts can render its own CLI reference inside a documentation site, and a console script `cyclopts` pointing at `cyclopts.cli:app`.

The build backend is hatchling with hatch-vcs, so the version is derived from git rather than written in the file, and the repository carries a `uv.lock` and a `.pre-commit-config.yaml`.

What it is for, and where to look first

Cyclopts is for writing a command line interface where the arguments are structured data, which covers most internal tooling, most developer tools and a good deal of scripting glue. The type hints you already write do the parsing, the validation and the help text.

The feature list names extensibility directly: easily customize converters, validators, token parsing and application launching. That is the migration path when the default behaviour is wrong for you, and it is the reason to prefer a library you can amend over one you must work around.

Type support is broader than builtins. The feature list states full support of all builtin types and user-specified ones including Pydantic, Dataclasses and Attrs, and the dev dependencies confirm Pydantic is exercised in the test suite rather than merely claimed. If your project already uses Pydantic models as its domain objects, a command can take a model directly and the CLI surface becomes the model's fields.

Three situations argue against it.

A machine-oriented interface where flags are positional strings regardless of what the code means is a case where type-driven parsing buys nothing.

A project on Python 3.10 or older is simply excluded, and adding a second CLI stack alongside an existing one to satisfy a floor is rarely worth it.

A team that has standardised on Typer or Click and has learned its help layout, its error format and its testing approach has a migration cost that the README's line count comparison does not measure. The comparison is about writing the code, not about maintaining an existing codebase.

What to check first, in order: confirm your docstring convention is one Cyclopts parses, since the help output depends entirely on it; then confirm your type annotations are the ones you want to be the interface, because they will be, whether or not that was the intention; then read the version handling, because `dynamic = ["version"]` means the `--version` output in the README example is a placeholder rather than your project's version.

Licensing is Apache-2.0 in the manifest with a `LICENSE` file at the root, which is permissive and appropriate for both internal and redistributed tools.

Editorial conclusion

Cyclopts fits a Python team writing a CLI with structured data where the docstring already documents the parameters, since the help output is then free and the type hints do the validation work. Do not adopt it on Python 3.10 or older, because the declared floor is Python 3.11. Verify first that your help text is in a format Cyclopts can parse, since the Parameters section in the README uses numpy style rather than Google style and a project documenting its CLI in another convention will get less out of it than it expects.

Frequently asked questions

How do I install Cyclopts?

With `pip install cyclopts`. Cyclopts requires Python 3.11 or newer, and the classifiers cover 3.11 through 3.14.

How do I create a multi-command CLI with Cyclopts?

Create a `cyclopts.App`, register commands with `@app.command` and what runs with no subcommand using `@app.default`, then call `app()`. The README's demo shows `python demo.py` running the default and `python demo.py foo 3` running the command.

Where does Cyclopts get its help text?

From the function's docstring, in numpy style with a `Parameters` section, parsed by the docstring-parser dependency. Choices come from `Literal` annotations, defaults from the signature and the required marker from the absence of a default, so nothing is written twice.

How does Cyclopts compare to Typer?

The README says Cyclopts includes docstring information, supports more complex types including Unions and has proper validation. It gives a 29-line Cyclopts version against 41 lines in Typer for the same command, the difference being that Typer needs a hand-written parser function and an explicit version callback.

What types can I use as Cyclopts arguments?

All builtin types plus user-specified ones including Pydantic, Dataclasses and Attrs, according to the README's feature list. The dev dependencies include pydantic, so a Pydantic model can be taken as an argument directly.

What license is Cyclopts released under?

Apache-2.0, declared in pyproject.toml with the LICENSE file at the repository root. The build backend is hatchling with hatch-vcs, so the version is derived from git tags rather than written in the manifest.

Official sources

  1. BrianPugh/cyclopts on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/brianpugh-cyclopts.svg)](https://hysenlabs.com/projects/brianpugh-cyclopts)

Was this useful?