• Almost everything in Python is an object, with its properties and methods.
  • JavaScript have primitive type concept. But Python don’t.
  • Python is dynamic type language.
  • Dynamic typing have it’s shortcoming.
  • Python provide library to help type check.
  • Must Clear Concepts that are related to type: Generic type, interface, ABC, type annotations.

Classes vs Types

History

Once upon a time, Python had both types and classes. Types were built-in objects defined in C; classes were what you built when using a class statement. The two were named differently because you couldn’t mix these; classes could not extend types.

This difference was artificial, a limitation in the language implementation. Starting with Python 2.2, the developers of Python have slowly moved towards unifying the two concepts, with the difference all but gone in Python 3. Built-in types are now also labelled classes, and you can extend them at will.

built-in function type()

type(obj)

type(obj) built-in function can return the class of the obj.

type(classname, superclasses, attributes_dict)

type() can be called with three parameters:

  • If type is called with three arguments, it will return a new type object. This provides us with a dynamic form of the class statement.
  • classname is a string defining the class name and becomes the name attribute;
  • superclasses is a list or tuple with the superclasses of our class. This list or tuple will become the bases attribute;
  • attributes_dict is a dictionary, functioning as the namespace of our class. It contains the definitions for the class body and it becomes the dict attribute.

Type Hints (pep-0483)

https://peps.python.org/pep-0483/ pep-0484

type checkers

type checker for Python is third library for now. e.g. Mypy. (PyCharm have type checker as well)

It is important for the user to be able to define types in a form that can be understood by type checkers. The goal of this PEP 0483 is to propose such a systematic way of defining types for type annotations of variables and functions using PEP 3107 syntax.

Gradual typing

  • dynamic type language, like python, javascript, perl, etc. is very popular, Static type langage, like Java, C++, C#, C is very popular as well, The main reason is both of them have some good points and bad points. They can’t substitute each other.

  • Gradual typing is a type system developed by Jeremy Siek and Walid Taha in 2006

  • Gradual typing allows parts of a program to be dynamically typed and other parts to be statically typed.
  • The programmer controls which parts are which by either leaving out type annotations or by adding them in. Thus leverage desirable aspects of both dynamic and static typing.

Subtype? what is Subtype relationships?

A crucial notion for static type checker is the subtype relationship.

It arises from the question:

If first_var has type first_type, and second_var has type second_type, is it safe to assign first_var = second_var? (It is safe when second_type is subtype of first_type)

A strong criterion for when it should be safe is:

  • every value from second_type is also in the set of values of first_type; and
  • every function from first_type is also in the set of functions of second_type.

By this definition:

  • Every type is a subtype of itself.
  • The set of values becomes smaller for subtype, while the set of functions becomes larger.

#### Subtype concept Conclusion

  • If B is subtype of A, then var_A = var_B is safe. 
  • The values of subtype will be smaller, but functions will be larger. - Every type is a subtype of itself.
  • Int is subtype of real number.
  • But List[int] is not a subtype of List[float]. Although list[int] value is set of list[float], but  appending a real number only works with List[float]

A formal example:

Integers are subtype of real numbers. Indeed, every integer is of course also a real number, and integers support more operations, such as, e.g., bitwise shifts « and »

lucky_number = 3.14    # type: float
lucky_number = 42      # Safe
lucky_number * 2       # This works
lucky_number << 5      # Fails

unlucky_number = 13    # type: int
unlucky_number << 5    # This works
unlucky_number = 2.72  # Unsafe

#### Two widespread approaches to declare subtype information to type checker. In nominal subtyping, the type tree is based on the class tree. But this approach should be used under control of the type checker, because in Python one can override attributes in an incompatible way:

class Base:
    answer = '42' # type: str
class Derived(Base):
    answer = 5 # should be marked as error by type checker

In structural subtyping the subtype relation is deduced from the declared methods.

#### is-consistent-with vs. is-subtype-of is-consistent-with, which is similar to is-subtype-of, except new type Any

The is-consistent-with relationship is defined by three rules:

  • A type t1 is consistent with a type t2 if t1 is a subtype of t2. (But not the other way around.)
  • Any is consistent with every type. (But Any is not a subtype of every type.)
  • Every type is consistent with Any. (But every type is not a subtype of Any.)

Types vs. Classes

Class is a dynamic, runtime concept.

the distinction between classes and types the following general rules apply:

  • No types defined below (i.e. Any, Union, etc.) can be instantiated, an attempt to do so will raise TypeError. (But non-abstract subclasses of Generic can be.)
  • No types defined below can be subclassed, except for Generic and classes derived from it.
  • All of these will raise TypeError if they appear in isinstance or issubclass (except for unparameterized generics).

types appear in variable and function type annotations, can be constructed from building blocks described below, and are used by static type checkers.

  • Any.
  • Union[t1, t2, …].
  • Optional[t1]
  • Tuple[t1, t2, …, tn].
  • Callable[[t1, t2, …, tn], tr]
  • Intersection[t1, t2, … ]

Generic type

What is Generic programming

Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.

Why Generic

  • Stronger type checks at compile time.
  • Enabling programmers to implement generic algorithms.

What is Generic type in Python

  • Use generic type constructor to construct new types in a generic manner. It is common when a particular class or a function behaves in such a type generic manner.
  • Using Union[t1, t2,…], Callable[[t1, t2, …, tn], tr] or Tuple[t1, t2, …, tn], etc. to construct new types.
  • For example, Tuple can take concrete type float and make a concrete type Vector = Tuple[float, …], Such semantics is known as generic type constructor
  • it is similar to semantics of functions, but a function takes a value and returns a value, while generic type constructor takes a type and “returns” a type.

Generic type Example1:

Container classes, such as list or dict, typically contain only values of a particular type. Therefore, a user might want to type annotate them as such:

users = [] # type: List[UserID]
users.append(UserID(42)) # OK
users.append('Some guy') # Should be rejected by the type checker

examples = {} # type: Dict[str, Any]
examples['first example'] = object() # OK
examples[2] = None   

To allow type annotations in these situations, built-in containers and container abstract base classes are extended with type parameters, so that they behave as generic type constructors. Classes, that behave as generic type constructors are called generic types. Example:

from typing import Iterable

class Task:
    ...
def work(todo_list: Iterable[Task]) -> None:
    ...

Here Iterable is a generic type that takes a concrete type Task and returns a concrete type Iterable[Task].

Generic function example:

The following function can take two arguments of type int and return an int, or take two arguments of type float and return a float, etc.:

def add(x, y):
    return x + y

add(1, 2) == 3
add('1', '2') == '12'
add(2.7, 3.5) == 6.2

Functions that behave in the type generic manner are called generic functions. Type annotations of generic functions are allowed by type variables. Their semantics with respect to generic types is somewhat similar to semantics of parameters in functions. But one does not assign concrete types to type variables, it is the task of a static type checker to find their possible values and warn the user if it cannot find. Example:

def take_first(seq: Sequence[T]) -> T: # a generic function
    return seq[0]

accumulator = 0 # type: int

accumulator += take_first([1, 2, 3])   # Safe, T deduced to be int
accumulator += take_first((2.7, 3.5))  # Unsafe

Type variables are used extensively in type annotations, also internal machinery of the type inference in type checkers is typically build on type variables

Type variables

X = TypeVar('X') declares a unique type variable. (TypeVar() is defined in typing.py)

  • The name must match the variable name.
  • By default, a type variable ranges over all possible types.
  • type variables Example: (it work for PyCharm’s static type checker)
from typing import TypeVar
T = TypeVar('T')
def do_nothing(one_arg: T, other_arg: T) -> None:
    pass

do_nothing(1, 2)               # OK, T is int
do_nothing('abc', UserID(42))  # also OK, T is object

Y = TypeVar('Y', t1, t2, ...). Ditto, constrained to t1, etc. Behaves similar to Union[t1, t2, ...].

Function type annotation with a constrained type variable:

from typing import TypeVar
AnyStr = TypeVar('AnyStr', str, bytes)
def longest(first: AnyStr, second: AnyStr) -> AnyStr:
    return first if len(first) >= len(second) else second

result = longest('a', 'abc')  # The inferred type for result is str
result1 = longest('a', b'abc')  # Fails static type check

both arguments to longest() must have the same type (str or bytes)

Defining your own generic types

  • Users can declare their classes as generic types using the special building block Generic. The definition class MyGeneric(Generic[X, Y, …]): … defines a generic type MyGeneric over type variables X, etc.
  • MyGeneric itself becomes parameterizable, e.g. MyGeneric[int, str, …] is a specific type with substitutions X -> int, etc.

Example:

from typing import Generic, Optional, TypeVar
T = TypeVar('T')
class CustomQueue(Generic[T]):
    def put(self, task: T) -> None:
        ...
    def get(self) -> T:
        ...

def communicate(queue: CustomQueue[str]) -> Optional[str]:
    ...
  • Classes that derive from generic types become generic.
  • A class can subclass multiple generic types.
  • However, classes derived from specific types returned by generics are not generic.

Examples:

class TodoList(Iterable[T], Container[T]):
    def check(self, item: T) -> None:
        ...

def check_all(todo: TodoList[T]) -> None:  # TodoList is generic
    ...

class URLList(Iterable[bytes]):
    def scrape_all(self) -> None:
        ...

def search(urls: URLList) -> Optional[bytes]  # URLList is not generic
    ...

Covariance and Contravariance concepts

If t2 is a subtype of t1, then a generic type constructor GenType is called:

  • Covariant, if GenType[t2] is a subtype of GenType[t1] for all such t1 and t2.
  • Contravariant, if GenType[t1] is a subtype of GenType[t2] for all such t1 and t2.
  • Invariant, if neither of the above is true.

Predefined generic types and Protocols in typing.py

Like:

  • Any.
  • Union[t1, t2, …].
  • Optional[t1]
  • Tuple[t1, t2, …, tn].
  • Callable[[t1, t2, …, tn], tr]
  • Intersection[t1, t2, … ]