Search
Search documentation
Inputs
Updated Oct 21, 2026
Most field configuration comes from pydantic itself — descriptions and
defaults via Field(...), constraints such as ge or pattern, choices via
Literal[...] or an Enum, repeated values via list[...], optionality via
X | None = None. For click behavior that has no pydantic equivalent (short
flags, env vars, conflicts, hidden, a custom value parser, …) annotate the
field with Argument(...) or Option(...).
Default routing
Plain fields of the input model are --options. To make a field positional,
annotate it with Argument():
from typing import Annotated
from pydantic import BaseModel
from fireargs import Argument
Name = Annotated[str, Argument()] # <name> positional
class GreetInput(BaseModel):
name: Name
verbose: bool = False # --verbose option (default routing)You can also use Option() explicitly to attach per-field option config —
even though plain fields are already options.
Flag names
A field's flag is its name in kebab-case: header_rows becomes
--header-rows, and a positional source_path is shown as <source-path>.
The handler, --json input and the --llms manifest keep the field name
(header_rows). Set name to choose the flag yourself:
from typing import Annotated
from pydantic import BaseModel
from fireargs import Option
ToPath = Annotated[str, Option(name="to")] # --to <value>
DryRun = Annotated[bool, Option(name="dryRun")] # --dryRun
class CopyInput(BaseModel):
header_rows: int = 1 # --header-rows <value>
to_path: ToPath = "."
dry_run: DryRun = FalseA name starting with no_ gives a plain --no-… flag, not a negation:
no_cache: bool = False is True when --no-cache is passed.
Wire keys shared with TypeScript
A Python field name is snake_case, so --json and --llms use snake_case
keys. To keep the keys of a TypeScript twin (headerRows), give the field a
pydantic alias, or the model an alias generator. --json input and output
and the --llms schemas then use the alias, while the flag is still derived
from the field name (--header-rows):
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class Wire(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, validate_by_name=True)
class ReadInput(Wire):
header_rows: int = 1 # --header-rows, "headerRows" on the wire--json input is read by alias; validate_by_name=True also accepts the
field name. Without it, a key sent under the field name is ignored like any
other unknown key.
The wrappers
Argument(**config) and Option(**config) return a pydantic
FieldInfo — what Field(...) returns — with fireargs' config attached. Use
them inside Annotated[...], so the type stays the field's own and a type
checker sees nothing new:
from typing import Annotated
from pydantic import BaseModel
from fireargs import Option
Limit = Annotated[int, Option(short="l", ge=1, description="Max rows")]
class ListInput(BaseModel):
limit: Limit = 20An annotated alias like Limit is a reusable field: give it the default at
each use site, so two commands can share the flag with different defaults.
Both wrappers accept pydantic's own Field settings: default,
default_factory, alias, title, description, examples, gt, ge,
lt, le, multiple_of, min_length, max_length, pattern, strict
and deprecated. A Field(...) in the same Annotated[...] works too —
pydantic merges them.
OptionConfig
Option(...) accepts, beyond the pydantic settings:
| Field | What it does |
|---|---|
name | Long flag without dashes, e.g. "to" → --to |
short | Short flag letter, e.g. "v" adds -v alongside --verbose |
envvar | Environment variable(s) to fall back to when the flag is absent |
show_envvar | Show the env var in --help |
conflicts | Field name(s) of options that conflict with this one |
implies | {"field_name": value} set when this option is given |
hidden | Hide from --help |
flag_value | Value used when the flag appears without an argument |
help_group | Heading under which this option is listed in help, e.g. "Logging" |
default_description | Display string for the default value in help |
parse | Turns the CLI string into the field's value — see below |
is_flag, count | click's: force a flag, or count repeats (-vvv → 3) on an int |
show_default | click's: False hides the default, a string replaces it |
metavar | click's: the value placeholder in help, instead of <value> |
prompt | click's: ask for the value interactively when it is missing |
hide_input | click's: mask the prompt; errors never echo the value |
confirmation_prompt | click's: ask twice |
ArgumentConfig
A positional takes fewer slots — no short flag, no conflicts/implies, no hidden, no help group. Beyond the pydantic settings:
| Field | What it does |
|---|---|
name | Name shown in help and usage, e.g. <file> |
envvar | Environment variable(s) to fall back to when absent |
metavar | Replaces <name> in the usage line |
default_description | Display string for the default value in help |
parse | Turns the CLI string into the field's value — see below |
A positional's default is shown next to its description under Arguments,
as [default: …].
Parsing CLI values
Click hands every value over as a string. fireargs converts it before
pydantic sees it, so the model describes the real value and --json callers
send that value directly:
- Numbers parse automatically. A field typed
intorfloat(or a list of them) turns--rows 10into10, and rejects--rows abcwitherror: option '--rows <value>' argument 'abc' is invalid. Not a number.--json '{"rows":"10"}'is still rejected — the JSON value must be a number. parsehandles any other syntax. It receives the raw string from argv or the env var and returns the field's value, which pydantic then validates. It never runs on--jsoninput or on defaults.
import json
from typing import Annotated, Literal
from pydantic import BaseModel
from fireargs import Option
def parse_header_rows(value: str) -> Literal[False] | list[int]:
return False if value == "false" else [int(row) for row in value.split(",")]
# --missing-values a,b → ["a", "b"]
MissingValues = Annotated[list[str] | None, Option(parse=lambda value: value.split(","))]
# --header-rows false → False, --header-rows 1,2 → [1, 2]
HeaderRows = Annotated[Literal[False] | list[int] | None, Option(parse=parse_header_rows)]
# --props '{"a":1}' → {"a": 1}
Props = Annotated[dict[str, int], Option(parse=json.loads)]
class ReadInput(BaseModel):
missing_values: MissingValues = None
header_rows: HeaderRows = None
props: Props = {}A field with parse always takes exactly one value (--key <value>), even
when its type is a list or a bool: parse owns the whole string. An
exception raised by parse is reported as an invalid argument, with the
exception's message, and exits 1 without reaching render_error:
error: option '--props <value>' argument 'nope' is invalid. Expecting value: line 1 column 1 (char 0)Reach for parse rather than a pydantic BeforeValidator when the comma
list or JSON string is only shell syntax. A validator belongs to the model,
so --json callers would go through it too, and --llms cannot tell them
which string syntax it expects.
Examples
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from fireargs import Argument, Option
# positional, required: <name>
Name = Annotated[str, Argument(description="user's name")]
# positional, optional: [greeting]; keep required positionals first
Greeting = Annotated[str | None, Argument()]
# option with short flag and env fallback
Port = Annotated[int, Option(short="p", envvar="PORT")]
# booleans with explicit conflicts
Rgb = Annotated[bool, Option(conflicts="cmyk")]
# hidden internal option
Secret = Annotated[str | None, Option(hidden=True)]
# flag value: --level alone is "info", --level debug is "debug"
Level = Annotated[str | None, Option(flag_value="info")]
class ServeInput(BaseModel):
name: Name
greeting: Greeting = None
port: Port = 8080
rgb: Rgb = False
cmyk: bool = False
secret: Secret = None
# repeated, with choices: --formats json --formats yaml
formats: list[Literal["json", "yaml", "toml"]] = []
level: Level = None
# a plain pydantic Field works too
retries: int = Field(3, ge=0, description="Retry this many times")What if I forget the wrapper?
Plain fields work — they default to --options with pydantic's validation.
The wrappers only add click-specific config that pydantic can't express.
Don't sprinkle them everywhere; reach for them when you genuinely need a
positional, a short flag, an env binding, or one of the other slots above.
Created with ❤ and Livemark