Skip to content

Commit 5928551

Browse files
authored
Reject isinstance() with TypedDict and NewType (#3654)
These can't be used for runtime type checking at runtime.
1 parent 8be7b74 commit 5928551

File tree

6 files changed

+30
-8
lines changed

6 files changed

+30
-8
lines changed

mypy/checkexpr.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,16 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type:
201201
except KeyError:
202202
# Undefined names should already be reported in semantic analysis.
203203
node = None
204-
if (isinstance(typ, IndexExpr)
205-
and isinstance(typ.analyzed, (TypeApplication, TypeAliasExpr))
204+
if ((isinstance(typ, IndexExpr)
205+
and isinstance(typ.analyzed, (TypeApplication, TypeAliasExpr)))
206206
# node.kind == TYPE_ALIAS only for aliases like It = Iterable[int].
207-
or isinstance(typ, NameExpr) and node and node.kind == nodes.TYPE_ALIAS):
207+
or (isinstance(typ, NameExpr) and node and node.kind == nodes.TYPE_ALIAS)):
208208
self.msg.type_arguments_not_allowed(e)
209+
if isinstance(typ, RefExpr) and isinstance(typ.node, TypeInfo):
210+
if typ.node.typeddict_type:
211+
self.msg.fail(messages.CANNOT_ISINSTANCE_TYPEDDICT, e)
212+
elif typ.node.is_newtype:
213+
self.msg.fail(messages.CANNOT_ISINSTANCE_NEWTYPE, e)
209214
self.try_infer_partial_type(e)
210215
callee_type = self.accept(e.callee, always_allow_any=True)
211216
if (self.chk.options.disallow_untyped_calls and

mypy/messages.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
NON_BOOLEAN_IN_CONDITIONAL = 'Condition must be a boolean'
8989
DUPLICATE_TYPE_SIGNATURES = 'Function has duplicate type signatures'
9090
GENERIC_INSTANCE_VAR_CLASS_ACCESS = 'Access to generic instance variables via class is ambiguous'
91+
CANNOT_ISINSTANCE_TYPEDDICT = 'Cannot use isinstance() with a TypedDict type'
92+
CANNOT_ISINSTANCE_NEWTYPE = 'Cannot use isinstance() with a NewType type'
9193

9294
ARG_CONSTRUCTOR_NAMES = {
9395
ARG_POS: "Arg",

mypy/test/testextensions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ def test_typeddict_errors(self):
8484
self.assertEqual(TypedDict.__module__, 'mypy_extensions')
8585
jim = Emp(name='Jim', id=1)
8686
with self.assertRaises(TypeError):
87-
isinstance({}, Emp)
87+
isinstance({}, Emp) # type: ignore
8888
with self.assertRaises(TypeError):
89-
isinstance(jim, Emp)
89+
isinstance(jim, Emp) # type: ignore
9090
with self.assertRaises(TypeError):
91-
issubclass(dict, Emp)
91+
issubclass(dict, Emp) # type: ignore
9292
with self.assertRaises(TypeError):
9393
TypedDict('Hi', x=1)
9494
with self.assertRaises(TypeError):

test-data/unit/check-newtype.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,11 @@ class C(B): pass # E: Cannot subclass NewType
336336
from typing import NewType
337337
Any = NewType('Any', int)
338338
Any(5)
339+
340+
[case testNewTypeAndIsInstance]
341+
from typing import NewType
342+
T = NewType('T', int)
343+
d: object
344+
if isinstance(d, T): # E: Cannot use isinstance() with a NewType type
345+
reveal_type(d) # E: Revealed type is '__main__.T'
346+
[builtins fixtures/isinstancelist.pyi]

test-data/unit/check-typeddict.test

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,14 @@ def set_coordinate(p: TaggedPoint, key: str, value: int) -> None:
717717

718718
-- isinstance
719719

720-
-- TODO: Implement support for this case.
721-
--[case testCannotIsInstanceTypedDictType]
720+
[case testTypedDictAndInstance]
721+
from mypy_extensions import TypedDict
722+
D = TypedDict('D', {'x': int})
723+
d: object
724+
if isinstance(d, D): # E: Cannot use isinstance() with a TypedDict type
725+
reveal_type(d) # E: Revealed type is '__main__.D'
726+
[builtins fixtures/isinstancelist.pyi]
727+
722728

723729
-- scoping
724730
[case testTypedDictInClassNamespace]

test-data/unit/fixtures/isinstancelist.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class type:
88

99
class tuple: pass
1010
class function: pass
11+
class ellipsis: pass
1112

1213
def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass
1314
def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass

0 commit comments

Comments
 (0)