Repository: pydantic/pydantic

Issue:
"TypeError: TypedDict does not support instance and class checks" for TypedDict field
# Bug

Models with TypedDict fields currently result in an error when they are instantiated, due to the fact that TypedDict does not support isinstance/issubclass checks. 

```py
from typing import TypedDict

from pydantic import BaseModel

TD = TypedDict('TD', {'a': int})


class ExampleModel(BaseModel):
    td: TD


m = ExampleModel(td={'a': 1})

# output:
# Traceback (most recent call last):
#   ...
#   File "pydantic/main.py", line 246, in pydantic.main.ModelMetaclass.__new__
#   File "pydantic/fields.py", line 310, in pydantic.fields.ModelField.infer
#   File "pydantic/fields.py", line 272, in pydantic.fields.ModelField.__init__
#   File "pydantic/fields.py", line 364, in pydantic.fields.ModelField.prepare
#   File "pydantic/fields.py", line 405, in pydantic.fields.ModelField._type_analysis
#   File "/usr/lib/python3.8/typing.py", line 1728, in _check_fails
#     raise TypeError('TypedDict does not support instance and class checks')
# TypeError: TypedDict does not support instance and class checks
```

Previously, models could have TypedDict fields without any problems. This error seems to have been introduced in commit #1266.

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
pydantic version: 1.5.1
            pydantic compiled: False
                 install path: /home/kpberry/Downloads/pydantic/pydantic
               python version: 3.8.2 (default, Mar 13 2020, 10:14:16)  [GCC 9.3.0]
                     platform: Linux-5.4.0-26-generic-x86_64-with-glibc2.29
     optional deps. installed: ['typing-extensions', 'email-validator', 'devtools']
```




Comments:
afaik `TypedDict` is supported by pydantic, see #760.

Anything that does work will be flakey, will not perform proper validation and could break at any time.

I think best to explicitly raise an error whenever a `TypedDict` is used, saying

>`TypedDict` is not yet supported, see #760

I think `TypedDict` fields were usable with pydantic==1.4, but, as far as I can tell, the above `TypeError` does occur in pydantic>=1.5. 

I think you're right, though, and it probably is best to raise an explicit error when `TypedDict` is used as a model field until `TypedDict` is fully supported. 
I confirm `TypedDict` were usable with pydantic 1.4
There's no logic for validating `TypedDict`, so while you might have been able to use them, I very much doubt it was a good idea.

We should either support them fully or raise a sensible exception explaining that they don't work.

This is an issue about raising a better error, #760 is about supporting them fully.
I'm aware that no logic validation is currently performed by _pydantic_ and it is fine with me. 

> We should either support them fully or raise a sensible exception explaining that they don't work.

There is also the option of treating them as `Dict[Any, Any]` for validation (they are dict at run-time after all). That way, one could still get proper _mypy_ warnings during development and pydantic validation would just reduce to `isinstance(variable, dict)` 

v1.4 behaved like this:

```
In []: from typing import TypedDict                                                                                                                                              
In []: from pydantic import BaseModel
                                                                                                                 
In []: class MyDict(TypedDict): 
    ...:     a: int
                                                                                                                                                 
In []: class A(BaseModel): 
    ...:     d: MyDict                                                                                                                                                                       

In []: A(d={})                                                                                                                                                                   
Out[]: A(d={})

In []: A(d=12)                                                                                                                                                                   
---------------------------------------------------------------------------
ValidationError: 1 validation error for A
d
  value is not a valid dict (type=type_error.dict)
```


> There's no logic for validating TypedDict

Actually TypedDict does contain validation instructions

```
>>> from typing import TypedDict

>>> class Person(TypedDict):
...     name: str
...     age: int

>>> Person.__annotations__
{'name': <class 'str'>, 'age': <class 'int'>}
```

So technically Pydantic should be able to validate dictionaries that are type hinted with `TypedDicts`
Yes, it's possible, now someone just needs to go and implement the feature. 😄 
I currently have a working solution for this which does validation based on the TypedDict annotations as suggested. I'm currently writing test cases and will post an update when I've got full coverage.
My solution is here, but there are parts of it that I think could probably be done better:
[https://github.com/kpberry/pydantic/commits/typed-dict-support](url)

1. `TypedDict` does not support `issubclass` checks (which was the initial issue in this thread), so in order to check if a type is a subtype of `TypedDict` in `ModelField._type_analysis`, it seems like we need to check if it is a subclass of `TypedDict`'s metaclass. Unfortunately, `_TypedDictMeta` is protected in both the typing and typing_extensions module, so the mypy check won't allow it to be imported (probably for good reason). For now, I check `self.type_.__class__.__name__ == '_TypedDictMeta'`, but I think there must be a better way to do this.
2. I'm not sure what the best spot is in `ModelField._type_analysis` to do the `TypedDict` subclass check. I put it before the origin checks to avoid the issue at the top of this thread, but there might be a better spot for it.
3. I added each `TypedDict` value type to `self.sub_fields` in `ModelField._type_analysis`, since it fit the pattern for validating `Tuple`s, etc. However, since each value corresponds to a specific named key, I had to add a `key_name` attribute to `ModelField` with the key name in order to implement `ModelField._validate_typeddict`. I'm not sure if it's a good idea to add new attributes to `ModelField` for this, especially considering that it's a relatively niche feature.

Would appreciate feedback/suggestions before trying to make a pull request.

Edit: Forgot to mention, all tests are passing and all lines that I added should be covered.
Edit 2: I noticed that there's an optional `total` parameter to `TypedDict`, so I added support and tests for that.

Patch:
diff --git a/docs/examples/annotated_types_named_tuple.py b/docs/examples/annotated_types_named_tuple.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/annotated_types_named_tuple.py
@@ -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)
diff --git a/docs/examples/annotated_types_typed_dict.py b/docs/examples/annotated_types_typed_dict.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/annotated_types_typed_dict.py
@@ -0,0 +1,45 @@
+from typing import TypedDict
+
+from pydantic import BaseModel, Extra, 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
+
+    class Config:
+        extra = Extra.forbid
+
+
+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)
+
+try:
+    Model(
+        u={
+            'identity': {'name': 'Smith', 'surname': 'John'},
+            'age': '37',
+            'email': 'john.smith@me.com',
+        }
+    )
+except ValidationError as e:
+    print(e)
diff --git a/docs/examples/models_from_typeddict.py b/docs/examples/models_from_typeddict.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/models_from_typeddict.py
@@ -0,0 +1,21 @@
+from typing import TypedDict
+
+from pydantic import ValidationError, create_model_from_typeddict
+
+
+class User(TypedDict):
+    name: str
+    id: int
+
+
+class Config:
+    extra = 'forbid'
+
+
+UserM = create_model_from_typeddict(User, __config__=Config)
+print(repr(UserM(name=123, id='3')))
+
+try:
+    UserM(name=123, id='3', other='no')
+except ValidationError as e:
+    print(e)
diff --git a/pydantic/__init__.py b/pydantic/__init__.py
--- a/pydantic/__init__.py
+++ b/pydantic/__init__.py
@@ -1,5 +1,6 @@
 # flake8: noqa
 from . import dataclasses
+from .annotated_types import create_model_from_namedtuple, create_model_from_typeddict
 from .class_validators import root_validator, validator
 from .decorator import validate_arguments
 from .env_settings import BaseSettings
@@ -16,6 +17,9 @@
 # WARNING __all__ from .errors is not included here, it will be removed as an export here in v2
 # please use "from pydantic.errors import ..." instead
 __all__ = [
+    # annotated types utils
+    'create_model_from_namedtuple',
+    'create_model_from_typeddict',
     # dataclasses
     'dataclasses',
     # class_validators
diff --git a/pydantic/annotated_types.py b/pydantic/annotated_types.py
new file mode 100644
--- /dev/null
+++ b/pydantic/annotated_types.py
@@ -0,0 +1,59 @@
+from typing import TYPE_CHECKING, Any, Dict, FrozenSet, NamedTuple, Type
+
+from .fields import Required
+from .main import BaseModel, create_model
+
+if TYPE_CHECKING:
+
+    class TypedDict(Dict[str, Any]):
+        __annotations__: Dict[str, Type[Any]]
+        __total__: bool
+        __required_keys__: FrozenSet[str]
+        __optional_keys__: FrozenSet[str]
+
+
+def create_model_from_typeddict(typeddict_cls: Type['TypedDict'], **kwargs: Any) -> Type['BaseModel']:
+    """
+    Create a `BaseModel` based on the fields of a `TypedDict`.
+    Since `typing.TypedDict` in Python 3.8 does not store runtime information about optional keys,
+    we warn the user if that's the case (see https://bugs.python.org/issue38834).
+    """
+    field_definitions: Dict[str, Any]
+
+    # Best case scenario: with python 3.9+ or when `TypedDict` is imported from `typing_extensions`
+    if hasattr(typeddict_cls, '__required_keys__'):
+        field_definitions = {
+            field_name: (field_type, Required if field_name in typeddict_cls.__required_keys__ else None)
+            for field_name, field_type in typeddict_cls.__annotations__.items()
+        }
+    else:
+        import warnings
+
+        warnings.warn(
+            'You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` for better support! '
+            'Without it, there is no way to differentiate required and optional fields when subclassed. '
+            'Fields will therefore be considered all required or all optional depending on class totality.',
+            UserWarning,
+        )
+        default_value = Required if typeddict_cls.__total__ else None
+        field_definitions = {
+            field_name: (field_type, default_value) for field_name, field_type in typeddict_cls.__annotations__.items()
+        }
+
+    return create_model(typeddict_cls.__name__, **kwargs, **field_definitions)
+
+
+def create_model_from_namedtuple(namedtuple_cls: Type['NamedTuple'], **kwargs: Any) -> Type['BaseModel']:
+    """
+    Create a `BaseModel` based on the fields of a named tuple.
+    A named tuple can be created with `typing.NamedTuple` and declared annotations
+    but also with `collections.namedtuple`, in this case we consider all fields
+    to have type `Any`.
+    """
+    namedtuple_annotations: Dict[str, Type[Any]] = getattr(
+        namedtuple_cls, '__annotations__', {k: Any for k in namedtuple_cls._fields}
+    )
+    field_definitions: Dict[str, Any] = {
+        field_name: (field_type, Required) for field_name, field_type in namedtuple_annotations.items()
+    }
+    return create_model(namedtuple_cls.__name__, **kwargs, **field_definitions)
diff --git a/pydantic/fields.py b/pydantic/fields.py
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -38,6 +38,7 @@
     get_origin,
     is_literal_type,
     is_new_type,
+    is_typeddict,
     new_type_supertype,
 )
 from .utils import PyObjectStr, Representation, lenient_issubclass, sequence_like, smart_deepcopy
@@ -416,6 +417,8 @@ def _type_analysis(self) -> None:  # noqa: C901 (ignore complexity)
             return
         elif is_literal_type(self.type_):
             return
+        elif is_typeddict(self.type_):
+            return
 
         origin = get_origin(self.type_)
         if origin is None:
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -34,7 +34,15 @@
 from .parse import Protocol, load_file, load_str_bytes
 from .schema import default_ref_template, model_schema
 from .types import PyObject, StrBytes
-from .typing import AnyCallable, get_args, get_origin, is_classvar, resolve_annotations, update_field_forward_refs
+from .typing import (
+    AnyCallable,
+    get_args,
+    get_origin,
+    is_classvar,
+    is_namedtuple,
+    resolve_annotations,
+    update_field_forward_refs,
+)
 from .utils import (
     ROOT_KEY,
     ClassAttribute,
@@ -732,7 +740,7 @@ def _get_value(
             }
 
         elif sequence_like(v):
-            return v.__class__(
+            seq_args = (
                 cls._get_value(
                     v_,
                     to_dict=to_dict,
@@ -748,6 +756,8 @@ def _get_value(
                 and (not value_include or value_include.is_included(i))
             )
 
+            return v.__class__(*seq_args) if is_namedtuple(v.__class__) else v.__class__(seq_args)
+
         elif isinstance(v, Enum) and getattr(cls.Config, 'use_enum_values', False):
             return v.value
 
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -65,6 +65,7 @@
     get_origin,
     is_callable_type,
     is_literal_type,
+    is_namedtuple,
     literal_values,
 )
 from .utils import ROOT_KEY, get_model, lenient_issubclass, sequence_like
@@ -795,7 +796,17 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
         f_schema, schema_overrides = get_field_info_schema(field)
         f_schema.update(get_schema_ref(enum_name, ref_prefix, ref_template, schema_overrides))
         definitions[enum_name] = enum_process_schema(field_type)
-    else:
+    elif is_namedtuple(field_type):
+        sub_schema, *_ = model_process_schema(
+            field_type.__pydantic_model__,
+            by_alias=by_alias,
+            model_name_map=model_name_map,
+            ref_prefix=ref_prefix,
+            ref_template=ref_template,
+            known_models=known_models,
+        )
+        f_schema.update({'type': 'array', 'items': list(sub_schema['properties'].values())})
+    elif not hasattr(field_type, '__pydantic_model__'):
         add_field_type_to_schema(field_type, f_schema)
 
         modify_schema = getattr(field_type, '__modify_schema__', None)
diff --git a/pydantic/typing.py b/pydantic/typing.py
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -188,6 +188,8 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]:
     'is_literal_type',
     'literal_values',
     'Literal',
+    'is_namedtuple',
+    'is_typeddict',
     'is_new_type',
     'new_type_supertype',
     'is_classvar',
@@ -298,6 +300,26 @@ 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_namedtuple(type_: Type[Any]) -> bool:
+    """
+    Check if a given class is a named tuple.
+    It can be either a `typing.NamedTuple` or `collections.namedtuple`
+    """
+    from .utils import lenient_issubclass
+
+    return lenient_issubclass(type_, tuple) and hasattr(type_, '_fields')
+
+
+def is_typeddict(type_: Type[Any]) -> bool:
+    """
+    Check if a given class is a typed dict (from `typing` or `typing_extensions`)
+    In 3.10, there will be a public method (https://docs.python.org/3.10/library/typing.html#typing.is_typeddict)
+    """
+    from .utils import lenient_issubclass
+
+    return lenient_issubclass(type_, dict) and hasattr(type_, '__total__')
+
+
 test_type = NewType('test_type', str)
 
 
diff --git a/pydantic/validators.py b/pydantic/validators.py
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -15,6 +15,7 @@
     FrozenSet,
     Generator,
     List,
+    NamedTuple,
     Pattern,
     Set,
     Tuple,
@@ -36,10 +37,13 @@
     get_class,
     is_callable_type,
     is_literal_type,
+    is_namedtuple,
+    is_typeddict,
 )
 from .utils import almost_equal_floats, lenient_issubclass, sequence_like
 
 if TYPE_CHECKING:
+    from .annotated_types import TypedDict
     from .fields import ModelField
     from .main import BaseConfig
     from .types import ConstrainedDecimal, ConstrainedFloat, ConstrainedInt
@@ -536,6 +540,42 @@ def pattern_validator(v: Any) -> Pattern[str]:
         raise errors.PatternError()
 
 
+NamedTupleT = TypeVar('NamedTupleT', bound=NamedTuple)
+
+
+def make_namedtuple_validator(namedtuple_cls: Type[NamedTupleT]) -> Callable[[Tuple[Any, ...]], NamedTupleT]:
+    from .annotated_types import create_model_from_namedtuple
+
+    NamedTupleModel = create_model_from_namedtuple(namedtuple_cls)
+    namedtuple_cls.__pydantic_model__ = NamedTupleModel  # type: ignore[attr-defined]
+
+    def namedtuple_validator(values: Tuple[Any, ...]) -> NamedTupleT:
+        annotations = NamedTupleModel.__annotations__
+
+        if len(values) > len(annotations):
+            raise errors.ListMaxLengthError(limit_value=len(annotations))
+
+        dict_values: Dict[str, Any] = dict(zip(annotations, values))
+        validated_dict_values: Dict[str, Any] = dict(NamedTupleModel(**dict_values))
+        return namedtuple_cls(**validated_dict_values)
+
+    return namedtuple_validator
+
+
+def make_typeddict_validator(
+    typeddict_cls: Type['TypedDict'], config: Type['BaseConfig']
+) -> Callable[[Any], Dict[str, Any]]:
+    from .annotated_types import create_model_from_typeddict
+
+    TypedDictModel = create_model_from_typeddict(typeddict_cls, __config__=config)
+    typeddict_cls.__pydantic_model__ = TypedDictModel  # type: ignore[attr-defined]
+
+    def typeddict_validator(values: 'TypedDict') -> Dict[str, Any]:
+        return TypedDictModel.parse_obj(values).dict(exclude_unset=True)
+
+    return typeddict_validator
+
+
 class IfConfig:
     def __init__(self, validator: AnyCallable, *config_attr_names: str) -> None:
         self.validator = validator
@@ -626,6 +666,13 @@ def find_validators(  # noqa: C901 (ignore complexity)
     if type_ is IntEnum:
         yield int_enum_validator
         return
+    if is_namedtuple(type_):
+        yield tuple_validator
+        yield make_namedtuple_validator(type_)
+        return
+    if is_typeddict(type_):
+        yield make_typeddict_validator(type_, config)
+        return
 
     class_ = get_class(type_)
     if class_ is not None:


Test patch:
diff --git a/tests/test_annotated_types.py b/tests/test_annotated_types.py
new file mode 100644
--- /dev/null
+++ b/tests/test_annotated_types.py
@@ -0,0 +1,278 @@
+"""
+Tests for annotated types that _pydantic_ can validate like
+- NamedTuple
+- TypedDict
+"""
+import json
+import sys
+from collections import namedtuple
+from typing import List, NamedTuple, Tuple
+
+if sys.version_info < (3, 9):
+    try:
+        from typing import TypedDict as LegacyTypedDict
+    except ImportError:
+        LegacyTypedDict = None
+
+    try:
+        from typing_extensions import TypedDict
+    except ImportError:
+        TypedDict = None
+else:
+    from typing import TypedDict
+
+    LegacyTypedDict = None
+
+import pytest
+
+from pydantic import BaseModel, ValidationError
+
+
+def test_namedtuple():
+    Position = namedtuple('Pos', 'x y')
+
+    class Event(NamedTuple):
+        a: int
+        b: int
+        c: int
+        d: str
+
+    class Model(BaseModel):
+        pos: Position
+        events: List[Event]
+
+    model = Model(pos=('1', 2), events=[[b'1', '2', 3, 'qwe']])
+    assert isinstance(model.pos, Position)
+    assert isinstance(model.events[0], Event)
+    assert model.pos.x == '1'
+    assert model.pos == Position('1', 2)
+    assert model.events[0] == Event(1, 2, 3, 'qwe')
+    assert repr(model) == "Model(pos=Pos(x='1', y=2), events=[Event(a=1, b=2, c=3, d='qwe')])"
+    assert model.json() == json.dumps(model.dict()) == '{"pos": ["1", 2], "events": [[1, 2, 3, "qwe"]]}'
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(pos=('1', 2), events=[['qwe', '2', 3, 'qwe']])
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('events', 0, 'a'),
+            'msg': 'value is not a valid integer',
+            'type': 'type_error.integer',
+        }
+    ]
+
+
+def test_namedtuple_schema():
+    class Position1(NamedTuple):
+        x: int
+        y: int
+
+    Position2 = namedtuple('Position2', 'x y')
+
+    class Model(BaseModel):
+        pos1: Position1
+        pos2: Position2
+        pos3: Tuple[int, int]
+
+    assert Model.schema() == {
+        'title': 'Model',
+        'type': 'object',
+        'properties': {
+            'pos1': {
+                'title': 'Pos1',
+                'type': 'array',
+                'items': [
+                    {'title': 'X', 'type': 'integer'},
+                    {'title': 'Y', 'type': 'integer'},
+                ],
+            },
+            'pos2': {
+                'title': 'Pos2',
+                'type': 'array',
+                'items': [
+                    {'title': 'X'},
+                    {'title': 'Y'},
+                ],
+            },
+            'pos3': {
+                'title': 'Pos3',
+                'type': 'array',
+                'items': [
+                    {'type': 'integer'},
+                    {'type': 'integer'},
+                ],
+            },
+        },
+        'required': ['pos1', 'pos2', 'pos3'],
+    }
+
+
+def test_namedtuple_right_length():
+    class Point(NamedTuple):
+        x: int
+        y: int
+
+    class Model(BaseModel):
+        p: Point
+
+    assert isinstance(Model(p=(1, 2)), Model)
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(p=(1, 2, 3))
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('p',),
+            'msg': 'ensure this value has at most 2 items',
+            'type': 'value_error.list.max_items',
+            'ctx': {'limit_value': 2},
+        }
+    ]
+
+
+@pytest.mark.skipif(not TypedDict, reason='typing_extensions not installed')
+def test_typeddict():
+    class TD(TypedDict):
+        a: int
+        b: int
+        c: int
+        d: str
+
+    class Model(BaseModel):
+        td: TD
+
+    m = Model(td={'a': '3', 'b': b'1', 'c': 4, 'd': 'qwe'})
+    assert m.td == {'a': 3, 'b': 1, 'c': 4, 'd': 'qwe'}
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(td={'a': [1], 'b': 2, 'c': 3, 'd': 'qwe'})
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('td', 'a'),
+            'msg': 'value is not a valid integer',
+            'type': 'type_error.integer',
+        }
+    ]
+
+
+@pytest.mark.skipif(not TypedDict, reason='typing_extensions not installed')
+def test_typeddict_non_total():
+    class FullMovie(TypedDict, total=True):
+        name: str
+        year: int
+
+    class Model(BaseModel):
+        movie: FullMovie
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(movie={'year': '2002'})
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('movie', 'name'),
+            'msg': 'field required',
+            'type': 'value_error.missing',
+        }
+    ]
+
+    class PartialMovie(TypedDict, total=False):
+        name: str
+        year: int
+
+    class Model(BaseModel):
+        movie: PartialMovie
+
+    m = Model(movie={'year': '2002'})
+    assert m.movie == {'year': 2002}
+
+
+@pytest.mark.skipif(not TypedDict, reason='typing_extensions not installed')
+def test_partial_new_typeddict():
+    class OptionalUser(TypedDict, total=False):
+        name: str
+
+    class User(OptionalUser):
+        id: int
+
+    class Model(BaseModel):
+        user: User
+
+    m = Model(user={'id': 1})
+    assert m.user == {'id': 1}
+
+
+@pytest.mark.skipif(not LegacyTypedDict, reason='python 3.9+ is used or typing_extensions is installed')
+def test_partial_legacy_typeddict():
+    class OptionalUser(LegacyTypedDict, total=False):
+        name: str
+
+    class User(OptionalUser):
+        id: int
+
+    with pytest.warns(
+        UserWarning,
+        match='You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` for better support!',
+    ):
+
+        class Model(BaseModel):
+            user: User
+
+        with pytest.raises(ValidationError) as exc_info:
+            Model(user={'id': 1})
+        assert exc_info.value.errors() == [
+            {
+                'loc': ('user', 'name'),
+                'msg': 'field required',
+                'type': 'value_error.missing',
+            }
+        ]
+
+
+@pytest.mark.skipif(not TypedDict, reason='typing_extensions not installed')
+def test_typeddict_extra():
+    class User(TypedDict):
+        name: str
+        age: int
+
+    class Model(BaseModel):
+        u: User
+
+        class Config:
+            extra = 'forbid'
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(u={'name': 'pika', 'age': 7, 'rank': 1})
+    assert exc_info.value.errors() == [
+        {'loc': ('u', 'rank'), 'msg': 'extra fields not permitted', 'type': 'value_error.extra'},
+    ]
+
+
+@pytest.mark.skipif(not TypedDict, reason='typing_extensions not installed')
+def test_typeddict_schema():
+    class Data(BaseModel):
+        a: int
+
+    class DataTD(TypedDict):
+        a: int
+
+    class Model(BaseModel):
+        data: Data
+        data_td: DataTD
+
+    assert Model.schema() == {
+        'title': 'Model',
+        'type': 'object',
+        'properties': {'data': {'$ref': '#/definitions/Data'}, 'data_td': {'$ref': '#/definitions/DataTD'}},
+        'required': ['data', 'data_td'],
+        'definitions': {
+            'Data': {
+                'type': 'object',
+                'title': 'Data',
+                'properties': {'a': {'title': 'A', 'type': 'integer'}},
+                'required': ['a'],
+            },
+            'DataTD': {
+                'type': 'object',
+                'title': 'DataTD',
+                'properties': {'a': {'title': 'A', 'type': 'integer'}},
+                'required': ['a'],
+            },
+        },
+    }
diff --git a/tests/test_typing.py b/tests/test_typing.py
new file mode 100644
--- /dev/null
+++ b/tests/test_typing.py
@@ -0,0 +1,56 @@
+from collections import namedtuple
+from typing import NamedTuple
+
+import pytest
+
+from pydantic.typing import is_namedtuple, is_typeddict
+
+try:
+    from typing import TypedDict as typing_TypedDict
+except ImportError:
+    typing_TypedDict = None
+
+try:
+    from typing_extensions import TypedDict as typing_extensions_TypedDict
+except ImportError:
+    typing_extensions_TypedDict = None
+
+
+try:
+    from mypy_extensions import TypedDict as mypy_extensions_TypedDict
+except ImportError:
+    mypy_extensions_TypedDict = None
+
+ALL_TYPEDDICT_KINDS = (typing_TypedDict, typing_extensions_TypedDict, mypy_extensions_TypedDict)
+
+
+def test_is_namedtuple():
+    class Employee(NamedTuple):
+        name: str
+        id: int = 3
+
+    assert is_namedtuple(namedtuple('Point', 'x y')) is True
+    assert is_namedtuple(Employee) is True
+    assert is_namedtuple(NamedTuple('Employee', [('name', str), ('id', int)])) is True
+
+    class Other(tuple):
+        name: str
+        id: int
+
+    assert is_namedtuple(Other) is False
+
+
+@pytest.mark.parametrize('TypedDict', (t for t in ALL_TYPEDDICT_KINDS if t is not None))
+def test_is_typeddict_typing(TypedDict):
+    class Employee(TypedDict):
+        name: str
+        id: int
+
+    assert is_typeddict(Employee) is True
+    assert is_typeddict(TypedDict('Employee', {'name': str, 'id': int})) is True
+
+    class Other(dict):
+        name: str
+        id: int
+
+    assert is_typeddict(Other) is False