Use Pydantic models as Click options.
Install:
pip install pydanclickLet's assume you have a Pydantic model:
class TrainingConfig(BaseModel):
epochs: int
lr: Annotated[float, Field(gt=0)] = 1e-4
early_stopping: bool = FalseAdd all its fields as options in your Click command:
from pydanclick import from_pydantic
@click.command()
@from_pydantic(TrainingConfig)
def cli(training_config: TrainingConfig):
# Here, we receive an already validated Pydantic object.
click.echo(training_config.model_dump_json(indent=2))~ python my_app.py --help
Usage: my_app.py [OPTIONS]
Options:
--early-stopping / --no-early-stopping
--lr FLOAT RANGE [x>0]
--epochs INTEGER [required]
--help Show this message and exit.- Take a tour of the features below
- Find examples in the
examples/folder - Read the 📚 Documentation
The following types are converted to native Click types:
| Pydantic type | Converted to |
|---|---|
bool |
click.BOOL |
str |
click.STRING |
int |
click.INT |
float |
click.FLOAT |
Annotated[int, Field(lt=..., ge=...) |
click.IntRange() |
Annotated[float, Field(lt=..., ge=...) |
click.FloatRange() |
pathlib.Path |
click.Path() |
uuid.UUID |
click.UUID |
datetime.datetime, datetime.date |
click.DateTime() |
Literal |
click.Choice |
Complex container types such as lists or dicts are also supported: they must be passed as JSON strings, and will be validated through Pydantic TypeAdapter.validate_json method:
--arg1 '[1, 2, 3]' --arg2 '{"a": bool, "b": false}'In any case, Pydantic validation will run during model instantiation.
pydanclick.from_pydantic can be called several times with different models.
Use the prefix parameter to namespace the options from different models:
class Foo(BaseModel):
a: str = ""
b: str = ""
class Bar(BaseModel):
x: int = 0
y: int = 0
@click.command()
@from_pydantic(Foo, prefix="foo")
@from_pydantic(Bar, prefix="bar")
def cli(foo: Foo, bar: Bar):
passwill give:
~ python cli.py
Usage: cli.py [OPTIONS]
Options:
--foo-a TEXT
--foo-b TEXT
--bar-x INTEGER
--bar-y INTEGER
--help Show this message and exit.
pydanclick can be used alongside regular options and arguments:
@click.command()
@click.argument("arg")
@click.option("--option")
@from_pydantic(Foo)
def cli(arg, option, foo: Foo):
passwill give:
~ python cli.py
Usage: cli.py [OPTIONS] ARG
Options:
--option TEXT
--a TEXT
--b TEXT
--help Show this message and exit.
Specify a custom variable name for the instantiated model with the same syntax as a regular Click option:
@click.command()
@from_pydantic("some_name", Foo)
def cli(some_name: Foo):
passOptions added with pydanclick.from_pydantic will appear in the command help page.
From docstrings: if griffe is installed, model docstring will be parsed and the Attributes section will be used to document options automatically (you can use pip install pydanclick[griffe] to install it). Use docstring_tyle to choose between google, numpy and sphinx coding style. Disable docstring parsing by passing parse_docstring=False.
From field description: pydanclick supports the Field(description=...) syntax from Pydantic. If specified, it will take precedence over the docstring description.
Explicitly: you can always specify a custom help string for a given field by using extra_options={"my_field": {"help": "my help string"}} where my_field is the name of your field.
Here are these three methods in action:
class Baz(BaseModel):
"""Some demo model.
Attributes:
a: this comes from the docstring (requires griffe)
"""
a: int = 0
b: Annotated[int, Field(description="this comes from the field description")] = 0
c: int = 0
@click.command()
@from_pydantic(Baz, extra_options={"c": {"help": "this comes from the `extra_options`"}})
def cli(baz: Baz):
passwill give:
~ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
--a INTEGER this comes from the docstring (requires griffe)
--b INTEGER this comes from the field description
--c INTEGER this comes from the `extra_options`
--help Show this message and exit.
Specify option names with rename and short option names with shorten:
@click.command()
@from_pydantic(Foo, rename={"a": "--alpha", "b": "--beta"}, shorten={"a": "-A", "b": "-B"})
def cli(foo: Foo):
passwill give:
~ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
-A, --alpha TEXT
-B, --beta TEXT
--help Show this message and exit.
Note that prefix won't be prepended to option names passed with rename or shorten.
Use extra_options to pass extra parameters to click.option for a given field.
For example, in the following code, the user will be prompted for the value of a:
@click.command()
@from_pydantic(Foo, extra_options={"a": {"prompt": True}})
def cli(foo: Foo):
passNested Pydantic models are supported, with arbitrary nesting level. Option names will be built by joining all parent names and the field names itself with dashes.
class Left(BaseModel):
x: int
class Right(BaseModel):
x: int
class Root(BaseModel):
left: Left
right: Right
x: int
@click.command()
@from_pydantic(Root)
def cli(root: Root):
passwill give:
~ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
--left-x INTEGER [required]
--right-x INTEGER [required]
--x INTEGER [required]
--help Show this message and exit.To use rename, shorten, exclude, extra_options with a nested field, use its dotted name, e.g. left.x or right.x. Note that the alias of a field will apply to all its sub-fields:
@click.command()
@from_pydantic(Root, rename={"right": "--the-other-left"})
def cli(root: Root):
passwill give:
~ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
--left-x INTEGER [required]
--the-other-left-x INTEGER [required]
--x INTEGER [required]
--help Show this message and exit.Unpacking provides a simpler API when working with list of submodels.
Consider the following example:
class Author:
name: str
primary: bool = False
class Book:
title: str
authors: list[Author]
@click.command()
@from_pydantic(Book, unpack_list=True)
def cli(book: Book):
passBy default, this would create two command-line arguments --title and --authors. Since authors has a complex type, it should be passed as a JSON string (e.g. --authors '[{"authors": {"name": "Alice", "primary": true}, {"name": "Bob"}]'). Using unpacked_listwill instead "unpack" the nested fieldnameinto the main namespace: this new argument is called--authors-name` and can be specified multiple time, for example:
python cli.py --authors-name Alice --authors-primary --authors-name Bobwould create:
Book(authors=[Author(name="Alice", primary=True), Author(name="Bob")])Note that you must always specify objects with optional arguments before objects without them. For example, the following command would make Bob the primary author, not Alice:
python cli.py --authors-name Bob --authors-name Alice --authors-primary(Why? Because under the hood, arguments are collected per field {"name": [Bob, Alice], "primary": [True]}, and relative placement between fields cannot be accessed.)
When in doubt, you can simply specify all arguments:
python cli.py --authors-name Bob --no-authors-primary --authors-name Alice --authors-primaryThis API is experimental and will not work in complex cases (deeply nested lists, lists of union, and much more). See issue #20 for context and details.
Functions:
- from_pydantic – Decorator to add fields from a Pydantic model as options to a Click command.
from_pydantic(
__var_or_model,
model=None,
*,
exclude=(),
rename=None,
shorten=None,
prefix=None,
parse_docstring=True,
docstring_style="google",
extra_options=None
)Decorator to add fields from a Pydantic model as options to a Click command.
Parameters:
- __var_or_model (
Union[str, Type[BaseModel]]) – name of the variable that will receive the Pydantic model in the decorated function - model (
Optional[Type[BaseModel]]) – Pydantic model - exclude (
Sequence[str]) – field names that won't be added to the command - rename (
Optional[Dict[str, str]]) – a mapping from field names to command line option names (this will override any prefix). Option names must start with two dashes - shorten (
Optional[Dict[str, str]]) – a mapping from field names to short command line option names. Option names must start with one dash - prefix (
Optional[str]) – a prefix to add to option names (without any dash) - parse_docstring (
bool) – if True andgriffeis installed, parse the docstring of the Pydantic model and pass argument documentation to the Clickhelpoption - docstring_style (
Literal['google', 'numpy', 'sphinx']) – style of the docstring (google,numpyorsphinx). Ignored ifparse_docstringis False - extra_options (
Optional[Dict[str, _ParameterKwargs]]) – a mapping from field names to a dictionary of options passed to theclick.option()function
Returns:
Install the environment and the pre-commit hooks with
make installRun tests with:
pytestpydanclick doesn't support (yet!):
- Pydantic v1
- converting fields to arguments, instead of options
- fields annotated with union of Pydantic models can only be used with JSON inputs, instead of properly merging all sub-fields
- custom argument validators
Other missing features:
- Reading model from file
- Specifying all field-specific options directly in the Pydantic model (would allow easier reuse)
- Most Click features should be supported out-of-the-box through the
extra_optionsparameter. However, most of them aren't tested - Click and Pydantic both include validation logic. In particular, Click support custom
ParamType, validation callbacks andBadParametererrors: it's not clear if we want to fully rely on Pydantic or on Click or on a mixture of both - populating Pydantic fields from existing options or arguments (combined with
exclude, it will provide a complete escape hatch to bypass Pydantclick when needed) - attaching Pydanclick arguments directly to the model class, to avoid duplication when re-using a model in multiple commands
Repository initiated with fpgmaas/cookiecutter-poetry.