Skip to content

Adds support for NamedTuple subtyping #11162

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

Merged
merged 6 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/source/kinds_of_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,29 @@ Python 3.6 introduced an alternative, class-based syntax for named tuples with t

p = Point(x=1, y='x') # Argument has incompatible type "str"; expected "int"

.. note::

You can use raw ``NamedTuple`` pseudo-class to annotate type
where any ``NamedTuple`` is expected.

For example, it can be useful for deserialization:

.. code-block:: python

def deserialize_named_tuple(arg: NamedTuple) -> Dict[str, Any]:
return arg._asdict()

Point = namedtuple('Point', ['x', 'y'])
Person = NamedTuple('Person', [('name', str), ('age', int)])

deserialize_named_tuple(Point(x=1, y=2)) # ok
deserialize_named_tuple(Person(name='Nikita', age=18)) # ok

deserialize_named_tuple((1, 2)) # Argument 1 to "deserialize_named_tuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple"

Note, that behavior is highly experimental, non-standard,
and can be not supported by other type checkers.

.. _type-of-class:

The type of class objects
Expand Down
9 changes: 7 additions & 2 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,13 @@ def visit_instance(self, left: Instance) -> bool:
rname = right.type.fullname
# Always try a nominal check if possible,
# there might be errors that a user wants to silence *once*.
if ((left.type.has_base(rname) or rname == 'builtins.object') and
not self.ignore_declared_variance):
# NamedTuples are a special case, because `NamedTuple` is not listed
# in `TypeInfo.mro`, so when `(a: NamedTuple) -> None` is used,
# we need to check for `is_named_tuple` property
if ((left.type.has_base(rname) or rname == 'builtins.object'
or (rname == 'typing.NamedTuple'
and any(l.is_named_tuple for l in left.type.mro)))
and not self.ignore_declared_variance):
# Map left type to corresponding right instances.
t = map_instance_to_supertype(left, right.type)
nominal = all(self.check_type_parameter(lefta, righta, tvar.variance)
Expand Down
87 changes: 87 additions & 0 deletions test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -973,3 +973,90 @@ B = namedtuple('X', ['a']) # E: First argument to namedtuple() should be
C = NamedTuple('X', [('a', 'Y')]) # E: First argument to namedtuple() should be "C", not "X"
class Y: ...
[builtins fixtures/tuple.pyi]

[case testNamedTupleTypeIsASuperTypeOfOtherNamedTuples]
from typing import Tuple, NamedTuple

class Bar(NamedTuple):
name: str = "Bar"

class Baz(NamedTuple):
a: str
b: str

class Biz(Baz): ...
class Other: ...
class Both1(Bar, Other): ...
class Both2(Other, Bar): ...
class Both3(Biz, Other): ...

def print_namedtuple(obj: NamedTuple) -> None:
reveal_type(obj.name) # N: Revealed type is "builtins.str"

b1: Bar
b2: Baz
b3: Biz
b4: Both1
b5: Both2
b6: Both3
print_namedtuple(b1) # ok
print_namedtuple(b2) # ok
print_namedtuple(b3) # ok
print_namedtuple(b4) # ok
print_namedtuple(b5) # ok
print_namedtuple(b6) # ok

print_namedtuple(1) # E: Argument 1 to "print_namedtuple" has incompatible type "int"; expected "NamedTuple"
print_namedtuple(('bar',)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[str]"; expected "NamedTuple"
print_namedtuple((1, 2)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple"
print_namedtuple((b1,)) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[Bar]"; expected "NamedTuple"
t: Tuple[str, ...]
print_namedtuple(t) # E: Argument 1 to "print_namedtuple" has incompatible type "Tuple[str, ...]"; expected "NamedTuple"

[builtins fixtures/tuple.pyi]
[typing fixtures/typing-namedtuple.pyi]

[case testNamedTupleTypeIsASuperTypeOfOtherNamedTuplesReturns]
from typing import Tuple, NamedTuple

class Bar(NamedTuple):
n: int

class Baz(NamedTuple):
a: str
b: str

class Biz(Bar): ...
class Other: ...
class Both1(Bar, Other): ...
class Both2(Other, Bar): ...
class Both3(Biz, Other): ...

def good1() -> NamedTuple:
b: Bar
return b
def good2() -> NamedTuple:
b: Baz
return b
def good3() -> NamedTuple:
b: Biz
return b
def good4() -> NamedTuple:
b: Both1
return b
def good5() -> NamedTuple:
b: Both2
return b
def good6() -> NamedTuple:
b: Both3
return b

def bad1() -> NamedTuple:
return 1 # E: Incompatible return value type (got "int", expected "NamedTuple")
def bad2() -> NamedTuple:
return () # E: Incompatible return value type (got "Tuple[]", expected "NamedTuple")
def bad3() -> NamedTuple:
return (1, 2) # E: Incompatible return value type (got "Tuple[int, int]", expected "NamedTuple")

[builtins fixtures/tuple.pyi]
[typing fixtures/typing-namedtuple.pyi]
17 changes: 17 additions & 0 deletions test-data/unit/fixtures/typing-namedtuple.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
TypeVar = 0
Generic = 0
Any = 0
overload = 0
Type = 0

T_co = TypeVar('T_co', covariant=True)
KT = TypeVar('KT')

class Iterable(Generic[T_co]): pass
class Iterator(Iterable[T_co]): pass
class Sequence(Iterable[T_co]): pass
class Mapping(Iterable[KT], Generic[KT, T_co]): pass

class Tuple(Sequence): pass
class NamedTuple(Tuple):
name: str
26 changes: 26 additions & 0 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,32 @@ x = X(a=1, b='s')
[out]
_testNamedTupleNew.py:12: note: Revealed type is "Tuple[builtins.int, fallback=_testNamedTupleNew.Child]"

[case testNamedTupleTypeInheritanceSpecialCase]
from typing import NamedTuple, Tuple
from collections import namedtuple

A = NamedTuple('A', [('param', int)])
B = namedtuple('B', ['param'])

def accepts_named_tuple(arg: NamedTuple):
reveal_type(arg._asdict())
reveal_type(arg._fields)
reveal_type(arg._field_defaults)

a = A(1)
b = B(1)

accepts_named_tuple(a)
accepts_named_tuple(b)
accepts_named_tuple(1)
accepts_named_tuple((1, 2))
[out]
_testNamedTupleTypeInheritanceSpecialCase.py:8: note: Revealed type is "collections.OrderedDict[builtins.str, Any]"
_testNamedTupleTypeInheritanceSpecialCase.py:9: note: Revealed type is "builtins.tuple[builtins.str]"
_testNamedTupleTypeInheritanceSpecialCase.py:10: note: Revealed type is "builtins.dict[builtins.str, Any]"
_testNamedTupleTypeInheritanceSpecialCase.py:17: error: Argument 1 to "accepts_named_tuple" has incompatible type "int"; expected "NamedTuple"
_testNamedTupleTypeInheritanceSpecialCase.py:18: error: Argument 1 to "accepts_named_tuple" has incompatible type "Tuple[int, int]"; expected "NamedTuple"

[case testNewAnalyzerBasicTypeshed_newsemanal]
from typing import Dict, List, Tuple

Expand Down