Domain-specific parsing such as durations or key=value pairs belongs in reusable types, not command bodies.
When the built-in types are not enough, subclass click.ParamType:
- set
name(shown in help and error messages), - implement
convert(self, value, param, ctx), returning the converted value or callingself.fail(message, param, ctx)to produce a proper usage error.
convert must accept values that are already converted: defaults, prompts and programmatic calls may pass the target type rather than a string. Start with if isinstance(value, TargetType): return value. Since Click 8.4, ParamType is generic, so you can annotate it as ParamType[timedelta] for type checkers.
For one-off validation, a callback is lighter. callback=fn on a parameter is called as fn(ctx, param, value) after conversion, and its return value replaces the value. Raise click.BadParameter("...") to reject it. Callbacks are good for cross-checks and normalisation.
Eager parameters (is_eager=True) are processed before the others. Combined with expose_value=False and a callback that calls ctx.exit(), they implement options like --version or --list-formats that do something immediately and stop, even if required parameters are missing. Callbacks run even when the value was not supplied, so check value (and ctx.resilient_parsing, which is set during shell completion) before acting.
For custom version output, Click 8.5 adds click.custom_version_option; version_option covers the standard case.
Example
import re
from datetime import timedelta
import click
class Duration(click.ParamType):
name = "duration"
def convert(self, value, param, ctx):
if isinstance(value, timedelta):
return value
m = re.fullmatch(r"(\d+)([smh])", value)
if not m:
self.fail(f"{value!r} is not like 30s, 5m or 2h", param, ctx)
unit = {"s": "seconds", "m": "minutes", "h": "hours"}[m[2]]
return timedelta(**{unit: int(m[1])})
def even_only(ctx, param, value):
if value % 2:
raise click.BadParameter("must be even")
return value
@click.command()
@click.option("--timeout", type=Duration(), default="30s")
@click.option("--workers", type=int, default=2, callback=even_only)
def run(timeout, workers):
click.echo(f"timeout={timeout.total_seconds():.0f}s workers={workers}")
run.main(["--timeout", "5m", "--workers", "4"], standalone_mode=False) # timeout=300s workers=4Remember
- Subclass ParamType with name and convert(); call self.fail for bad input
- convert must also accept already-converted values
- callback=(ctx, param, value) validates; is_eager + ctx.exit() for --version-style options
Watch out for
- Writing convert() that assumes the value is always a string, breaking defaults of the target type.