Skip to content

Commit

Permalink
Add the typing spec (#1517)
Browse files Browse the repository at this point in the history
Copied over from https://github.com/JelleZijlstra/typing-spec with some further improvements

The text largely derives from the typing PEPs, though I added some
material. The organization is based on an outline by @erictraut.
@rchen152 and @Daverball made contributions to the text.
  • Loading branch information
JelleZijlstra authored Dec 11, 2023
1 parent 0c89ed5 commit 7117775
Show file tree
Hide file tree
Showing 21 changed files with 7,805 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ Reference
the docs -- since the Python typing system is standardised via PEPs, this
information should apply to most Python type checkers.

Specification
=============

.. toctree::
:maxdepth: 2

spec/index

Indices and tables
==================

Expand Down
189 changes: 189 additions & 0 deletions docs/spec/aliases.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
Type aliases
============

(See :pep:`613` for the introduction of ``TypeAlias``, and
:pep:`695` for the ``type`` statement.)

Type aliases may be defined by simple variable assignments::

Url = str

def retry(url: Url, retry_count: int) -> None: ...

Or by using ``typing.TypeAlias``::

from typing import TypeAlias

Url: TypeAlias = str

def retry(url: Url, retry_count: int) -> None: ...

Or by using the ``type`` statement (Python 3.12 and higher)::

type Url = str

def retry(url: Url, retry_count: int) -> None: ...

Note that we recommend capitalizing alias names, since they represent
user-defined types, which (like user-defined classes) are typically
spelled that way.

Type aliases may be as complex as type hints in annotations --
anything that is acceptable as a type hint is acceptable in a type
alias::

from typing import TypeVar
from collections.abc import Iterable

T = TypeVar('T', bound=float)
Vector = Iterable[tuple[T, T]]

def inproduct(v: Vector[T]) -> T:
return sum(x*y for x, y in v)
def dilate(v: Vector[T], scale: T) -> Vector[T]:
return ((x * scale, y * scale) for x, y in v)
vec: Vector[float] = []


This is equivalent to::

from typing import TypeVar
from collections.abc import Iterable

T = TypeVar('T', bound=float)

def inproduct(v: Iterable[tuple[T, T]]) -> T:
return sum(x*y for x, y in v)
def dilate(v: Iterable[tuple[T, T]], scale: T) -> Iterable[tuple[T, T]]:
return ((x * scale, y * scale) for x, y in v)
vec: Iterable[tuple[float, float]] = []

``TypeAlias``
-------------

The explicit alias declaration syntax with ``TypeAlias`` clearly differentiates between the three
possible kinds of assignments: typed global expressions, untyped global
expressions, and type aliases. This avoids the existence of assignments that
break type checking when an annotation is added, and avoids classifying the
nature of the assignment based on the type of the value.

Implicit syntax (pre-existing):

::

x = 1 # untyped global expression
x: int = 1 # typed global expression

x = int # type alias
x: type[int] = int # typed global expression


Explicit syntax:

::

x = 1 # untyped global expression
x: int = 1 # typed global expression

x = int # untyped global expression (see note below)
x: type[int] = int # typed global expression

x: TypeAlias = int # type alias
x: TypeAlias = "MyClass" # type alias


Note: The examples above illustrate implicit and explicit alias declarations in
isolation. For the sake of backwards compatibility, type checkers should support
both simultaneously, meaning an untyped global expression ``x = int`` will
still be considered a valid type alias.

``type`` statement
------------------

Type aliases may also be defined using the ``type`` statement (Python 3.12 and
higher).

The ``type`` statement allows the creation of explicitly generic
type aliases::

type ListOrSet[T] = list[T] | set[T]

Type parameters declared as part of a generic type alias are valid only
when evaluating the right-hand side of the type alias.

As with ``typing.TypeAlias``, type checkers should restrict the right-hand
expression to expression forms that are allowed within type annotations.
The use of more complex expression forms (call expressions, ternary operators,
arithmetic operators, comparison operators, etc.) should be flagged as an
error.

Type alias expressions are not allowed to use traditional type variables (i.e.
those allocated with an explicit ``TypeVar`` constructor call). Type checkers
should generate an error in this case.

::

T = TypeVar("T")
type MyList = list[T] # Type checker error: traditional type variable usage

``NewType``
-----------

There are also situations where a programmer might want to avoid logical
errors by creating simple classes. For example::

class UserId(int):
pass

def get_by_user_id(user_id: UserId):
...

However, this approach introduces a runtime overhead. To avoid this,
``typing.py`` provides a helper function ``NewType`` that creates
simple unique types with almost zero runtime overhead. For a static type
checker ``Derived = NewType('Derived', Base)`` is roughly equivalent
to a definition::

class Derived(Base):
def __init__(self, _x: Base) -> None:
...

While at runtime, ``NewType('Derived', Base)`` returns a dummy function
that simply returns its argument. Type checkers require explicit casts
from ``int`` where ``UserId`` is expected, while implicitly casting
from ``UserId`` where ``int`` is expected. Examples::

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
...

UserId('user') # Fails type check

name_by_id(42) # Fails type check
name_by_id(UserId(42)) # OK

num = UserId(5) + 1 # type: int

``NewType`` accepts exactly two arguments: a name for the new unique type,
and a base class. The latter should be a proper class (i.e.,
not a type construct like ``Union``, etc.), or another unique type created
by calling ``NewType``. The function returned by ``NewType``
accepts only one argument; this is equivalent to supporting only one
constructor accepting an instance of the base class (see above). Example::

class PacketId:
def __init__(self, major: int, minor: int) -> None:
self._major = major
self._minor = minor

TcpPacketId = NewType('TcpPacketId', PacketId)

packet = PacketId(100, 100)
tcp_packet = TcpPacketId(packet) # OK

tcp_packet = TcpPacketId(127, 0) # Fails in type checker and at runtime

Both ``isinstance`` and ``issubclass``, as well as subclassing will fail
for ``NewType('Derived', Base)`` since function objects don't support
these operations.
Loading

0 comments on commit 7117775

Please sign in to comment.