3.8. Typing NamedTuple

3.8.1. SetUp

>>> from typing import NamedTuple

3.8.2. Tuple

Problem:

>>> def hello(user):
...     print(f'Hello {user[0]} {user[1]}')
>>>
>>>
>>> mark = ('Mark', 'Watney')
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = ['Mark', 'Watney']
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = {'Mark', 'Watney'}
>>> hello(mark)
Traceback (most recent call last):
TypeError: 'set' object is not subscriptable

Solution:

>>> def hello(user: tuple):
...     print(f'Hello {user[0]} {user[1]}')
>>>
>>>
>>> mark = ('Mark', 'Watney')
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = {'Mark', 'Watney'}
>>> hello(mark)  # Expected type 'tuple', got 'set[str]' instead
Traceback (most recent call last):
TypeError: 'set' object is not subscriptable

3.8.3. Tuple[str,str]

Problem:

>>> def hello(user: tuple):
...     print(f'Hello {user[0]} {user[1]}')
>>>
>>>
>>> mark = ('Mark', 'Watney')
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = ('Mark', 'Watney', 'mwatney@nasa.gov')
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = ('Mark',)
>>> hello(mark)
Traceback (most recent call last):
IndexError: tuple index out of range

Solution:

>>> def hello(user: tuple[str,str]):
...     print(f'Hello {user[0]} {user[1]}')
>>>
>>>
>>> mark = ('Mark', 'Watney')
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = ('Mark', 'Watney', 'mwatney@nasa.gov')
>>> hello(mark)  # Expected type 'tuple[str, str]', got 'tuple[str, str, str]' instead
Hello Mark Watney
>>>
>>> mark = ('Mark',)
>>> hello(mark)  # Expected type 'tuple[str, str]', got 'tuple[str]' instead
Traceback (most recent call last):
IndexError: tuple index out of range

3.8.4. NamedTuple

>>> class User(NamedTuple):
...     firstname: str
...     lastname: str
>>>
>>>
>>> def hello(user: User):
...     print(f'Hello {user[0]} {user[1]}')
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = User(firstname='Mark', lastname='Watney')
>>> hello(mark)
Hello Mark Watney

Using NamedTuple we can also make hello() function more readable by using named attributes user.firstname and user.lastname instead of indexes, such as: user[0] and user[1]:

>>> class User(NamedTuple):
...     firstname: str
...     lastname: str
>>>
>>>
>>> def hello(user: User):
...     print(f'Hello {user.firstname} {user.lastname}')
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> hello(mark)
Hello Mark Watney
>>>
>>> mark = User(firstname='Mark', lastname='Watney')
>>> hello(mark)
Hello Mark Watney

Note, that this is a regular class so you can also use methods in it:

>>> class User(NamedTuple):
...     firstname: str
...     lastname: str
...
...     def hello(self):
...         print(f'Hello {self.firstname} {self.lastname}')
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> mark.hello()
Hello Mark Watney
>>>
>>> mark = User(firstname='Mark', lastname='Watney')
>>> mark.hello()
Hello Mark Watney

3.8.5. Default

>>> class Point(NamedTuple):
...     x: int
...     y: int
>>>
>>>
>>> pt = Point()
Traceback (most recent call last):
TypeError: Point.__new__() missing 2 required positional arguments: 'x' and 'y'
>>> class Point(NamedTuple):
...     x: int = 0
...     y: int = 0
>>>
>>>
>>> pt = Point()
>>> pt
Point(x=0, y=0)

3.8.6. Extensibility

>>> class Point(NamedTuple):
...     x: int
...     y: int
...     z: int = 0
>>> pt = Point(1, 2)
>>> pt
Point(x=1, y=2, z=0)
>>> pt = Point(1, 2, 3)
>>> pt
Point(x=1, y=2, z=3)

3.8.7. Contract

Problem:

>>> def get_user(uid):
...     return (1, 'Mark', 'Watney', 42, 178.0, 75.5, True, False, None)
>>>
>>>
>>> mark = get_user(1000)
>>>
>>> mark[1]
'Mark'
>>>
>>> mark[2]
'Watney'
>>>
>>> mark[6]
True

Tuple annotation:

>>> def get_user(uid: int) -> tuple[int,str,str,int,float,float,bool,bool,bool|None]:
...     return (1, 'Mark', 'Watney', 42, 178.0, 75.5, True, False, None)
>>>
>>>
>>> mark = get_user(1000)
>>>
>>> mark[1]
'Mark'
>>>
>>> mark[2]
'Watney'
>>>
>>> mark[6]
True

NamedTuple annotation:

>>> class User(NamedTuple):
...     id: int
...     firstname: str
...     lastname: str
...     age: int
...     height: int | float
...     weight: int | float
...     is_astronaut: bool
...     is_assigned: bool
...     mission: str | None
>>>
>>>
>>> def get_user(uid: int) -> User:
...     return User(1, 'Mark', 'Watney', 42, 178.0, 75.5, True, False, None)
>>>
>>>
>>> mark = get_user(1000)
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.is_astronaut
True
>>>
>>> mark[1]
'Mark'
>>>
>>> mark[2]
'Watney'
>>>
>>> mark[6]
True

Moreover returning values are much more readable:

>>> def get_user(uid: int) -> User:
...     return User(
...         id=1,
...         firstname='Mark',
...         lastname='Watney',
...         age=42,
...         height=178.0,
...         weight=75.5,
...         is_astronaut=True,
...         is_assigned=False,
...         mission=None)

3.8.8. Iteration

>>> class User(NamedTuple):
...     firstname: str
...     lastname: str
>>>
>>> mark = User(firstname='Mark', lastname='Watney')
>>> mark[0]
'Mark'
>>>
>>> mark[1]
'Watney'
>>> for field in mark:
...     print(field)
...
Mark
Watney

3.8.9. IsInstance

Note, that NamedTuple is still a tuple and you can compare both!

>>> class User(NamedTuple):
...     firstname: str
...     lastname: str
>>>
>>> mark = User(firstname='Mark', lastname='Watney')
>>> isinstance(mark, tuple)
True
>>> type(mark)
<class '__main__.User'>
>>> User.mro()
[<class '__main__.User'>, <class 'tuple'>, <class 'object'>]

3.8.10. Equality

>>> class User(NamedTuple):
...     firstname: str
...     lastname: str
>>>
>>>
>>> a = ('Mark', 'Watney')
>>> b = User('Mark', 'Watney')
>>> c = User(firstname='Mark', lastname='Watney')

Equality:

>>> a == b
True
>>>
>>> a == c
True
>>>
>>> b == c
True

Identity:

>>> a is b
False
>>>
>>> a is c
False
>>>
>>> b is c
False

3.8.11. Size

>>> from sys import getsizeof
>>>
>>>
>>> class User(NamedTuple):
...     firstname: str
...     lastname: str
>>>
>>>
>>> a = ('Mark', 'Watney')
>>> b = User('Mark', 'Watney')
>>> c = User(firstname='Mark', lastname='Watney')
>>> getsizeof(a)
56
>>>
>>> getsizeof(b)
56
>>>
>>> getsizeof(c)
56

3.8.12. Use Case - 0x01

>>> class Point(NamedTuple):
...     x: int = 0
...     y: int = 0
>>>
>>>
>>> class Position:
...     position: Point
...
...     def __init__(self, initial_position: Point = Point()):
...         self.position = initial_position
...
...     def set_position(self, position: Point) -> None:
...         self.position = position
...
...     def get_position(self) -> Point:
...         return self.position
>>>
>>>
>>> current = Position()
>>>
>>> current.get_position()
Point(x=0, y=0)
>>>
>>> current.set_position(Point(1, 2))
>>>
>>> current.get_position()
Point(x=1, y=2)

3.8.13. Use Case - 0x02

>>> class GeographicCoordinate(NamedTuple):
...     latitude: float
...     longitude: float
>>>
>>>
>>> locations: list[tuple[float,float]] = [
...     (25.91375, -60.15503),
...     (-11.01983, -166.48477),
...     (-11.01983, -166.48477),
... ]
>>>
>>> locations: list[GeographicCoordinate] = [
...     GeographicCoordinate(25.91375, -60.15503),
...     GeographicCoordinate(-11.01983, -166.48477),
...     GeographicCoordinate(-11.01983, -166.48477),
... ]
>>>
>>> locations: list[GeographicCoordinate] = [
...     GeographicCoordinate(latitude=25.91375, longitude=-60.15503),
...     GeographicCoordinate(latitude=-11.01983, longitude=-166.48477),
...     GeographicCoordinate(latitude=-11.01983, longitude=-166.48477),
... ]

3.8.14. Use Case - 0x03

>>> from itertools import starmap
>>> from pprint import pprint
>>>
>>> DATA = [
...     ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
...     (5.8, 2.7, 5.1, 1.9, 'virginica'),
...     (5.1, 3.5, 1.4, 0.2, 'setosa'),
...     (5.7, 2.8, 4.1, 1.3, 'versicolor'),
...     (6.3, 2.9, 5.6, 1.8, 'virginica'),
...     (6.4, 3.2, 4.5, 1.5, 'versicolor'),
...     (4.7, 3.2, 1.3, 0.2, 'setosa'),
...     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
...     (7.6, 3.0, 6.6, 2.1, 'virginica'),
...     (4.9, 3.0, 1.4, 0.2, 'setosa'),
...     (4.9, 2.5, 4.5, 1.7, 'virginica'),
...     (7.1, 3.0, 5.9, 2.1, 'virginica'),
...     (4.6, 3.4, 1.4, 0.3, 'setosa'),
...     (5.4, 3.9, 1.7, 0.4, 'setosa'),
...     (5.7, 2.8, 4.5, 1.3, 'versicolor'),
...     (5.0, 3.6, 1.4, 0.3, 'setosa'),
...     (5.5, 2.3, 4.0, 1.3, 'versicolor'),
...     (6.5, 3.0, 5.8, 2.2, 'virginica'),
...     (6.5, 2.8, 4.6, 1.5, 'versicolor'),
...     (6.3, 3.3, 6.0, 2.5, 'virginica'),
...     (6.9, 3.1, 4.9, 1.5, 'versicolor'),
...     (4.6, 3.1, 1.5, 0.2, 'setosa'),
... ]
>>> class Iris(NamedTuple):
...     sl: float
...     sw: float
...     pl: float
...     pw: float
...     species: str
>>> result = starmap(Iris, DATA[1:])
>>> data = list(result)
>>> pprint(data)
[Iris(sl=5.8, sw=2.7, pl=5.1, pw=1.9, species='virginica'),
 Iris(sl=5.1, sw=3.5, pl=1.4, pw=0.2, species='setosa'),
 Iris(sl=5.7, sw=2.8, pl=4.1, pw=1.3, species='versicolor'),
 Iris(sl=6.3, sw=2.9, pl=5.6, pw=1.8, species='virginica'),
 Iris(sl=6.4, sw=3.2, pl=4.5, pw=1.5, species='versicolor'),
 Iris(sl=4.7, sw=3.2, pl=1.3, pw=0.2, species='setosa'),
 Iris(sl=7.0, sw=3.2, pl=4.7, pw=1.4, species='versicolor'),
 Iris(sl=7.6, sw=3.0, pl=6.6, pw=2.1, species='virginica'),
 Iris(sl=4.9, sw=3.0, pl=1.4, pw=0.2, species='setosa'),
 Iris(sl=4.9, sw=2.5, pl=4.5, pw=1.7, species='virginica'),
 Iris(sl=7.1, sw=3.0, pl=5.9, pw=2.1, species='virginica'),
 Iris(sl=4.6, sw=3.4, pl=1.4, pw=0.3, species='setosa'),
 Iris(sl=5.4, sw=3.9, pl=1.7, pw=0.4, species='setosa'),
 Iris(sl=5.7, sw=2.8, pl=4.5, pw=1.3, species='versicolor'),
 Iris(sl=5.0, sw=3.6, pl=1.4, pw=0.3, species='setosa'),
 Iris(sl=5.5, sw=2.3, pl=4.0, pw=1.3, species='versicolor'),
 Iris(sl=6.5, sw=3.0, pl=5.8, pw=2.2, species='virginica'),
 Iris(sl=6.5, sw=2.8, pl=4.6, pw=1.5, species='versicolor'),
 Iris(sl=6.3, sw=3.3, pl=6.0, pw=2.5, species='virginica'),
 Iris(sl=6.9, sw=3.1, pl=4.9, pw=1.5, species='versicolor'),
 Iris(sl=4.6, sw=3.1, pl=1.5, pw=0.2, species='setosa')]
>>> data[0]
Iris(sl=5.8, sw=2.7, pl=5.1, pw=1.9, species='virginica')
>>>
>>> data[0].sl
5.8
>>> data[0].species
'virginica'
>>>
>>> tuple(data[0])
(5.8, 2.7, 5.1, 1.9, 'virginica')

3.8.15. Further Reading

3.8.16. References

3.8.17. Assignments

Code 3.41. Solution
"""
* Assignment: Typing Annotations NamedTuple
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Declare proper types for variables
    2. Use `NamedTuple`
    3. Run doctests - all must succeed

Polish:
    1. Zadeklaruj odpowiedni typ zmiennych
    2. Użyj `NamedTuple`
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> assert data == User('Mark', 'Watney', 42), \
    'Do not modify variable `data` value, just add type annotation'
"""

# Declare proper types for variables
class User:
    ...

# Do not modify lines below
data: User = ('Mark', 'Watney', 42)