Pydantic Typer Typer extension to enable pydantic support [!WARNING] This package is still in early development and some things might not work as expected, or change between versions. Table of Contents Installation Usage License Installation console pip install pydantic-typer [!NOTE] pydantic-typer comes with pydantic and typer as dependencies, so you don't need to install anything else. Usage For general typer usage, please refer to the typer documentation. All the code blocks below can be copied and used directly (they are tested Python files). To run any of the examples, copy the code to a file main.py, and run it: console python main.py Basic Usage :technologist: Simply use pydantic_typer.run instead of typer.run to enable pydantic support ```python from typing import Annotated import pydantic import typer import pydantic_typer class User(pydantic.BaseModel): id: Annotated[int, pydantic.Field(description="The id of the user.")] name: Annotated[str, pydantic.Field(description="The name of the user.")] = "Jane Doe" def main(num: int, user: User): typer.echo(f"{num} {type(num)}") typer.echo(f"{user} {type(user)}") if name == "main": pydantic_typer.run(main) ``` :t-rex: Non-Annotated Version ```python import pydantic import typer import pydantic_typer class User(pydantic.BaseModel): id: int = pydantic.Field(description="The id of the user.") name: str = pydantic.Field("Jane Doe", description="The name of the user.") def main(num: int, user: User): typer.echo(f"{num} {type(num)}") typer.echo(f"{user} {type(user)}") if name == "main": pydantic_typer.run(main) ``` :computer: Usage ```console $ # Run the basic example: $ python main.py Usage: main.py [OPTIONS] NUM Try 'main.py --help' for help. ╭─ Error ────────────────────────────────────────────────────────────╮ │ Missing argument 'NUM'. │ ╰────────────────────────────────────────────────────────────────────╯ $ # We're missing a required argument, try using --help as suggested: $ python main.py --help Usage: main.py [OPTIONS] NUM ╭─ Arguments ────────────────────────────────────────────────────────╮ │ * num INTEGER [default: None] [required] │ ╰────────────────────────────────────────────────────────────────────╯ ╭─ Options ──────────────────────────────────────────────────────────╮ │ * --user.id INTEGER The id of the user. [default: None] │ │ [required] │ │ --user.name TEXT The name of the user. │ │ [default: Jane Doe] │ │ --help Show this message and exit. │ ╰──────────────────────────────────────────────────────────────────── $ # Notice the help text for user.id and user.name are inferred from the pydantic.Field. $ # user.id is reqired, because we don't provide a default value for the field. $ # Now run the example with the required arguments: $ python main.py 1 --user.id 1 1 id=1 name='Jane Doe' $ # It worked! You can also experiment with an invalid user.id: $ python main.py 1 --user.id some-string Usage: example_001_basic.py [OPTIONS] NUM Try 'example_001_basic.py --help' for help. ╭─ Error ─────────────────────────────────────────────────────────────╮ │ Invalid value for '--user.id': 'some-string' is not a valid integer.│ ╰─────────────────────────────────────────────────────────────────────╯ ``` Usage with nested models :technologist: pydantic_typer.run also works with nested pydantic models ```python from future import annotations from typing import Optional import pydantic import typer import pydantic_typer class Pet(pydantic.BaseModel): name: str species: str class Person(pydantic.BaseModel): name: str age: Optional[float] = None # noqa: UP007 For Python versions >=3.10, prefer float | None pet: Pet def main(person: Person): typer.echo(f"{person} {type(person)}") if name == "main": pydantic_typer.run(main) ``` :computer: Usage console $ # Run the nested models example with the required options: $ python main.py --person.name "Patrick" --person.pet.name "Snoopy" --person.pet.species "Dog" name='Patrick' age=None pet=Pet(name='Snoopy', species='Dog') Use pydantic models with typer.Argument :technologist: You can annotate the parameters with typer.Argument to make all model fields CLI arguments ```python from future import annotations import pydantic import typer from typing_extensions import Annotated import pydantic_typer class User(pydantic.BaseModel): id: int name: str def main(num: Annotated[int, typer.Option()], user: Annotated[User, typer.Argument()]): typer.echo(f"{num} {type(num)}") typer.echo(f"{user} {type(user)}") if name == "main": pydantic_typer.run(main) ``` :computer: Usage ```console $ # Run the example $ python main.py Usage: main.py [OPTIONS] _PYDANTIC_USER_ID _PYDANTIC_USER_NAME Try 'main.py --help' for help. ╭─ Error ─────────────────────────────────────────────────────────────╮ │ Missing argument '_PYDANTIC_USER_ID'. │ ╰─────────────────────────────────────────────────────────────────────╯ $ #Notice how _PYDANTIC_USER_ID and _PYDANTIC_USER_NAME are now cli arguments instead of options. $ # Supply the arguments in the right order: python main.py 1 Patrick --num 1 1 id=1 name='Patrick' ``` :bulb: You can also override annotations directly on the pydantic model fields: ```python from future import annotations import pydantic import typer from typing_extensions import Annotated import pydantic_typer class User(pydantic.BaseModel): id: Annotated[int, typer.Argument(metavar="THE_ID")] name: Annotated[str, typer.Option()] def main(num: Annotated[int, typer.Option()], user: Annotated[User, typer.Argument()]): typer.echo(f"{num} {type(num)}") typer.echo(f"{user} {type(user)}") if name == "main": pydantic_typer.run(main) ``` Here, User is a typer.Argument, but we manually override the fields again: We override the metavar of to User.id be THE_ID And User.name to be a typer.Option Use pydantic models in multiple commands :technologist: For larger typer apps, you can use pydantic_typer.Typer instead of annotating each command function individually to enable pydantic models on all commands ```python from future import annotations import pydantic import typer from typing_extensions import Annotated from pydantic_typer import Typer app = Typer() class User(pydantic.BaseModel): id: int name: Annotated[str, typer.Option()] = "John" @app.command() def hi(user: User): typer.echo(f"Hi {user}") @app.command() def bye(user: User): typer.echo(f"Bye {user}") if name == "main": app() ``` Use pydantic types :technologist: You can also annotate arguments with pydantic types and they will be validated ```python import click import typer from pydantic import HttpUrl, conint import pydantic_typer EvenInt = conint(multiple_of=2) def main(num: EvenInt, url: HttpUrl, ctx: click.Context): # type: ignore typer.echo(f"{num} {type(num)}") typer.echo(f"{url} {type(url)}") if name == "main": pydantic_typer.run(main) ``` :technologist: Pydantic types also work in lists and tuples ```python from typing import List import typer from pydantic import AnyHttpUrl import pydantic_typer def main(urls: List[AnyHttpUrl] = typer.Option([], "--url")): typer.echo(f"urls: {urls}") if name == "main": pydantic_typer.run(main) ``` Use Union types :technologist: Thanks to pydantic.TypeAdapter, which we use internally, we also support Union types ```python import typer import pydantic_typer def main(value: bool | int | float | str = 1): typer.echo(f"{value} {type(value)}") if name == "main": pydantic_typer.run(main) ``` :computer: Usage ```console $ # Run the example using a boolean $ python main.py --value True True $ # Run the example using an integer $ python main.py --value 2 2 $ # Run the example using a float $ python main.py --value 2.1 2.1 $ # Run the example using a string $ python main.py --value "Hello World" Hello World $ # Before, we intentionally used 2, when testing the integer $ # Check what happens if you pass 1 $ python main.py --value 1 True $ # We get back a boolean! $ # This is because Unions are generally evaluated left to right. $ # So in this case bool > int > float > str, if parsing is successful. $ # There are some exceptions, where pydantic tries to be smart, see here for details: $ # https://docs.pydantic.dev/latest/concepts/unions/#smart-mode ``` Limitations [!WARNING] pydantic-typer does not yet support sequences of pydantic models: See this issue for details [!WARNING] pydantic-typer does not yet support self-referential pydantic models. [!WARNING] pydantic-typer does not yet support lists with complex sub-types, in particular unions such as list[str|int]. License pydantic-typer is distributed under the terms of the MIT license.
Latest version: 0.0.13 Released: 2024-11-14