Repository: python/mypy

Issue:
Stubtest: Many false positives when the runtime has a `TypedDict`
**Bug Report**

Given a stub that looks like this:

```py
from typing import Any
from typing_extensions import TypedDict

class DBConfig(TypedDict, total=False):
    ATOMIC_REQUESTS: bool
    AUTOCOMMIT: bool
    CONN_MAX_AGE: int
    DISABLE_SERVER_SIDE_CURSORS: bool
    ENGINE: str
    HOST: str
    NAME: str
    OPTIONS: dict[str, Any] | None
    PASSWORD: str
    PORT: str
    TEST: dict[str, Any]
    TIME_ZONE: str
    USER: str
```

And a runtime that looks like this:

```py
from typing import Any, Dict, Optional, Union
from typing_extensions import TypedDict

class DBConfig(TypedDict, total=False):
    ATOMIC_REQUESTS: bool
    AUTOCOMMIT: bool
    CONN_MAX_AGE: Optional[int]
    CONN_HEALTH_CHECKS: bool
    DISABLE_SERVER_SIDE_CURSORS: bool
    ENGINE: str
    HOST: str
    NAME: str
    OPTIONS: Optional[Dict[str, Any]]
    PASSWORD: str
    PORT: Union[str, int]
    TEST: Dict[str, Any]
    TIME_ZONE: str
    USER: str
```

Stubtest emits the following errors, all of which are false positives:

```
error: dj_database_url.DBConfig is inconsistent, metaclass differs
Stub: in file /home/runner/work/typeshed/typeshed/stubs/dj-database-url/dj_database_url.pyi:-1
N/A
Runtime: in file /tmp/tmpwl18jpo6/lib/python3.10/site-packages/dj_database_url.py:37
<class 'typing_extensions._TypedDictMeta'>

error: dj_database_url.DBConfig.__new__ is inconsistent, stub argument "cls" should be positional-only (rename with a leading double underscore, i.e. "__cls")
Stub: in file /home/runner/work/typeshed/typeshed/stubs/dj-database-url/dj_database_url.pyi:91
def [Self] (cls: Type[Self`0]) -> Self`0
Runtime: in file /tmp/tmpwl18jpo6/lib/python3.10/site-packages/typing_extensions.py:679
def (cls, _typename, _fields=None, /, **kwargs)

error: dj_database_url.DBConfig.__new__ is inconsistent, stub does not have argument "_typename"
Stub: in file /home/runner/work/typeshed/typeshed/stubs/dj-database-url/dj_database_url.pyi:91
def [Self] (cls: Type[Self`0]) -> Self`0
Runtime: in file /tmp/tmpwl18jpo6/lib/python3.10/site-packages/typing_extensions.py:679
def (cls, _typename, _fields=None, /, **kwargs)

error: dj_database_url.DBConfig.__new__ is inconsistent, stub does not have argument "_fields"
Stub: in file /home/runner/work/typeshed/typeshed/stubs/dj-database-url/dj_database_url.pyi:91
def [Self] (cls: Type[Self`0]) -> Self`0
Runtime: in file /tmp/tmpwl18jpo6/lib/python3.10/site-packages/typing_extensions.py:679
def (cls, _typename, _fields=None, /, **kwargs)
```

This came up in https://github.com/python/typeshed/pull/9975

**Expected Behavior**

No error should be reported; the TypedDicts are identical.

**Your Environment**

- Mypy version used: 1.1.1


Discussion:

Resolving patch:
diff --git a/mypy/stubtest.py b/mypy/stubtest.py
--- a/mypy/stubtest.py
+++ b/mypy/stubtest.py
@@ -26,7 +26,7 @@
 from functools import singledispatch
 from pathlib import Path
 from typing import Any, Generic, Iterator, TypeVar, Union
-from typing_extensions import get_origin
+from typing_extensions import get_origin, is_typeddict
 
 import mypy.build
 import mypy.modulefinder
@@ -436,12 +436,12 @@ class SubClass(runtime):  # type: ignore[misc]
 
 
 def _verify_metaclass(
-    stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
+    stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str], *, is_runtime_typeddict: bool
 ) -> Iterator[Error]:
     # We exclude protocols, because of how complex their implementation is in different versions of
-    # python. Enums are also hard, ignoring.
+    # python. Enums are also hard, as are runtime TypedDicts; ignoring.
     # TODO: check that metaclasses are identical?
-    if not stub.is_protocol and not stub.is_enum:
+    if not stub.is_protocol and not stub.is_enum and not is_runtime_typeddict:
         runtime_metaclass = type(runtime)
         if runtime_metaclass is not type and stub.metaclass_type is None:
             # This means that runtime has a custom metaclass, but a stub does not.
@@ -485,7 +485,10 @@ def verify_typeinfo(
         return
 
     yield from _verify_final(stub, runtime, object_path)
-    yield from _verify_metaclass(stub, runtime, object_path)
+    is_runtime_typeddict = stub.typeddict_type is not None and is_typeddict(runtime)
+    yield from _verify_metaclass(
+        stub, runtime, object_path, is_runtime_typeddict=is_runtime_typeddict
+    )
 
     # Check everything already defined on the stub class itself (i.e. not inherited)
     to_check = set(stub.names)
@@ -493,7 +496,7 @@ def verify_typeinfo(
     to_check.update(
         m for m in vars(runtime) if not is_probably_private(m) and m not in IGNORABLE_CLASS_DUNDERS
     )
-    # Special-case the __init__ method for Protocols
+    # Special-case the __init__ method for Protocols and the __new__ method for TypedDicts
     #
     # TODO: On Python <3.11, __init__ methods on Protocol classes
     # are silently discarded and replaced.
@@ -501,6 +504,8 @@ def verify_typeinfo(
     # Ideally, we'd figure out a good way of validating Protocol __init__ methods on 3.11+.
     if stub.is_protocol:
         to_check.discard("__init__")
+    if is_runtime_typeddict:
+        to_check.discard("__new__")
 
     for entry in sorted(to_check):
         mangled_entry = entry


Test patch:
diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py
--- a/mypy/test/teststubtest.py
+++ b/mypy/test/teststubtest.py
@@ -1572,24 +1572,36 @@ class _Options(TypedDict):
         )
 
     @collect_cases
-    def test_protocol(self) -> Iterator[Case]:
+    def test_runtime_typing_objects(self) -> Iterator[Case]:
+        yield Case(
+            stub="from typing_extensions import Protocol, TypedDict",
+            runtime="from typing_extensions import Protocol, TypedDict",
+            error=None,
+        )
         yield Case(
             stub="""
-            from typing_extensions import Protocol
-
             class X(Protocol):
                 bar: int
                 def foo(self, x: int, y: bytes = ...) -> str: ...
             """,
             runtime="""
-            from typing_extensions import Protocol
-
             class X(Protocol):
                 bar: int
                 def foo(self, x: int, y: bytes = ...) -> str: ...
             """,
             error=None,
         )
+        yield Case(
+            stub="""
+            class Y(TypedDict):
+                a: int
+            """,
+            runtime="""
+            class Y(TypedDict):
+                a: int
+            """,
+            error=None,
+        )
 
     @collect_cases
     def test_type_var(self) -> Iterator[Case]: