NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #335 most downloaded on PyPI
Intuitive, easy CLIs based on type hints.
Last release 4 days ago
01 Oct 2026
Ships on a steady schedule
a new release about every 2 weeks
Nearly every release is documented
notes for 60 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
3 years old
167 releases · first in 2023
Accept --flag=none on a bool | None annotation by @BrianPugh in #978
--flag=none on a bool | None annotation by @BrianPugh in #978Full Changelog: v5.1.0...v5.1.1
Support bool union'd with other types.
bool union'd with other types.
Full Changelog: v5.0.0...v5.1.0
One column per month.
Check out the new "Migrating to v5" docs page , covering each intentional breaking change with before/after examples.
Check out the new "Migrating to v5" docs page, covering each intentional breaking change with before/after examples.
All v4 features up to 4.25.3 have been merged into this release.
Dropped Python 3.10 support (Python 3.10 EoL is Oct 31, 2026) #889
Removed fuzzy command-matching. Command names must match exactly.
PascalCase → pascal-case name-transform change, retrying by stripping dashes/underscores on no exact match. Removed as cleanup for the major release.@app.command
def MyCommand(): ... # registers as "my-command"
# v4: `mycommand` fuzzy-matched -> my-command
# v5: `mycommand` no longer resolves; use `my-command`Fallthrough parsing: child wins. Previously a meta app claimed any keyword parameter it recognized regardless of token position — even after a subcommand, and even if the subcommand defined the same name. In v5 (default parse_mode="fallthrough"), when both levels define the same name, the subcommand wins for tokens placed after it.
@app.meta.default
def main(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
verbose: Annotated[bool, Parameter(alias="-v")] = False,
):
app(tokens)
@app.command
def greet(name: str, *, version: Annotated[bool, Parameter(alias="-v")] = False):
...$ myapp greet -v Alice # after the subcommand: CHANGED
# v4: meta verbose=True; greet version=False
# v5: meta verbose=False; greet version=True (child wins)
$ myapp -v greet Alice # before the subcommand: unchanged
# v4: meta verbose=True; greet version=False
# v5: meta verbose=True; greet version=False
Only placement after the subcommand changed. To reject parent-level parameters placed after a subcommand entirely, use parse_mode="strict".
Forwarded *tokens preserve the -- delimiter. A forwarding meta's raw-stream capture parameter (*tokens with allow_leading_hyphen=True) now keeps a user-typed end-of-options delimiter, so the re-parse inside app(tokens) still treats the trailing tokens as positional.
$ myapp sub -- -x
# v4: meta captured ("sub", "-x") -> Error: Unknown option: -x.
# v5: meta captured ("sub", "--", "-x") -> inner parse keeps -x positionalWrappers that manually re-inserted -- as a workaround will now see it twice — remove the workaround. Leaf commands (that don't forward) are unchanged: the delimiter is consumed as a marker and never appears in bound values.
A greedy *args subcommand claims all post-command tokens. A subcommand whose only positional parameter is *args with allow_leading_hyphen=True genuinely claims every token after the command; meta parameters placed after such a subcommand no longer bubble up.
@app.meta.default
def main(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
user: str = "default",
):
app(tokens)
@app.command
def sub(*args: Annotated[str, Parameter(allow_leading_hyphen=True)]):
...$ myapp sub --user=alice
# v4: meta user=alice; sub args=()
# v5: meta user=default; sub args=('--user=alice',)
Place meta parameters before the subcommand, or give the subcommand explicit keyword parameters instead of a catch-all.
User parameters shadow --help / --version. A command may now define its own parameter named like an auto-registered help/version flag; the token binds to the user's parameter instead of triggering the handler, and the command's help page lists the user's parameter.
@app.command
def sub(*, version: bool = False):
return version$ myapp sub --version
# v4: printed the app version
# v5: binds the version parameter to TrueShadowing is per-flag (--help still works when only --version is shadowed), and commands taking **kwargs are NOT affected — they keep automatic --help/--version. If a command shadows --help, a --version token in the same invocation triggers version printing (interception runs before binding).
cyclopts tree: -d now disables descriptions. Previously -d was a positive alias for --description — a no-op, since descriptions are on by default. It is now a negative alias (equivalent to --no-description), as originally intended.
$ cyclopts tree my_script.py -d
# v4: descriptions shown (flag was a no-op)
# v5: descriptions hiddenParse errors exit with code 2. CLI usage errors (unknown option, missing argument, coercion failure, ...) now exit with code 2, matching argparse and Click. Previously they exited with 1, indistinguishable from a command that ran and reported failure (the default result_action maps a False return to exit code 1). Application-level exit codes and 130 for keyboard interrupts are unchanged.
$ myapp --bogus; echo $?
# v4: 1
# v5: 2--version respects the -- delimiter. A --version token after the end-of-options delimiter is positional data, not a version request — matching how --help already behaved.
$ myapp -- --version
# v4: printed the app version (command never ran)
# v5: the command runs; "--version" is bound positionallyresult_action names are validated eagerly. An invalid action name now raises a descriptive ValueError (listing the valid actions) at App construction, attribute assignment, or invocation-time override — before any command executes. Previously an invalid name was silently accepted and only raised a bare, message-less ValueError at result-handling time, after the command had already run.
App(result_action="bogus") # ValueError, raised immediately
app(tokens, result_action="bogus") # ValueError, command never runsApp.parse_mode — hierarchical parameter scoping between meta apps and subcommands. Two modes:
"fallthrough" (default): meta parameters may appear anywhere in the token stream; post-command tokens the subcommand leaves unconsumed bubble up to the meta. When both levels define the same flag, the child wins."strict": parameters bind only at the command level where they appear — Click/Typer-style scoping. A meta parameter placed after a subcommand is rejected with a scope-aware hint (Did you mean to place it directly after "myapp"?), and help pages exclude parent meta parameters at the child level. Shell completion respects both modes.parse_mode is inherited by subapps unless explicitly overridden. See the new "Parse Mode" docs page.
Combined short options are resolved against the MERGED flag namespace of both levels — the person typing myapp cmd -xvf doesn't know which level implements each flag, so a single GNU left-to-right scan of the merged table routes each character to its owner (subcommand wins same-letter collisions; the first value-taking option absorbs the remainder or next token as its value; unknown characters are reported by the subcommand).
# meta owns -v and -u (value-taking); cmd owns -x and -f:
$ myapp cmd -xvf # -x -f -> cmd; -v -> meta
$ myapp cmd -xuroot # -x -> cmd; meta user="root" (attached value)
$ myapp cmd -xu alice # -x -> cmd; meta user="alice" (next token)Other cross-scope cases: a meta flag interleaved between a child option and its value binds at the meta while the child pairs its option with the value, and a meta option missing its value reports the real "requires an argument" error instead of a misleading "Unknown option".
Variable token-lengths within a Union. Members that consume a different number of tokens can now coexist. Order matters: the longer (multi-token) member should come first so it gets first claim on the tokens.
def main(value: tuple[int, int] | int): ...
# --value 5 -> 5
# --value 1 2 -> (1, 2)On v4 this raises Cannot Union types that consume different numbers of tokens.
list[Union[...]] with differing token-lengths. Each element is matched independently against the union members.
def main(coords: list[tuple[int, int] | int]): ...null/none (case-insensitive) parse to None. Applies to optional types. Union ordering decides the result: a member that legitimately accepts the literal string wins first.
def main(value: int | None): ... # --value none -> None
def main(path: Path | None): ... # --path none -> Path("none") (Path matches first)
def main(value: str | None): ... # --value none -> "none" (str matches first)
# works inside collections too: list[int | None] "1 none 3" -> [1, None, 3]ArgumentCollection.copy(), including copy(reset_tokens=True) for a copy whose Arguments share metadata but carry fresh empty token lists.
Dynamic per-parameter shell completion #917
Parameter.completer callable receives a CompletionContext and returns context-aware suggestions, so completions can depend on values only known at runtime (git branches, running containers, rows from a database). Works in bash/zsh/fish.def complete_user(ctx):
return load_users() # e.g. {"alice": "admin", "bob": "member"} -> {value: description}
@app.command
def promote(user: Annotated[str, Parameter(completer=complete_user)]): ...Parameter.metavar and type-derived value placeholders #919
--config PATH), separating a value's shape from a positional parameter's display identifier.Parameter.metavar to override the placeholder, or turn it off via the formatter. Closes #885.def main(config: Annotated[Path, Parameter(metavar="FILE")]): ... # shows "--config FILE"cyclopts.* Rich named style, so you can recolor any part of the help page by supplying your own theme; your overrides win over the built-in defaults.cyclopts.border), the usage line (cyclopts.usage), parameter/command names (cyclopts.name), and the metadata annotation styles ([default], [choices], [required], ...).Per-group help panel styling via Group.theme #916
Group can now carry its own theme (a dict or Rich Theme) to style just that group's panel independently of the rest of the help page.danger = Group("Danger Zone", theme={"cyclopts.border": "red", "cyclopts.name": "bright_red"})App.interactive_shell() improvements #941
intro banner (supports Rich markup; None uses the default banner, "" prints nothing).history option (True for a default history-file location, or a path) backed by readline.remap_flags so bare help/version words at the root map to their long flags.quit words and registered commands are handled more robustly, with registered commands and meta commands taking precedence over quit words.CHOICE metavar for choice parameters and collapse variadic-collection metavars to X... by @BrianPugh in #949rich-rst>=1.3.1,<3 to rich-rst>=2.0.1,<3.Full Changelog: v4.25.3...v5.0.0
fix(help): show CHOICE metavar for choice parameters and collapse variadic-collection metavars to X... by @BrianPugh in #949
CHOICE metavar for choice parameters and collapse variadic-collection metavars to X... by @BrianPugh in #949Full Changelog: v5.0.0b2...v5.0.0b3
Changes since v5.0.0b1 . All v4 features up to 4.25.2 have been merged into this release. This is hopefully the final beta release before v5.0.0 prope
Changes since v5.0.0b1. All v4 features up to 4.25.2 have been merged into this release. This is hopefully the final beta release before v5.0.0 proper.
Parameter.completer callable receives a CompletionContext and returns context-aware suggestions, so completions can depend on values only known at runtime (git branches, running containers, rows from a database). Works in bash/zsh/fish.def complete_user(ctx):
return load_users() # e.g. {"alice": "admin", "bob": "member"} -> {value: description}
@app.command
def promote(user: Annotated[str, Parameter(completer=complete_user)]): ...Parameter.metavar and type-derived value placeholders #919
--config PATH), separating a value's shape from a positional parameter's display identifier.Parameter.metavar to override the placeholder, or turn it off via the formatter. Closes #885.def main(config: Annotated[Path, Parameter(metavar="FILE")]): ... # shows "--config FILE"cyclopts.* Rich named style, so you can recolor any part of the help page by supplying your own theme; your overrides win over the built-in defaults.cyclopts.border), the usage line (cyclopts.usage), parameter/command names (cyclopts.name), and the metadata annotation styles ([default], [choices], [required], ...).Group.theme #916
Group can now carry its own theme (a dict or Rich Theme) to style just that group's panel independently of the rest of the help page.danger = Group("Danger Zone", theme={"cyclopts.border": "red", "cyclopts.name": "bright_red"})App.interactive_shell() improvements #941
intro banner (supports Rich markup; None uses the default banner, "" prints nothing).history option (True for a default history-file location, or a path) backed by readline.remap_flags so bare help/version words at the root map to their long flags.quit words and registered commands are handled more robustly, with registered commands and meta commands taking precedence over quit words.Full Changelog: v5.0.0b1...v5.0.0b2
Check out the new "Migrating to v5" docs page , covering each intentional breaking change with before/after examples.
Check out the new "Migrating to v5" docs page, covering each intentional breaking change with before/after examples.
Removed fuzzy command-matching. Command names must match exactly.
PascalCase → pascal-case name-transform change, retrying by stripping dashes/underscores on no exact match. Removed as cleanup for the major release.@app.command
def MyCommand(): ... # registers as "my-command"
# v4: `mycommand` fuzzy-matched -> my-command
# v5: `mycommand` no longer resolves; use `my-command`Fallthrough parsing: child wins. Previously a meta app claimed any keyword parameter it recognized regardless of token position — even after a subcommand, and even if the subcommand defined the same name. In v5 (default parse_mode="fallthrough"), when both levels define the same name, the subcommand wins for tokens placed after it.
@app.meta.default
def main(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
verbose: Annotated[bool, Parameter(alias="-v")] = False,
):
app(tokens)
@app.command
def greet(name: str, *, version: Annotated[bool, Parameter(alias="-v")] = False):
...$ myapp greet -v Alice # after the subcommand: CHANGED
# v4: meta verbose=True; greet version=False
# v5: meta verbose=False; greet version=True (child wins)
$ myapp -v greet Alice # before the subcommand: unchanged
# v4: meta verbose=True; greet version=False
# v5: meta verbose=True; greet version=False
Only placement after the subcommand changed. To reject parent-level parameters placed after a subcommand entirely, use parse_mode="strict".
Forwarded *tokens preserve the -- delimiter. A forwarding meta's raw-stream capture parameter (*tokens with allow_leading_hyphen=True) now keeps a user-typed end-of-options delimiter, so the re-parse inside app(tokens) still treats the trailing tokens as positional.
$ myapp sub -- -x
# v4: meta captured ("sub", "-x") -> Error: Unknown option: -x.
# v5: meta captured ("sub", "--", "-x") -> inner parse keeps -x positionalWrappers that manually re-inserted -- as a workaround will now see it twice — remove the workaround. Leaf commands (that don't forward) are unchanged: the delimiter is consumed as a marker and never appears in bound values.
A greedy *args subcommand claims all post-command tokens. A subcommand whose only positional parameter is *args with allow_leading_hyphen=True genuinely claims every token after the command; meta parameters placed after such a subcommand no longer bubble up.
@app.meta.default
def main(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
user: str = "default",
):
app(tokens)
@app.command
def sub(*args: Annotated[str, Parameter(allow_leading_hyphen=True)]):
...$ myapp sub --user=alice
# v4: meta user=alice; sub args=()
# v5: meta user=default; sub args=('--user=alice',)
Place meta parameters before the subcommand, or give the subcommand explicit keyword parameters instead of a catch-all.
User parameters shadow --help / --version. A command may now define its own parameter named like an auto-registered help/version flag; the token binds to the user's parameter instead of triggering the handler, and the command's help page lists the user's parameter.
@app.command
def sub(*, version: bool = False):
return version$ myapp sub --version
# v4: printed the app version
# v5: binds the version parameter to TrueShadowing is per-flag (--help still works when only --version is shadowed), and commands taking **kwargs are NOT affected — they keep automatic --help/--version. If a command shadows --help, a --version token in the same invocation triggers version printing (interception runs before binding).
cyclopts tree: -d now disables descriptions. Previously -d was a positive alias for --description — a no-op, since descriptions are on by default. It is now a negative alias (equivalent to --no-description), as originally intended.
$ cyclopts tree my_script.py -d
# v4: descriptions shown (flag was a no-op)
# v5: descriptions hiddenParse errors exit with code 2. CLI usage errors (unknown option, missing argument, coercion failure, ...) now exit with code 2, matching argparse and Click. Previously they exited with 1, indistinguishable from a command that ran and reported failure (the default result_action maps a False return to exit code 1). Application-level exit codes and 130 for keyboard interrupts are unchanged.
$ myapp --bogus; echo $?
# v4: 1
# v5: 2--version respects the -- delimiter. A --version token after the end-of-options delimiter is positional data, not a version request — matching how --help already behaved.
$ myapp -- --version
# v4: printed the app version (command never ran)
# v5: the command runs; "--version" is bound positionallyresult_action names are validated eagerly. An invalid action name now raises a descriptive ValueError (listing the valid actions) at App construction, attribute assignment, or invocation-time override — before any command executes. Previously an invalid name was silently accepted and only raised a bare, message-less ValueError at result-handling time, after the command had already run.
App(result_action="bogus") # ValueError, raised immediately
app(tokens, result_action="bogus") # ValueError, command never runsApp.parse_mode — hierarchical parameter scoping between meta apps and subcommands. Two modes:
"fallthrough" (default): meta parameters may appear anywhere in the token stream; post-command tokens the subcommand leaves unconsumed bubble up to the meta. When both levels define the same flag, the child wins."strict": parameters bind only at the command level where they appear — Click/Typer-style scoping. A meta parameter placed after a subcommand is rejected with a scope-aware hint (Did you mean to place it directly after "myapp"?), and help pages exclude parent meta parameters at the child level. Shell completion respects both modes.parse_mode is inherited by subapps unless explicitly overridden. See the new "Parse Mode" docs page.
Combined short options are resolved against the MERGED flag namespace of both levels — the person typing myapp cmd -xvf doesn't know which level implements each flag, so a single GNU left-to-right scan of the merged table routes each character to its owner (subcommand wins same-letter collisions; the first value-taking option absorbs the remainder or next token as its value; unknown characters are reported by the subcommand).
# meta owns -v and -u (value-taking); cmd owns -x and -f:
$ myapp cmd -xvf # -x -f -> cmd; -v -> meta
$ myapp cmd -xuroot # -x -> cmd; meta user="root" (attached value)
$ myapp cmd -xu alice # -x -> cmd; meta user="alice" (next token)Other cross-scope cases: a meta flag interleaved between a child option and its value binds at the meta while the child pairs its option with the value, and a meta option missing its value reports the real "requires an argument" error instead of a misleading "Unknown option".
Variable token-lengths within a Union. Members that consume a different number of tokens can now coexist. Order matters: the longer (multi-token) member should come first so it gets first claim on the tokens.
def main(value: tuple[int, int] | int): ...
# --value 5 -> 5
# --value 1 2 -> (1, 2)On v4 this raises Cannot Union types that consume different numbers of tokens.
list[Union[...]] with differing token-lengths. Each element is matched independently against the union members.
def main(coords: list[tuple[int, int] | int]): ...null/none (case-insensitive) parse to None. Applies to optional types. Union ordering decides the result: a member that legitimately accepts the literal string wins first.
def main(value: int | None): ... # --value none -> None
def main(path: Path | None): ... # --path none -> Path("none") (Path matches first)
def main(value: str | None): ... # --value none -> "none" (str matches first)
# works inside collections too: list[int | None] "1 none 3" -> [1, None, 3]ArgumentCollection.copy(), including copy(reset_tokens=True) for a copy whose Arguments share metadata but carry fresh empty token lists.
rich-rst>=1.3.1,<3 to rich-rst>=2.0.1,<3.v5 changes up to this point. Most notably in this release we re-introduced the rich-rst dependency now that licensing issues have been resolved.
v5 changes up to this point. Most notably in this release we re-introduced the rich-rst dependency now that licensing issues have been resolved.
Removed fuzzy command-matching. Command names must match exactly.
PascalCase → pascal-case name-transform change, retrying by stripping dashes/underscores on no exact match. Removed as cleanup for the major release.@app.command
def MyCommand(): ... # registers as "my-command"
# v4: `mycommand` fuzzy-matched -> my-command
# v5: `mycommand` no longer resolves; use `my-command`
Variable token-lengths within a Union. Members that consume a different
number of tokens can now coexist. Order matters: the longer (multi-token)
member should come first so it gets first claim on the tokens.
def main(value: tuple[int, int] | int): ...
# --value 5 -> 5
# --value 1 2 -> (1, 2)
On v4 this raises Cannot Union types that consume different numbers of tokens.
list[Union[...]] with differing token-lengths. Each element is matched
independently against the union members.
def main(coords: list[tuple[int, int] | int]): ...
null/none (case-insensitive) parse to None. Applies to optional
types. Union ordering decides the result: a member that legitimately accepts
the literal string wins first.
def main(value: int | None): ... # --value none -> None
def main(path: Path | None): ... # --path none -> Path("none") (Path matches first)
def main(value: str | None): ... # --value none -> "none" (str matches first)
# works inside collections too: list[int | None] "1 none 3" -> [1, None, 3]
rich-rst>=1.3.1,<3 to rich-rst>=2.0.1,<3.## What's Changed * Changes up to v4.10.1 Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v5.0.0a5...v5.0.0a6
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v5.0.0a5...v5.0.0a6
## What's Changed * Changes up to v4.5.4 Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v5.0.0a4...v5.0.0a5
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v5.0.0a4...v5.0.0a5
Change token-parsing/coercion logic to now allow variable token-lengths within unions.
null or none to be parsed into the python object None.Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v5.0.0a3...v5.0.0a4
v5 stable is probably a few months away, but performing this very early pre-release as it may help out certain individuals.
v5 stable is probably a few months away, but performing this very early pre-release as it may help out certain individuals.
name_transform function. The fuzzy-matching made the change backward compatible, but it not a good long-term solution. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/673rich-rst from being a required dependency. It is only necessary if using App.help_format="rst". Rich-rst has docutils as a subdependency, which has some less straight-forward licensing, so making this dependency be optional is nice for compliance. It can be installed via pip install cyclopts[rst]. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/674Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.2.1...v5.0.0a1
Fix nested json-object parsing by routing JSON-object tokens through the Argument layer ( #953 ) by @BrianPugh in #954
Argument layer (#953) by @BrianPugh in #954set/frozenset by @BrianPugh in #956Full Changelog: v4.25.2...v4.25.3
reject NaN in numeric range validators by @kudala-bharani in #942
Full Changelog: v4.25.1...v4.25.2
Keep allow_leading_hyphen=True positional values intact when a later char matches a short option by @BrianPugh in #934
allow_leading_hyphen=True positional values intact when a later char matches a short option by @BrianPugh in #934special thanks to @yilei for identifying these bugs!
Full Changelog: v4.25.0...v4.25.1
Interactive shell no longer exits on Ctrl-C or malformed input by @BrianPugh in #925
Full Changelog: v4.24.0...v4.25.0
Add Parameter.choices by @BrianPugh in #921
Parameter.choices by @BrianPugh in #921Config sources by @BrianPugh in #918"sys_exit_if_non_zero_else_return" result action and document result_action modes by @BrianPugh in #907validate_by_name is set by @xyf5432 in #909Full Changelog: v4.23.3...v4.24.0
Fix deprecated not being rendered by @wallento in #900
Full Changelog: v4.23.2...v4.23.3
Add type-hints to help copy methods by @Ichunjo in #899
Honor a dict value type's Parameter annotation by @BrianPugh in #890
Number, Path, and Slice validators silently skipping set, frozenset, and dict elements by @BrianPugh in #891Full Changelog: v4.23.0...v4.23.1
Performing this as a minor release as it may break setups who rely on strict snapshot testing.
Performing this as a minor release as it may break setups who rely on strict snapshot testing.
Full Changelog: v4.22.5...v4.23.0
Various fixes around abstract collection types:
Various fixes around abstract collection types:
TypeError when an abstract collection parameter is given no values by @BrianPugh in #881Collection, Container, and Reversible type hints by @BrianPugh in #882--empty-* flags for abstract collection hints by @BrianPugh in #883Full Changelog: v4.22.4...v4.22.5
Singularize the ConsumeMultipleError noun when the count is 1 by @BrianPugh in #879
ConsumeMultipleError noun when the count is 1 by @BrianPugh in #879Full Changelog: v4.22.3...v4.22.4
Fix parsing Enum types with __init__ defined. by @BrianPugh in #877
Enum types with __init__ defined. by @BrianPugh in #877Full Changelog: v4.22.2...v4.22.3
Fix spurious MissingArgumentError for explicitly-supplied empty mappings by @BrianPugh in #871
MissingArgumentError for explicitly-supplied empty mappings by @BrianPugh in #871=value on Parameter.count=True flags instead of silently dropping it by @BrianPugh in #872Full Changelog: v4.22.1...v4.22.2
Split iterable Parameter(env_var=...) values via env_var_split by @chuenchen309 in #866
Parameter(env_var=...) values via env_var_split by @chuenchen309 in #866=value by @chuenchen309 in #865Full Changelog: v4.22.0...v4.22.1
Fix config search_parents=False still walking parent directories by @chuenchen309 in #864
search_parents=False still walking parent directories by @chuenchen309 in #864timedelta strings instead of silently ignoring garbage by @chuenchen309 in #863-0xFF, +0o17) by @chuenchen309 in #862Full Changelog: v4.21.2...v4.22.0
Fix -- delimiter leaking into an "Unknown option" error by @chuenchen309 in #861
-- delimiter leaking into an "Unknown option" error by @chuenchen309 in #861Full Changelog: v4.21.1...v4.21.2
complete zsh subcommands after meta positionals by @Sanjays2402 in https://github.com/BrianPugh/cyclopts/pull/860
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.21.0...v4.21.1
Add Parameter.negative_alias to append extra names to a flag's negative form (e.g. a short -d) without dropping the generated --no-*/--empty-* names.
Parameter.negative_alias to append extra names to a flag's negative form (e.g. a short -d) without dropping the generated --no-*/--empty-* names. Mirrors name/alias. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/859#857: a dataclass command with a default_factory field could raise a spurious "Input should be a valid list" validation error simply because pydantic was imported anywhere in the program. Factory defaults are now invoked during introspection, so the produced value (rather than the <factory> sentinel) is used as the default and shown in help text — consistent across dataclass, attrs, and pydantic. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/858Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.20.0...v4.21.0
Add cyclopts tree command and App.command_tree() by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/845
cyclopts tree command and App.command_tree() by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/845consume_multiple to nested structured-type fields by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/846Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.19.0...v4.20.0
New Attribute: Parameter.short_alias to automatically generate short aliases for parameters. by @mgielda in https://github.com/BrianPugh/cyclopts/pull
Parameter.short_alias to automatically generate short aliases for parameters. by @mgielda in https://github.com/BrianPugh/cyclopts/pull/831ArgumentCollection.filter_by(missing=...) for required/conditionally-required fields. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/840Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.18.0...v4.19.0
Added native slice support by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/835
slice support by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/835
slice type-hints are now properly handled.cyclopts.validators.Slicecyclopts.types.NonEmptySlice convenience type.Parameter metadata in nested Annotated/Optional/NewType types by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/838Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.17.0...v4.18.0
handle async commands in default_dispatcher for interactive shells by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/827
Parameter.show_default to be a string to display. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/833Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.16.1...v4.17.0
Fix fish completion for positional args and run fish tests in ci by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/823
prog_name matches a builtin helper by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/825Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.16.0...v4.16.1
Return-value objects can now declare their own exit code by defining a __cyclopts_returncode__ method, which is honored by all built-in result_action
__cyclopts_returncode__ method, which is honored by all built-in result_action handlers. Custom result_action callables can opt in via the new cyclopts.resolve_returncode helper. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/818**kwargs is annotated as Unpack[SomeTypedDict], the TypedDict's fields are promoted to top-level CLI options instead of the generic --[KEYWORD] catch-all. Per-field Required / NotRequired markers are respected independently of the parent kwargs argument. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/819CycloptsError.msg now accepts a rich.Text instance. Cyclopts' error-message style palette is exposed as cyclopts.exceptions.STYLE_OFFENDING_VALUE, STYLE_NAME, STYLE_VALID_CHOICE, STYLE_SUGGESTION, and STYLE_SOURCE . By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/814Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.15.0...v4.16.0
AddedApp.synonym, which declares alternate command name(s) that trigger a "Did you mean..." suggestion without registering the command under those nam
App.synonym, which declares alternate command name(s) that trigger a "Did you mean..." suggestion without registering the command under those names. Useful when a user types a semantically-equivalent word (e.g. remove vs uninstall) that the built-in fuzzy matcher would miss because the words aren't spelled similarly. Accepts a single str or an iterable of strings. Synonyms are not runnable and are hidden from --help.@app.command(synonym=["remove", "rm"])
def uninstall(name: str): ...
$ my-script remove mypackage
Unknown command "remove". Did you mean "uninstall"? Available commands: uninstall.
If multiple commands declare the same synonym, all matching commands are suggested (Did you mean "uninstall" or "purge"?).
By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/815Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.14.1...v4.15.0
use trusted publishing when uploading to pypi by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/812
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.14.0...v4.14.1
Increase rich-rst version constraint upper-bound from <2.0.0 to <3.0.0 by @germa89 in https://github.com/BrianPugh/cyclopts/pull/810
<2.0.0 to <3.0.0 by @germa89 in https://github.com/BrianPugh/cyclopts/pull/810Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.13.0...v4.14.0
Friendlier error message for enum/literal choices by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/806
CycloptsError printing. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/808Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.12.0...v4.13.0
Allow for validators to be string(s) (class method forward reference) by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/803
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.11.2...v4.12.0
Fix _should_attempt_json_list for detecting list[str] | None and Annotated annotations by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/800
_should_attempt_json_list for detecting list[str] | None and Annotated annotations by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/800Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.11.1...v4.11.2
Include meta-apps when assembling usage string by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/793
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.11.0...v4.11.1
Added usage_name override for configuring docs creation by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/791
usage_name override for configuring docs creation by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/791dict[str, dataclass-like] params by @wrongbad in https://github.com/BrianPugh/cyclopts/pull/787is_annotated over get_origin by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/792Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.10.2...v4.11.0
NormFloat - A float in the range [0, 1]
NormFloat - A float in the range [0, 1]SignedNormFloat - A float in the range [-1, 1]PercentInt - An int in the range [0, 100]Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.10.1...v4.10.2
Fix ValueError when parsing pre-release Pydantic versions by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/775
ValueError when parsing pre-release Pydantic versions by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/775Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.10.0...v4.10.1
New App.error_formatter field to control out CycloptsError are displayed. The formatter receives a CycloptsError and returns any rich-printable object
New App.error_formatter field to control out CycloptsError are displayed. The formatter receives a CycloptsError and returns any rich-printable object, replacing the default CycloptsPanel. Can be set at app creation or passed as a runtime override to parse_args / __call__ / run_async. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/771
By default (without error_formatter), errors are displayed in a Rich panel:
$ my-app foo
╭─ Error ─────────────────────────────────────────────────╮
│ Invalid value "foo" for "VALUE": unable to convert │
│ "foo" into int. │
╰─────────────────────────────────────────────────────────╯
With a custom formatter, you can simplify or restyle the output:
from cyclopts import App, CycloptsError
def my_error_formatter(e: CycloptsError):
return f"[bold red]error:[/bold red] {e}"
app = App(error_formatter=my_error_formatter)
@app.default
def main(value: int):
pass
app()
$ my-app foo
error: Invalid value "foo" for "VALUE": unable to convert "foo" into int.
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.9.0...v4.10.0
Parameter.consume_multiple now accepts int or tuple[int, int] for min/max element bounds.
Parameter.consume_multiple now accepts int or tuple[int, int] for min/max element bounds.
int sets a minimum (e.g. consume_multiple=2 requires at least 2 values).consume_multiple=(1, 3) requires 1–3 values).ConsumeMultipleError.# Require 1–3 space-separated values: --files a.txt b.txt c.txt
files: Annotated[list[str], Parameter(consume_multiple=(1, 3))]
By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/764
New field Parameter.allow_repeating controls whether an option can be specified multiple times.
False raises RepeatArgumentError on repeat (useful with consume_multiple).True allows repeats for any type; scalars use last-wins semantics.None (default) preserves existing behavior: lists accumulate, scalars error.# Allow --files a b c, but not --files a --files b
files: Annotated[list[str], Parameter(consume_multiple=True, allow_repeating=False)]
# Last value wins: --color red --color blue → "blue"
color: Annotated[str, Parameter(allow_repeating=True)]
By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/768
New field App.help_prologue displays text before the "Usage" line in help output.
Inherited by subcommands; override per-command or set to "" to disable.
app = App(help_prologue="myapp v1.0.0 — https://example.com")
By @tahv in https://github.com/BrianPugh/cyclopts/pull/769
a b --bar 8 d now correctly errors instead of silently assigning d to the positional list. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/766Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.8.0...v4.9.0
Lazy Loading --help improvements.
Lazy Loading --help improvements.
Previously, running --help on a parent command would import and resolve all lazy child commands, negating much of the startup-time benefits. Now, parent --help displays lazy commands without triggering any imports.
To show descriptions for lazy commands in --help output, provide help= at registration time:
app.command("myapp.commands:deploy", help="Deploy the application.")
Other metadata (group=, show=, sort_key=) can also be provided at registration time and will be used for help display without resolving the command.
Lazy commands are now only resolved when:
--help is requested (e.g., myapp deploy --help)app["command_name"]Thanks to @zzstoatzz for the initial contribution in #757.
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.7.0...v4.8.0
Better "just works" shell-completion install location for oh-my-zsh users. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/758
list[ExistingFile]). by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/760Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.6.0...v4.7.0
Added Parameter.requires_equals. When enabled, values for keywords must be provided by an =. For example, --foo=bar. --foo bar would raise an error te
Parameter.requires_equals. When enabled, values for keywords must be provided by an =. For example, --foo=bar. --foo bar would raise an error telling the user to use a =. Defaults to False (same behavior as before). by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/751Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.5.4...v4.6.0
Fix short-flag deduplication logic. Previously if 2 same-letter flags were provided (e.g. -n and -N), only -n would be displayed on the help page. Doe
-n and -N), only -n would be displayed on the help page. Does not impact actual value parsing. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/748Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.5.3...v4.5.4
List correct values when unsupported value passed to Enum param by @joelostblom in https://github.com/BrianPugh/cyclopts/pull/745
Enum param by @joelostblom in https://github.com/BrianPugh/cyclopts/pull/745Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.5.2...v4.5.3
Resolve lazy commands when generating shell completion by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/743
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.5.1...v4.5.2
Fix Annotated/Decorator Parameter resolution when Optional by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/741
Parameter resolution when Optional by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/741Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.5.0...v4.5.1
Introduces cyclopts.types.StdioPath. This type subclasses pathlib.Path. If the special string - is supplied, then the object will read/write to stdin/
cyclopts.types.StdioPath. This type subclasses pathlib.Path. If the special string - is supplied, then the object will read/write to stdin/stdout. Only available on python >=3.12 by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/737Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.4.6...v4.5.0
Bugfix: Check __cyclopts__ (from using @Parameter decorator on a class) after unwrapping Annotated by @BrianPugh in https://github.com/BrianPugh/cyclo
__cyclopts__ (from using @Parameter decorator on a class) after unwrapping Annotated by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/736Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.4.5...v4.4.6
Don't recommend parse=False parameters for did-you-mean. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/733
parse=False parameters for did-you-mean. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/733Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.4.4...v4.4.5
Ensure extra keys don't exist in json for iterable types. by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/727
pydantic.BaseModel containers by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/729Special Thanks to @AntoninRousset for very clear and organized bug reporting with minimal-working examples!
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.4.3...v4.4.4
Place positive short flags after positive options, place negative short flags after negative options. By @BrianPugh in https://github.com/BrianPugh/cy
Place positive short flags after positive options, place negative short flags after negative options. By @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/721
Example python code:
@app.default
def foo(dry_run: Annotated[bool, Parameter(alias=["-d"])] = False):
...
Before:
--dry-run --no-dry-run -d Enable dry run mode. [default: False]
After:
--dry-run -d --no-dry-run Enable dry run mode. [default: False]
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.4.2...v4.4.3
Fix erroneously fuzzy-matching command "version" to "--version" in v4 by @BrianPugh in https://github.com/BrianPugh/cyclopts/pull/719
Full Changelog: https://github.com/BrianPugh/cyclopts/compare/v4.4.1...v4.4.2
Your coding agent can read these notes before it upgrades. Set up the MCP server →