cyclopts review
Cyclopts turns annotated Python functions into command-line programs. Function parameters become positional arguments or options, type hints drive conversion and validation, and recognized docstring formats supply help text. It handles unions, Literal choices, dataclasses, attrs classes, and Pydantic models without routing through Click. Version 4.23.2 is a small typing release that adds type hints to help-copy methods; the measured install below used 4.23.1.
Cyclopts is a good fit for Python 3.10+ tools whose real complexity is typed parameters, generated help, and configuration sources. Keep argparse for small scripts, and stay with Click when the surrounding code already depends on its contexts or plugins.
We installed it
| Install | ✓ · 0.3s | 8 packages on disk · 10 MB |
| Import | ✓ | import cyclopts in 0.25s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does cyclopts install cleanly?
Yes. In a fresh container with an empty cache, pip install cyclopts finished in 0.3s, leaving 8 packages and 10 MB on disk. pip-audit reported no known vulnerabilities.
What does cyclopts need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import cyclopts succeeded in 0.25s, and the package ships py.typed for type checkers.
cyclopts or typer: which should you use?
typer: Use it when Click compatibility and a larger body of examples matter more than Cyclopts union handling. Cyclopts is a good fit for Python 3.10+ tools whose real complexity is typed parameters, generated help, and configuration sources.
When should you not use cyclopts?
The tool must run on Python 3.9 or older; Cyclopts requires Python 3.10 or newer
Use it if
- Your function signatures already describe the CLI and you want docstrings to provide parameter help without repeating each description in decorators
- Arguments include unions, Literal choices, nested dataclasses, attrs classes, or Pydantic models that should be converted before the command runs
- One application needs subcommands, grouped validation, environment values, and TOML or YAML configuration through the same parser
- You want to test a command by passing a token list to an App and asserting its Python return value
- The tool must run on Python 3.9 or older; Cyclopts requires Python 3.10 or newer
- Existing code or plugins depend on Click internals, Click contexts, or CliRunner; Cyclopts is a separate parser and those extensions do not carry over
- A tiny script should have no third-party CLI dependency; argparse is already in Python and avoids Cyclopts plus its installed dependency set
- Your team wants a slow-moving interface; the repository ships frequently and 4.23.2 followed 4.23.1 with another public typing adjustment
- Help text must remain independent of docstring formatting; Cyclopts parses numpydoc, Google, or reStructuredText conventions, so an unmatched or stale docstring affects the user-facing help
- Most maintainers know Click or Typer and cannot budget time for Cyclopts-specific groups, meta apps, config precedence, and parameter metadata
Setup reality
Our clean Python 3.12 install of Cyclopts 4.23.1 succeeded in 0.3 seconds. It left eight packages and 10 MB on disk. The package data reported 33 direct dependencies, requires Python 3.10 or newer, contains only Python code, and includes py.typed. Importing cyclopts took 0.25 seconds. pip-audit found no known vulnerabilities, and the installed metadata identifies the license as Apache Software License. The registry now lists 4.23.2.
There are no credentials or required config files. Start with run(function) for one command or an App when you need subcommands. Docstrings are part of the interface: keep parameter names synchronized and use a format the parser recognizes. Underscores become hyphens in option names, keyword-only parameters become options, and boolean options may gain a negative form. Check generated help in tests rather than discovering those rules after release.
Configuration sources are optional and ordered. CLI tokens take priority, while Env, Toml, Json, or Yaml sources supply missing values according to the order given to App. A misspelled optional config path can behave like an empty file unless you require its existence. YAML support needs the package extra. Treat secrets from environment variables as values to consume, not text to print in help or error logs.
The larger learning cost sits in Parameter, Group, and the meta App. Parameter owns names, converters, validators, and environment bindings. Group controls help sections and multi-parameter rules. A meta App handles options that must run before every subcommand. Pin the minor version for production tools and read its release notes before upgrading. Version 4.23.2 changes typing for help-copy methods rather than runtime parsing, but the fast release cadence still deserves controlled updates.
Patterns
Turn one function into a command run-one-function
from cyclopts import run
def resize(path: str, width: int, height: int = 1080):
"""Resize an image.
Parameters
----------
path
Source image.
width
Output width.
height
Output height.
"""
return path, width, height
run(resize)run() suits a single command. Move to App when you need subcommands, shared options, or custom help behavior.
Create an application with subcommands register-subcommands
from cyclopts import App
app = App(name='shipper', version='2.0.0')
@app.command
def build(target: str, *, clean: bool = False):
"""Build one target."""
@app.command
def deploy(environment: str):
"""Deploy the build."""
if __name__ == '__main__':
app()Function underscores become command-name hyphens. Running an App without a default command displays help.
Mix a number with named choices accept-union-choice
from typing import Literal
@app.command
def serve(
workers: int | Literal['auto', 'maximum'] = 'auto',
):
return workersUnion members are attempted in order. A broad str member placed first can consume input intended for a narrower converter.
Add a short alias and remove a negative flag customize-option
from typing import Annotated
from cyclopts import Parameter
@app.command
def sync(
*,
verbose: Annotated[bool, Parameter(alias='-v')] = False,
force: Annotated[bool, Parameter(negative=())] = False,
):
passBoolean parameters normally receive positive and negative spellings. negative=() suppresses the generated negative form.
Reject an invalid path or worker count validate-input
from pathlib import Path
from typing import Annotated
from cyclopts import Parameter, validators
@app.command
def ingest(
source: Annotated[Path, Parameter(validator=validators.Path(exists=True, dir_okay=False))],
workers: Annotated[int, Parameter(validator=validators.Number(gte=1, lte=32))] = 4,
):
passValidators run before the function. The user receives a parser error instead of a traceback from command code.
Layer environment and TOML values load-env-and-toml
import cyclopts
from cyclopts import App
app = App(config=[
cyclopts.config.Env('SHIPPER_'),
cyclopts.config.Toml(
'pyproject.toml',
root_keys=('tool', 'shipper'),
search_parents=True,
),
])The source list defines precedence for values absent from CLI tokens. Use must_exist=True when a missing config file should stop execution.
Expand a dataclass into options bind-dataclass
from dataclasses import dataclass
@dataclass
class Database:
host: str = 'localhost'
port: int = 5432
tls: bool = False
@app.command
def migrate(database: Database):
return databaseNested fields become dotted option names. Inspect command help to confirm the spelling callers must use.
Make authentication options exclusive group-exclusive-options
from typing import Annotated
from cyclopts import Group, Parameter, validators
auth = Group('Authentication', validator=validators.MutuallyExclusive())
@app.command
def fetch(
*,
token: Annotated[str | None, Parameter(group=auth)] = None,
use_netrc: Annotated[bool, Parameter(group=auth)] = False,
):
passA Group controls both the help panel and validation across its parameters. It is more than a display heading.
Parse options before every command add-global-options
@app.meta.default
def launch(*tokens: str, verbose: bool = False):
configure_logging(verbose)
return app(tokens)
if __name__ == '__main__':
app.meta()Call app.meta() as the program entry point. Calling app() directly bypasses these global options.
Test without starting a subprocess test-token-list
def test_build():
result = app(
['build', 'docs', '--clean'],
exit_on_error=False,
)
assert result == expectedexit_on_error=False turns parse failures into exceptions that the test can assert instead of SystemExit.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| typer | PyPI | Use it when Click compatibility and a larger body of examples matter more than Cyclopts union handling |
| click | PyPI | Use it when decorator-defined options, Click contexts, plugins, and CliRunner are established project conventions |
| argparse-dataclass | PyPI | Use it when standard argparse behavior is desired and a dataclass is the only extra abstraction needed |
| python-fire | PyPI | Use it for internal tools that should expose existing Python objects with very little CLI declaration |
More cli & tooling guides
commander · chalk · typescript · esbuild · yargs · click · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

