3.10. Typing Type

  • All classes are types

>>> class Account:
...     firstname: str
...     lastname: str
...
...     def __init__(self, firstname: str, lastname: str) -> None:
...         self.firstname = firstname
...         self.lastname = lastname
...
...     def login(self, username: str, password: str | None = None) -> None:
...         ...
...
...     def logout(self) -> None:
...         ...
>>>
>>>
>>> class User(Account):
...     pass
>>>
>>> class Admin(Account):
...     pass
>>>
>>>
>>> mark: Account = User('Mark', 'Watney')
>>> melissa: Account = Admin('Melissa', 'Lewis')

3.10.1. Instance

>>> class User:
...     pass
>>>
>>>
>>> a: User = User()
>>> b: User = User()

3.10.2. Inheritance

  • Dependency Inversion Principle

  • Always depend upon abstraction not an implementation

  • More information in OOP SOLID

>>> class Account:
...     pass
>>>
>>> class User(Account):
...     pass
>>>
>>> class Admin(Account):
...     pass
>>>
>>>
>>> a: Account = User()
>>> b: Account = Admin()

3.10.3. Instance Variables

>>> class User:
...     firstname: str
...     lastname: str
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'

3.10.4. Class Variables

>>> from typing import ClassVar
>>>
>>> class User:
...     AGE_MIN: ClassVar[int] = 18
...     AGE_MAX: ClassVar[int] = 65

3.10.5. Method Return Type

>>> class User:
...     def login(self) -> None:
...         ...

3.10.6. Required Method Arguments

>>> class User:
...     def login(self, username: str, password: str):
...         ...

3.10.7. Optional Method Arguments

>>> class User:
...     def login(self, username: str, password: str | None = None):
...         ...

3.10.8. Init Method

>>> class User:
...     firstname: str
...     lastname: str
...
...     def __init__(self, firstname: str, lastname: str) -> None:
...         self.firstname = firstname
...         self.lastname = lastname

3.10.9. Composition

>>> class Group:
...     name: str
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     group: Group

3.10.10. Aggregation

>>> class Group:
...     name: str
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     group: list[Group]

3.10.11. Forward Reference Problem

>>> class User:  
...     firstname: str
...     lastname: str
...     friends: list[User]
...
Traceback (most recent call last):
NameError: name 'User' is not defined

3.10.12. Forward Reference Solution: Str

  • Annotations can be a str

>>> class User:
...     firstname: str
...     lastname: str
...     friends: list['User']
>>> class User:
...     firstname: str
...     lastname: str
...     friends: 'list[User]'
>>> class User:
...     firstname: 'str'
...     lastname: 'str'
...     friends: 'list[User]'

3.10.13. Forward Reference Solution: Future Annotations

  • Since Python 3.7

  • from __future__ import annotations

>>> from __future__ import annotations
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     friends: list[User]
>>> User.__annotations__
{'firstname': <class 'str'>, 'lastname': <class 'str'>, 'friends': list[__main__.User]}

3.10.14. Forward Reference Solution: Self

>>> from typing import Self
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     friends: list[Self]
>>> User.__annotations__
{'firstname': <class 'str'>, 'lastname': <class 'str'>, 'friends': list[typing.Self]}

3.10.15. Final Class

  • Since Python 3.8: PEP 591 -- Adding a final qualifier to typing

  • There is no runtime checking of these properties

The following code demonstrates how to use @final decorator to mark class as final:

>>> from typing import final
>>>
>>> @final
... class User:
...     pass
>>>
>>> class Admin(User):  # error
...     pass

The code above will yield with an error: 'User' is marked as '@final' and should not be subclassed.

3.10.16. Final Method

  • Since Python 3.8: PEP 591 -- Adding a final qualifier to typing

  • There is no runtime checking of these properties

The following code demonstrates how to use @final decorator to mark method as final:

>>> from typing import final
>>>
>>> class User:
...     @final
...     def login(self):
...         pass
>>>
>>> class Admin(User):
...     def login(self):  # error
...         pass

The code above will yield with an error: 'User.login' is marked as '@final' and should not be overridden:

3.10.17. Final Attribute

The following code demonstrates how to use Final class to mark attribute as final:

>>> from typing import Final
>>>
>>> class User:
...     username: Final[str]
...     password: str
...
...     def __init__(self, username, password):
...         self.username = username
...         self.password = password
...
...     def change_password(self, new_password):
...         self.password = new_password

The following code will yield with an error: 'Final' name should be initialized with a value:

>>> class User:
...     username: Final[str]
...     password: str

3.10.18. Override

  • Method need to be overriden in child class

>>> from typing import override
>>>
>>>
>>> class Account:
...     @override
...     def login(self):
...         ...
>>>
>>> class User(Account):
...     def login(self):
...         print('ok')

3.10.19. Overload

  • Signs that a method is overloaded on purpose

  • It is not a mistake, that it has the same name as in base class

>>> from typing import overload
>>>
>>>
>>> class Account:
...     def login(self):
...         ...
>>>
>>> class User(Account):
...     @overload
...     def login(self):
...         print('ok')

3.10.20. Override and Overload

>>> class Account:
...     @override
...     def login(self):
...         ...
>>>
>>> class User(Account):
...     @overload
...     def login(self):
...         print('ok')

3.10.21. Recap

  • Type annotations

  • Type hints

  • Optional, but a good practice

  • Python does not verify if values have correct types

  • IDEs such as PyCharm, VS Code or tools such as mypy can do that

No Attribute Definition:

>>> class User:
...     pass

Basic Types:

>>> class User:
...     firstname: str
...     lastname: str
...     age: int

Union:

>>> class User:
...     firstname: str
...     lastname: str
...     age: int | float

Optional:

>>> class User:
...     firstname: str
...     lastname: str
...     age: int | float
...     height: float | None
...     weight: float | None

Sequences:

>>> class User:
...     firstname: str
...     lastname: str
...     age: int
...     groups: list[str]

Relation One to One:

>>> class Group:
...     gid: int
...     name: int
>>>
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     group: Group

Relation One to Many:

>>> class Group:
...     gid: int
...     name: str
>>>
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     groups: list[Group]

3.10.22. Use Case - 1

>>> class Point:
...     x: int
...     y: int
...     z: int

3.10.23. Use Case - 2

>>> class Date:
...     year: int
...     month: int
...     day: int

3.10.24. Use Case - 3

>>> class Laptop:
...     cpu: str
...     ram: str
...     ssd: str

3.10.25. Use Case - 4

>>> class Iris:
...     features: list[float]
...     label: str

3.10.26. Use Case - 5

>>> class Iris:
...     sepal_length: float
...     sepal_width: float
...     petal_length: float
...     petal_width: float
...     species: str

3.10.27. Use Case - 6

>>> class User:
...     firstname: str
...     lastname: str
...     email: str
...     active: bool
...     age: int | float
...     height: float | None
...     weight: float | None
...     groups: list[str] | None
...     friends: list['User'] | None

3.10.28. Use Case - 7

>>> from datetime import date
>>> from typing import Literal
>>>
>>>
>>> class Address:
...     type: Literal['home', 'work']
...     street: str
...     house: str
...     apartment: str
...     post_code: str
...     city: str
...     region: str
...     country: str
>>>
>>>
>>> class PhoneNumber:
...     type: Literal['home', 'work', 'mobile']
...     number: str
>>>
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     age: int | float
...     birthdate: date
...     gender: Literal['male', 'female']
...     height: float | None
...     weight: float | None
...     education: list[str] | None
...     job: str | None
...     addresses: list[Address] | None
...     emails: list[str] | None
...     phones: PhoneNumber | None
...     friends: list['Person'] | None

3.10.29. Use Case - 8

>>> class Point:
...     x: int
...     y: int
...
...     def set_coordinates(self, x: int, y: int) -> None:
...         self.x = x
...         self.y = y
...
...     def get_coordinates(self) -> tuple[int,int]:
...         return self.x, self.y

3.10.30. Use Case - 9

>>> class Point:
...     x: int
...     y: int
...
...     def __init__(self, x: int = 0, y: int = 0) -> None:
...         self.x = x
...         self.y = y
>>>
>>>
>>> class Position:
...     position: Point
...
...     def __init__(self, initial_position: Point = Point()) -> None:
...         self.position = initial_position
...
...     def get_coordinates(self) -> Point:
...         return self.position

3.10.31. Use Case - 10

  • SOLID Dependency Inversion Principle

>>> class Cache:
...     pass
>>>
>>> class DatabaseCache(Cache):
...     pass
>>>
>>> class MemoryCache(Cache):
...     pass
>>>
>>> class FilesystemCache(Cache):
...     pass
>>>
>>>
>>> a: Cache = DatabaseCache()
>>> b: Cache = MemoryCache()
>>> c: Cache = FilesystemCache()

3.10.32. Use Case - 11

>>> class Iris:
...     def __init__(self, features: list[float], label: str) -> None:
...         self.features: list[float] = features
...         self.label: str = label
>>>
>>> data: list[Iris] = [
...     Iris([4.7, 3.2, 1.3, 0.2], 'setosa'),
...     Iris([7.0, 3.2, 4.7, 1.4], 'versicolor'),
...     Iris([7.6, 3.0, 6.6, 2.1], 'virginica'),
... ]

3.10.33. Use Case - 12

>>> from typing import ClassVar
>>>
>>> class Settings:
...     RESOLUTION_X_MIN: ClassVar[int] = 0
...     RESOLUTION_X_MAX: ClassVar[int] = 1024
...     RESOLUTION_Y_MIN: ClassVar[int] = 0
...     RESOLUTION_Y_MAX: ClassVar[int] = 768

3.10.34. Use Case - 13

>>> from typing import ClassVar
>>>
>>>
>>> class Position:
...     x: int
...     y: int
>>>
>>>
>>> class Hero:
...     HEALTH_MIN: ClassVar[int] = 50
...     HEALTH_MAX: ClassVar[int] = 100
...     GOLD_MIN: ClassVar[int] = 50
...     GOLD_MAX: ClassVar[int] = 100
...     DAMAGE_MIN: ClassVar[int] = 5
...     DAMAGE_MAX: ClassVar[int] = 20
...     name: str
...     health: int
...     gold: int
...     position: Position