New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add support for NamedTuple
and TypedDict
types
#2216
Changes from 11 commits
0cbe31f
c5e236a
0f5cb59
de54b65
1c1b499
986feda
a7bf673
6764357
62b4feb
7b45015
7368bf9
63f4a1e
ebfd440
4cb9dbd
58dbfd7
5663cd3
11bd960
8221ce2
0a250e1
3361496
d737f07
3fe8ff9
08868ab
567bf4d
9481776
1c6f369
5db83e9
4da1475
1e60f5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
add support for `NamedTuple` and `TypedDict` types |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from typing import NamedTuple | ||
|
||
from pydantic import BaseModel, ValidationError | ||
|
||
|
||
class Point(NamedTuple): | ||
x: int | ||
y: int | ||
|
||
|
||
class Model(BaseModel): | ||
p: Point | ||
|
||
|
||
print(Model(p=('1', '2'))) | ||
|
||
try: | ||
Model(p=('1.3', '2')) | ||
except ValidationError as e: | ||
print(e) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from typing import TypedDict | ||
|
||
from pydantic import BaseModel, ValidationError | ||
|
||
|
||
# `total=False` means keys are non-required | ||
class UserIdentity(TypedDict, total=False): | ||
name: str | ||
surname: str | ||
|
||
|
||
class User(TypedDict): | ||
identity: UserIdentity | ||
age: int | ||
|
||
|
||
class Model(BaseModel): | ||
u: User | ||
|
||
|
||
print(Model(u={'identity': {'name': 'Smith', 'surname': 'John'}, 'age': '37'})) | ||
|
||
print(Model(u={'identity': {'name': None, 'surname': 'John'}, 'age': '37'})) | ||
|
||
print(Model(u={'identity': {}, 'age': '37'})) | ||
|
||
|
||
try: | ||
Model(u={'identity': {'name': ['Smith'], 'surname': 'John'}, 'age': '24'}) | ||
except ValidationError as e: | ||
print(e) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -88,9 +88,19 @@ with custom properties and validation. | |||||
`typing.Tuple` | ||||||
: see [Typing Iterables](#typing-iterables) below for more detail on parsing and validation | ||||||
|
||||||
`subclass of typing.NamedTuple (or collections.namedtuple)` | ||||||
: Same as `tuple` but instantiates with the given namedtuple. | ||||||
_pydantic_ will validate the tuple if you use `typing.NamedTuple` since fields are annotated. | ||||||
If you use `collections.namedtuple`, no validation will be done. | ||||||
PrettyWood marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
See [Annotated Types](#annotated-types) below for more detail on parsing and validation | ||||||
|
||||||
`typing.Dict` | ||||||
: see [Typing Iterables](#typing-iterables) below for more detail on parsing and validation | ||||||
|
||||||
`subclass of typing.TypedDict` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think the subclass is implicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It used to be the case for me too but I now tend to disagree since we added the support of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would still prefer to remove |
||||||
: Same as `dict` but _pydantic_ will validate the dictionary since keys are annotated. | ||||||
See [Annotated Types](#annotated-types) below for more detail on parsing and validation | ||||||
|
||||||
`typing.Set` | ||||||
: see [Typing Iterables](#typing-iterables) below for more detail on parsing and validation | ||||||
|
||||||
|
@@ -395,6 +405,29 @@ With proper ordering in an annotated `Union`, you can use this to parse types of | |||||
``` | ||||||
_(This script is complete, it should run "as is")_ | ||||||
|
||||||
## Annotated Types | ||||||
|
||||||
### NamedTuple | ||||||
|
||||||
```py | ||||||
{!.tmp_examples/annotated_types_named_tuple.py!} | ||||||
``` | ||||||
_(This script is complete, it should run "as is")_ | ||||||
|
||||||
### TypedDict | ||||||
|
||||||
!!! note | ||||||
This is a new feature of the python standard library as of python 3.8. | ||||||
Prior to python 3.8, it requires the [typing-extensions](https://pypi.org/project/typing-extensions/) package. | ||||||
But required and optional fields are properly differentiated only since python 3.9. | ||||||
It is hence recommanded using [typing-extensions](https://pypi.org/project/typing-extensions/) with python 3.8 as well. | ||||||
PrettyWood marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
```py | ||||||
{!.tmp_examples/annotated_types_typed_dict.py!} | ||||||
``` | ||||||
_(This script is complete, it should run "as is")_ | ||||||
|
||||||
## Pydantic Types | ||||||
|
||||||
*pydantic* also provides a variety of other useful types: | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -188,6 +188,8 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: | |
'is_literal_type', | ||
'literal_values', | ||
'Literal', | ||
'is_named_tuple_type', | ||
'is_typed_dict_type', | ||
'is_new_type', | ||
'new_type_supertype', | ||
'is_classvar', | ||
|
@@ -298,6 +300,18 @@ def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]: | |
return tuple(x for value in values for x in all_literal_values(value)) | ||
|
||
|
||
def is_named_tuple_type(type_: Type[Any]) -> bool: | ||
from .utils import lenient_issubclass | ||
|
||
return lenient_issubclass(type_, tuple) and hasattr(type_, '_fields') | ||
|
||
|
||
def is_typed_dict_type(type_: Type[Any]) -> bool: | ||
from .utils import lenient_issubclass | ||
|
||
return lenient_issubclass(type_, dict) and getattr(type_, '__annotations__', None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Humm, I wonder if this is a water tight check. I can't think of a better solution though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently there is no easier way afaik. There will be a public method in 3.10 though |
||
|
||
|
||
test_type = NewType('test_type', str) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,5 +4,5 @@ Cython==0.29.21;sys_platform!='win32' | |
devtools==0.6.1 | ||
email-validator==1.1.2 | ||
dataclasses==0.6; python_version < '3.7' | ||
typing-extensions==3.7.4.1; python_version < '3.8' | ||
typing-extensions==3.7.4.3; python_version < '3.9' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had to bump this to have the good version of |
||
python-dotenv==0.15.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again I think the "subclass of..." is implicit