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.
pip install pydantic-typer
Note
pydantic-typer
comes with pydantic
and typer
as dependencies, so you don't need to install anything else.
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:
python main.py
๐งโ๐ป Simply use |
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) |
๐ฆ Non-Annotated Versionimport 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) |
๐ป Usage$ # 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 <class 'int'>
id=1 name='Jane Doe' <class '__main__.User'>
$ # 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.โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ |
๐งโ๐ป |
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) |
๐ป Usage$ # 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') <class '__main__.Person'> |
๐งโ๐ป You can annotate the parameters with |
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) |
๐ป Usage$ # 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 <class 'int'>
id=1 name='Patrick' <class '__main__.User'> |
๐ก You can also override annotations directly on the pydantic model fields: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,
|
๐งโ๐ป For larger |
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() |
๐งโ๐ป You can also annotate arguments with pydantic types and they will be validated |
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) |
๐งโ๐ป Pydantic types also work in lists and tuples |
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) |
๐งโ๐ป Thanks to |
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) |
๐ป Usage$ # Run the example using a boolean
$ python main.py --value True
True <class 'bool'>
$ # Run the example using an integer
$ python main.py --value 2
2 <class 'int'>
$ # Run the example using a float
$ python main.py --value 2.1
2.1 <class 'float'>
$ # Run the example using a string
$ python main.py --value "Hello World"
Hello World <class 'str'>
$ # Before, we intentionally used 2, when testing the integer
$ # Check what happens if you pass 1
$ python main.py --value 1
True <class 'bool'>
$ # 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 |
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]
.
pydantic-typer
is distributed under the terms of the MIT license.