Search
Search documentation
Composability
Updated Oct 21, 2026
Inputs and outputs in fireargs are pydantic models, so anything pydantic can
compose works here too — inheritance, Annotated field aliases, generic
models, nested models. Define common parameters once and reuse them across
every command that needs them, including the per-field click config you
attached via Argument() / Option().
Reusable fields
An Annotated alias is a field you can drop into any model. Its CLI config
and its default travel with it, and a use site can still override the default.
Type checkers see only = value defaults, so a model you build by hand in code
(a test, say) needs those fields spelled out:
from typing import Annotated
from fireargs import Option
Page = Annotated[int, Option(short="p", default=1, ge=1, description="Page number")]
Limit = Annotated[int, Option(short="l", default=20, ge=1, le=100, description="Page size")]
Verbose = Annotated[bool, Option(short="v", default=False)]Reusable parameter blocks
Group fields into a model and mix it into each command's input by inheritance:
from pydantic import BaseModel
class Pagination(BaseModel):
page: Page
limit: Limit
class Verbosity(BaseModel):
verbose: VerboseThe metadata you attached with Option(...) lives on the field, so when
you reuse Pagination in another command, --page / -p still come along
for free.
Composing into a command
from typing import Literal
class User(BaseModel):
id: str
name: str
class UsersInput(Pagination, Verbosity):
role: Literal["admin", "user"] | None = None
class UsersOutput(BaseModel):
users: list[User]
total: int
@command(description="List users")
def list_users(input: UsersInput) -> UsersOutput:
return UsersOutput(users=[], total=0)Pydantic orders inherited fields by the method resolution order, from the
last base to the first, then the class's own: UsersInput(Pagination, Verbosity) declares verbose, then page and limit, then role. That
order is the --help order, and for positionals the order they are read in
— keep blocks with positionals in a deliberate place among the bases.
A field redeclared in a subclass replaces the inherited one, so a command can change one default and keep the rest of the block:
class EventsInput(Pagination):
limit: Limit = 50
since: str | None = NoneReusable output blocks
Output models compose the same way. A common pattern is a generic "paginated response" model:
class Paginated[T](BaseModel):
items: list[T]
total: int
page: int
limit: int
class Post(BaseModel):
id: str
title: str
@command(description="List posts")
def list_posts(input: Pagination) -> Paginated[Post]:
return Paginated(items=[], total=0, page=input.page, limit=input.limit)Every Paginated[...] output ends up with identical pagination metadata in
its --llms JSON Schema — same source of truth. The item model lands in
$defs.
Shared handler code
The decorator reads the handler's annotations, so each command keeps its own small handler with concrete types; shared scaffolding goes into plain functions the handlers call:
def paginate[T](items: list[T], input: Pagination) -> Paginated[T]:
start = (input.page - 1) * input.limit
return Paginated(
items=items[start : start + input.limit],
total=len(items),
page=input.page,
limit=input.limit,
)
@command(description="List members")
def list_members(input: UsersInput) -> Paginated[User]:
return paginate([User(id="1", name="Ada")], input)The helper hides the boilerplate; the commands only specify what genuinely differs.
Programs as another composition layer
Beyond model composition, Program(...) composes commands themselves
into trees. Mounting the same leaf under different router keys is a valid
form of reuse:
cli = Program(name="myapp", router={"users": list_users, "members": list_users})Both invocation paths route to the same handler. See
Programs for the dispatch and --llms flattening
details.
What carries through
When you reuse a field or a block, every per-field detail rides along:
- Pydantic's own metadata —
description, defaults, constraints,alias,Literalchoices, validators. - fireargs config —
Argument()/Option()(name, short, envvar, conflicts, hidden, flag_value, help_group, default_description, parse).
Both surface in click's --help and in the --llms manifest exactly as if
you'd written the fields inline. Compose freely; nothing leaks or
duplicates.
Created with ❤ and Livemark