Click and Milo both use decorators, but they put the contract in different
places. Click attaches CLI metadata through stacked decorators such as
@click.option; Milo keeps the public contract in the Python signature,
Annotated[...]metadata, defaults, and docstring.
Official references: Click commands and groups and Click arguments.
Before
import click
@click.group()
def cli():
pass
@cli.command()
@click.option("--environment", required=True)
@click.option("--service", required=True)
@click.option("--version", default="latest")
def deploy(environment: str, service: str, version: str):
click.echo({"environment": environment, "service": service, "version": version})
After
from typing import Annotated
from milo import CLI, Description, MinLen
cli = CLI(name="deployer", description="Deploy services")
@cli.command("deploy", description="Deploy a service")
def deploy(
environment: Annotated[str, MinLen(1), Description("Target environment")],
service: Annotated[str, MinLen(1), Description("Service name")],
version: str = "latest",
) -> dict[str, str]:
return {"environment": environment, "service": service, "version": version}
if __name__ == "__main__":
cli.run()
Mapping
| Click concept | Milo equivalent |
|---|---|
@click.command() |
@cli.command("name", description="...") |
@click.option("--name", default=...) |
name: T = default |
@click.option("--name", required=True) |
name: T |
@click.argument("name") |
Usuallyname: T; document any compatibility wrapper if argv shape must remain positional |
@click.group() |
group = cli.group("name", description="...") |
click.echo(...) |
Return structured values, or useContextoutput helpers |
click.Context / pass_context |
ctx: Context = Noneinjection |
Groups
from milo import CLI
cli = CLI(name="repo")
remote = cli.group("remote", description="Manage remotes")
@remote.command("list", description="List remotes")
def list_remotes() -> list[str]:
return ["origin"]
CLI users run repo remote list. Programmatic and MCP callers use
remote.list.
What To Watch
- Click decorators can hide contract details above the function. In Milo, contract review starts at the function signature.
- Avoid writing normal command output with
print()orclick.echo()in code that may run under MCP. Return values are safer for agents and--format json. - If you rely on Click's exact positional argument behavior, migrate that command with a compatibility test before changing user-facing argv shape.