Search
Search documentation
Commands
Updated Oct 21, 2026
@command(**config) turns a handler function into a Command, a subclass
of click's Command. The handler's first parameter annotation is the input
model, its return annotation is the output type, and an optional second
parameter receives a Context. Always call the decorator — @command(),
even without config.
from pydantic import BaseModel
from fireargs import command
class GreetInput(BaseModel):
name: str
class Greeting(BaseModel):
greeting: str
@command()
def greet(input: GreetInput) -> Greeting:
return Greeting(greeting=f"hello {input.name}")
if __name__ == "__main__":
greet()The command is named after the function in kebab-case — list_users becomes
list-users — unless name is set. Under a program the
router key names it instead.
Input and output
The input must be a pydantic BaseModel subclass: its fields determine the
positional arguments and --options (see Inputs). A
command without input takes an empty model. The output can be any type
pydantic validates — a model, a list[...], a scalar, None (see
Outputs). The handler's return value is validated
against it on every run, so returning a matching dict works and a mismatch
raises pydantic's ValidationError.
A handler without these annotations is rejected with a TypeError when it
is decorated.
@command.render
Optional. Turns the validated output into what a human sees at the shell.
It runs only without --json; with it, the output is written as JSON
instead. A returned string is written to stdout with a trailing newline;
return None to write the output yourself (spinners, colors, tables).
Declare a second parameter to receive a RenderContext carrying input
(the parsed input), json and command. Without a render, a run without
--json prints nothing.
from typing import Annotated
from pydantic import BaseModel
from fireargs import Argument, command
Path = Annotated[str, Argument()]
class ListInput(BaseModel):
path: Path
@command()
def list_names(input: ListInput) -> list[str]:
return [f"{input.path}/a", f"{input.path}/b"]
@list_names.render
def render_names(output: list[str]) -> str:
return "\n".join(f"- {name}" for name in output)The handler
Receives the parsed, pydantic-validated input and, when it declares a second
parameter, a Context. Must return a value matching the output type.
context.json is True when --json was given, with or without a value: a
handler that drives spinners or progress output keeps quiet then, so stdout
stays parseable. context.command is the running click command.
Handlers, renders and render_error hooks may be async def: fireargs runs
the coroutine with asyncio.run. Click itself is synchronous, so a command
with an async handler cannot be invoked from code that is already inside a
running event loop.
import asyncio
from pydantic import BaseModel
from fireargs import Context, command
class FetchInput(BaseModel):
url: str
@command(description="Fetch a URL")
async def fetch(input: FetchInput, context: Context) -> dict[str, str]:
await asyncio.sleep(0)
return {"url": input.url, "mode": "json" if context.json else "text"}Input that fails the model is reported like a commander parse error — one
error: ... line per field on stderr, exit code 1. Other exceptions
propagate out of the click command: in click's standalone mode they print a
traceback and exit 1. A render_error hook takes all of them instead (see
JSON mode).
What gets derived from the input model
For each field of the input model:
| Pydantic construct | Becomes |
|---|---|
str / int / float | --key <value>, required unless defaulted; a number's CLI string is parsed |
bool | --key flag |
Annotated[T, Argument()] | positional <key>; [key] when it has a default |
| a default value | shown as [default: …] in --help; pydantic fills it |
Literal["a", "b"] / a string Enum | a click Choice: --key [a|b] |
list[T] | --key <value>, repeated --key a --key b; positional variadic <key...> |
list[Literal[...]] | repeated, with choices on each value |
X | None = None | optional; left out of --json output while None |
description= on the field | shown in --help and surfaced as description in --llms JSON Schema |
Positional arguments are declared in field order, before every option. key
is shown in kebab-case: header_rows becomes --header-rows, while the
handler, --json and --llms keep header_rows. See
Flag names.
For per-field click config that pydantic doesn't model (short flag, env var,
conflicts, hidden, a flag value, a custom parse, …) annotate the field with
Argument(...) or Option(...).
CommandConfig
The keyword arguments of @command(...). All are optional.
| Field | What it does |
|---|---|
name | the command name; defaults to the function name in kebab-case |
description | help text, and the tool description in --llms |
aliases | extra names the parent program also routes to this command |
help_group | heading this command is listed under in its parent's help |
version | adds a -V, --version flag printing it |
version_option | {"flags": ..., "description": ...} for that flag, e.g. {"flags": "-v, --version"} |
json_option, llms_option | customize or disable the built-in --json/--llms flags. See LLMs mode |
render_error | (error[, context]) -> value — turn a failed run into output. See JSON mode |
context_settings | click's context_settings, e.g. {"help_option_names": ["-h", "--help"]} |
epilog, short_help, options_metavar | click's help text slots |
add_help_option, no_args_is_help | click's help toggles |
hidden, deprecated | click's: leave out of the parent's help, or mark deprecated |
render_error can also be registered with a decorator, like the render:
@greet.render_error.
The returned Command is a real click command, so the rest of click works
unchanged: greet.main(args, standalone_mode=False), click's CliRunner, or
add_command on a plain click.Group.
Why a decorator
The handler's annotations already state the input and output types; the decorator reads them instead of asking for them twice, so the schemas and the handler cannot drift apart, and type checkers see the same types fireargs validates.
Created with ❤ and Livemark