Skip to content

Commit fbf2188

Browse files
authored
Adds support for NamedTuple subtyping (#11162)
Closes #11160
1 parent 06cd503 commit fbf2188

File tree

5 files changed

+160
-2
lines changed

5 files changed

+160
-2
lines changed

docs/source/kinds_of_types.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,29 @@ Python 3.6 introduced an alternative, class-based syntax for named tuples with t
566566
567567
p = Point(x=1, y='x') # Argument has incompatible type "str"; expected "int"
568568
569+
.. note::
570+
571+
You can use raw ``NamedTuple`` pseudo-class to annotate type
572+
where any ``NamedTuple`` is expected.
573+
574+
For example, it can be useful for deserialization:
575+
576+
.. code-block:: python
577+
578+
def deserialize_named_tuple(arg: NamedTuple) -> Dict[str, Any]:
579+
return arg._asdict()
580+
581+
Point = namedtuple('Point', ['x', 'y'])
582+
Person = NamedTuple('Person', [('name', str), ('age', int)])
583+
584+
deserialize_named_tuple(Point(x=1, y=2)) # ok
585+
deserialize_named_tuple(Person(name='Nikita', age=18)) # ok
586+
587+
deserialize_named_tuple((1, 2)) # Argument 1 to "deserialize_named_tuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple"
588+
589+
Note, that behavior is highly experimental, non-standard,
590+
and can be not supported by other type checkers.
591+
569592
.. _type-of-class:
570593

571594
The type of class objects

mypy/subtypes.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,13 @@ def visit_instance(self, left: Instance) -> bool:
263263
rname = right.type.fullname
264264
# Always try a nominal check if possible,
265265
# there might be errors that a user wants to silence *once*.
266-
if ((left.type.has_base(rname) or rname == 'builtins.object') and
267-
not self.ignore_declared_variance):
266+
# NamedTuples are a special case, because `NamedTuple` is not listed
267+
# in `TypeInfo.mro`, so when `(a: NamedTuple) -> None` is used,
268+
# we need to check for `is_named_tuple` property
269+
if ((left.type.has_base(rname) or rname == 'builtins.object'
270+
or (rname == 'typing.NamedTuple'
271+
and any(l.is_named_tuple for l in left.type.mro)))
272+
and not self.ignore_declared_variance):
268273
# Map left type to corresponding right instances.
269274
t = map_instance_to_supertype(left, right.type)
270275
nominal = all(self.check_type_parameter(lefta, righta, tvar.variance)

test-data/unit/check-namedtuple.test

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,3 +973,90 @@ B = namedtuple('X', ['a']) # E: First argument to namedtuple() should be
973973
C = NamedTuple('X', [('a', 'Y')]) # E: First argument to namedtuple() should be "C", not "X"
974974
class Y: ...
975975
[builtins fixtures/tuple.pyi]
976+
977+
[case testNamedTupleTypeIsASuperTypeOfOtherNamedTuples]
978+
from typing import Tuple, NamedTuple
979+
980+
class Bar(NamedTuple):
981+
name: str = "Bar"
982+
983+
class Baz(NamedTuple):
984+
a: str
985+
b: str
986+
987+
class Biz(Baz): ...
988+
class Other: ...
989+
class Both1(Bar, Other): ...
990+
class Both2(Other, Bar): ...
991+
class Both3(Biz, Other): ...
992+
993+
def print_namedtuple(obj: NamedTuple) -> None:
994+
reveal_type(obj.name) # N: Revealed type is "builtins.str"
995+
996+
b1: Bar
997+
b2: Baz
998+
b3: Biz
999+
b4: Both1
1000+
b5: Both2
1001+
b6: Both3
1002+
print_namedtuple(b1) # ok
1003+
print_namedtuple(b2) # ok
1004+
print_namedtuple(b3) # ok
1005+
print_namedtuple(b4) # ok
1006+
print_namedtuple(b5) # ok
1007+
print_namedtuple(b6) # ok
1008+
1009+
print_namedtuple(1) # E: Argument 1 to "print_namedtuple" has incompatible type "int"; expected "NamedTuple"
1010+
print_namedtuple(('bar',)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[str]"; expected "NamedTuple"
1011+
print_namedtuple((1, 2)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple"
1012+
print_namedtuple((b1,)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[Bar]"; expected "NamedTuple"
1013+
t: Tuple[str, ...]
1014+
print_namedtuple(t) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[str, ...]"; expected "NamedTuple"
1015+
1016+
[builtins fixtures/tuple.pyi]
1017+
[typing fixtures/typing-namedtuple.pyi]
1018+
1019+
[case testNamedTupleTypeIsASuperTypeOfOtherNamedTuplesReturns]
1020+
from typing import Tuple, NamedTuple
1021+
1022+
class Bar(NamedTuple):
1023+
n: int
1024+
1025+
class Baz(NamedTuple):
1026+
a: str
1027+
b: str
1028+
1029+
class Biz(Bar): ...
1030+
class Other: ...
1031+
class Both1(Bar, Other): ...
1032+
class Both2(Other, Bar): ...
1033+
class Both3(Biz, Other): ...
1034+
1035+
def good1() -> NamedTuple:
1036+
b: Bar
1037+
return b
1038+
def good2() -> NamedTuple:
1039+
b: Baz
1040+
return b
1041+
def good3() -> NamedTuple:
1042+
b: Biz
1043+
return b
1044+
def good4() -> NamedTuple:
1045+
b: Both1
1046+
return b
1047+
def good5() -> NamedTuple:
1048+
b: Both2
1049+
return b
1050+
def good6() -> NamedTuple:
1051+
b: Both3
1052+
return b
1053+
1054+
def bad1() -> NamedTuple:
1055+
return 1 # E: Incompatible return value type (got "int", expected "NamedTuple")
1056+
def bad2() -> NamedTuple:
1057+
return () # E: Incompatible return value type (got "Tuple[]", expected "NamedTuple")
1058+
def bad3() -> NamedTuple:
1059+
return (1, 2) # E: Incompatible return value type (got "Tuple[int, int]", expected "NamedTuple")
1060+
1061+
[builtins fixtures/tuple.pyi]
1062+
[typing fixtures/typing-namedtuple.pyi]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
TypeVar = 0
2+
Generic = 0
3+
Any = 0
4+
overload = 0
5+
Type = 0
6+
7+
T_co = TypeVar('T_co', covariant=True)
8+
KT = TypeVar('KT')
9+
10+
class Iterable(Generic[T_co]): pass
11+
class Iterator(Iterable[T_co]): pass
12+
class Sequence(Iterable[T_co]): pass
13+
class Mapping(Iterable[KT], Generic[KT, T_co]): pass
14+
15+
class Tuple(Sequence): pass
16+
class NamedTuple(Tuple):
17+
name: str

test-data/unit/pythoneval.test

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,32 @@ x = X(a=1, b='s')
13901390
[out]
13911391
_testNamedTupleNew.py:12: note: Revealed type is "Tuple[builtins.int, fallback=_testNamedTupleNew.Child]"
13921392

1393+
[case testNamedTupleTypeInheritanceSpecialCase]
1394+
from typing import NamedTuple, Tuple
1395+
from collections import namedtuple
1396+
1397+
A = NamedTuple('A', [('param', int)])
1398+
B = namedtuple('B', ['param'])
1399+
1400+
def accepts_named_tuple(arg: NamedTuple):
1401+
reveal_type(arg._asdict())
1402+
reveal_type(arg._fields)
1403+
reveal_type(arg._field_defaults)
1404+
1405+
a = A(1)
1406+
b = B(1)
1407+
1408+
accepts_named_tuple(a)
1409+
accepts_named_tuple(b)
1410+
accepts_named_tuple(1)
1411+
accepts_named_tuple((1, 2))
1412+
[out]
1413+
_testNamedTupleTypeInheritanceSpecialCase.py:8: note: Revealed type is "collections.OrderedDict[builtins.str, Any]"
1414+
_testNamedTupleTypeInheritanceSpecialCase.py:9: note: Revealed type is "builtins.tuple[builtins.str]"
1415+
_testNamedTupleTypeInheritanceSpecialCase.py:10: note: Revealed type is "builtins.dict[builtins.str, Any]"
1416+
_testNamedTupleTypeInheritanceSpecialCase.py:17: error: Argument 1 to "accepts_named_tuple" has incompatible type "int"; expected "NamedTuple"
1417+
_testNamedTupleTypeInheritanceSpecialCase.py:18: error: Argument 1 to "accepts_named_tuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple"
1418+
13931419
[case testNewAnalyzerBasicTypeshed_newsemanal]
13941420
from typing import Dict, List, Tuple
13951421

0 commit comments

Comments
 (0)