Skip to content

Initial attempt at pydantic support. - #630

Closed
gkarg wants to merge 1 commit into
fastapi:masterfrom
gkarg:pydantic-models
Closed

Initial attempt at pydantic support.#630
gkarg wants to merge 1 commit into
fastapi:masterfrom
gkarg:pydantic-models

Conversation

@gkarg

@gkarg gkarg commented Jun 30, 2023

Copy link
Copy Markdown

This PR builds on #111 (comment) by @sm-hawkfish / @ananis25
and attempts to implement #111 (comment) as described by @pypae

To re-state the problem this tries to solve:

  1. Typer, being "FastAPI of CLIs" ought to support pydantic models, I was shocked to find out it doesn't 🤷‍♂️
  2. The proposed, and in my opinion, very sensible user-facing interface would be:
#!/usr/bin/env python3

import click
import pydantic
import typer


class User(pydantic.BaseModel):
    id: int
    name: str = "Jane Doe"


def main(num: int, user: User):
    print(num, type(num))
    print(user, type(user))


if __name__ == "__main__":
    typer.run(main)
$ ./typer_demo.py 1 --user.id 2 --user.name "John Doe"

1 <class 'int'>
id=2 name='John Doe' <class '__main__.User'>

This PR basically implements this, with one major (and many minor) problems:

  • it's not possible to register arguments/options with . (dot) in the name in click itself :(
  • so for the time being, I'm going for -- delimiter, and it looks like this:
#!/usr/bin/env python3

import datetime
from typing import Any, Callable, Dict, Optional, List

import click
import pydantic
import typer

class DriverLicense(pydantic.BaseModel):
    license_id: str
    expires_at: datetime.datetime

class User(pydantic.BaseModel):
    id: int
    name = "Jane Doe"
    main_license: Optional[DriverLicense] = None
    driver_licenses: List[DriverLicense] = []


def main(num: int, user: User):
    print(num, type(num))
    print(user, type(user))


if __name__ == "__main__":
    typer.run(main)

->

Usage: tcli.py [OPTIONS] NUM

Arguments:
  NUM  [required]

Options:
  --user--id INTEGER
  --user--main-license--license-id TEXT
  --user--main-license--expires-at [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
  --user--driver-licenses--license-id TEXT
  --user--driver-licenses--expires-at [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
  --user--name TEXT               [default: Jane Doe]
  --help                          Show this message and exit.

which is, admittedly, very ugly, but oh well.

Beside that, I've yet to test all corner cases and weird field/param combinations (and write proper tests), and probably think long and hard, if things like

class User(pydantic.BaseModel):
     ...

def main(user: Annotated[User, typer.Argument[...]):
    ...

should/shouldn't be allowed, etc.

However, I'd love to first hear your thoughts on the viability of this PR in general.

@github-actions

Copy link
Copy Markdown
Contributor

📝 Docs preview for commit 1e0334f at: https://649ef33e50d4bf086d7df752--typertiangolo.netlify.app

@MrT3acher

Copy link
Copy Markdown

I tested it. It's great. thanks.

but why you are parsing arguments as below:

cli --model--field1 value1 --model--field2 value2

it isn't better to do like this:

cli --model-field1 value1 --model-field2 value2

or:

cli --model.field1 value1 --model.field2 value2

@gkarg

gkarg commented Jul 27, 2023

Copy link
Copy Markdown
Author

Hi, thanks.

As mentioned in the PR text, click itself (which is the basis for typer), does not allow . in argument names, at least not out of the box. I haven't yet investigated it further, maybe it can be done with an option, or as a feature request to click, or (worst scenario) as monkey-patch on click. But that means, sadly, that the most user-friendly notation, as in

cli --model.field1 value1 --model.field2 value2

is not available to us :(

Your other option,

cli --model-field1 value1 --model-field2 value2

is probably doable, but that would introduce ambiguity for distinguishing submodels and scalar fields. The closest example of resolving such ambiguity can sometimes be found in env var configs in some applications, e.g. AIRFLOW__DATABASE__SOME_URL (note two underscores for "section") -> airflow.database.some_url. Incedently I'm reusing some code from pydantic, that is capable of parsing env var in that exact format, and it seemed like a good idea.

Just to re-iterate, I do agree, that this format is very ugly, and would be much more happy with . notation.

@toppk

toppk commented Aug 5, 2023

Copy link
Copy Markdown

Just to give my two cents. I'm doing something like this (not fully implemented) but with attr, my poison of choice atm.

One thing that I'll add is that my use-case is similar to ssh's '-o' option, so I'm looking to use it's command line structure, which converted to your example would be:

Usage: tcli.py [OPTIONS] NUM

Arguments:
 NUM  [required]

Options:
 --user id=INTEGER
 --user main-license--license-id=TEXT
 --user main-license--expires-at=[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
 --user driver-licenses--license-id=TEXT
 --user driver-licenses--expires-at=[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
 --user name=TEXT               [default: Jane Doe]
 --help                          Show this message and exit.

doesn't look exactly right, since for the ssh use case the fields are clearly not intertwined as they are for your use-case. What I did for your use-case is just create a custom parser, which of course, doesn't get the pretty command line integration, but it is pretty usable.

Either way I would suggest working backwards from the desired command line, while it's possible to create what you have, this would start to break down really quickly if you did something like

def main(user: Annotated[list[User], typer.Argument[...]):
    ...

@djpugh

djpugh commented Oct 15, 2023

Copy link
Copy Markdown

@gkarg - thanks for this - I'd love to get pydantic (or any complex) models into the cli via this kind of logic! I'm relatively new to using typer (at least outside of quick tests), but familiar with click

As mentioned in the PR text, click itself (which is the basis for typer), does not allow . in argument names, at least not out of the box. I haven't yet investigated it further, maybe it can be done with an option, or as a feature request to click, or (worst scenario) as monkey-patch on click. But that means, sadly, that the most user-friendly notation, as in

I just tested with click version 8.1.7, and the option can take e.g. --a.b just not as the name

e.g.

@click.command()
@click.option('--a.b')
def test(**kwargs):
    print(kwargs)

Won't work, but:

@click.command()
@click.option('a_b', '--a.b')
def test(**kwargs):
    print(kwargs)

Will work with --a.b as the arg and the kwarg key as a_b - click requires names to be valid python identifiers - https://github.com/pallets/click/blob/ca5e1c3d75e95cbc70fa6ed51ef263592e9ac0d0/src/click/core.py#L2575.

Alternatively, but more clunkily, forwarding unprocessed args could handle it - that was how i did it previously (fragilely) when I was trying to do pydantic <> click directly? https://click.palletsprojects.com/en/8.1.x/advanced/#forwarding-unknown-options

In the past i tried to implement pydantic <> click handling directly, and di dmake use of that for complex args

@svlandeg svlandeg added feature New feature, enhancement or request p3 labels Feb 29, 2024
@pypae pypae mentioned this pull request Apr 24, 2024
7 tasks
@pypae

pypae commented Apr 24, 2024

Copy link
Copy Markdown

Note: I started another draft implementation for pydantic support in #803 which isn't as tightly coupled to typer.

@github-actions

github-actions Bot commented Sep 1, 2025

Copy link
Copy Markdown
Contributor

This pull request has a merge conflict that needs to be resolved.

@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Sep 1, 2025
@svlandeg svlandeg linked an issue Nov 19, 2025 that may be closed by this pull request

@svlandeg svlandeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing this PR as there is ongoing work in the pipeline to address this more fundamentally, e.g. #1831

@svlandeg svlandeg closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflicts Automatically generated when a PR has a merge conflict feature New feature, enhancement or request p3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support Pydantic Models as ParamTypes

6 participants