Skip to content

Commit

Permalink
Add "Modernizing Obsolete Typing Features" guide
Browse files Browse the repository at this point in the history
  • Loading branch information
srittau committed Dec 12, 2023
1 parent 007e5e6 commit 760b2e2
Show file tree
Hide file tree
Showing 2 changed files with 320 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/source/guides.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Type System Guides

libraries
writing_stubs
modernizing
unreachable
typing_anti_pitch
319 changes: 319 additions & 0 deletions docs/source/modernizing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
.. role:: python(code)
:language: python

.. role:: t-ext(class)

.. _modernizing:

************************************
Modernizing Obsolete Typing Features
************************************

Introduction
============

This guide helps to modernize your code by replacing older typing features
with their modern equivalents. Not all features described here are obsolete,
but they superseded by more modern alternatives, that are recommended to use.

These newer features are not available in all Python versions, although
some features are available as backports from the
`typing-extensions <https://pypi.org/project/typing-extensions/>`_
package, or require quoting or using :python:`from __future__ import annotations`.
Each section states the minimum Python version required to use the
feature, whether it is available in typing-extensions, and whether it is
available using quoting.

.. note::

The latest version of typing-extensions is available for all Python
versions that have not reached their end of life, but not necessarily for
older versions.

.. note::

:python:`from __future__ import annotations` is available since Python 3.7.
This will only work inside type annotations, while quoting is still
required outside. For example, this example runs on Python 3.7 and up,
although the pipe operator was only introduced in Python 3.10::

from __future__ import annotations
from typing_extensions import TypeAlias

def f(x: int | None) -> int | str: ... # the future import is sufficient
Alias: TypeAlias = "int | str" # this requires quoting

.. _modernizing-type-comments:

Type Comments
=============

*Alternative available since:* Python 3.0, 3.6

Type comments were originally introduced to support type annotations in
Python 2 and variable annotations before Python 3.6. While most type checkers
still support them, they are considered obsolete, and type checkers are
not required to support them.

For example, replace::

x = 3 # type: int
def f(x, y): # type: (int, int) -> int
return x + y

with::

x: int = 3
def f(x: int, y: int) -> int:
return x + y

When using forward references or types only available during type checking,
it's necessary to either use :python:`from __future__ import annotations`
(available since Python 3.7) or to quote the type::

def f(x: "Parrot") -> int: ...

class Parrot: ...

.. _modernizing-typing-text:

``typing.Text``
===============

*Alternative available since:* Python 3.0

:class:`typing.Text` was a type alias intended for Python 2 compatibility.
It is equivalent to :class:`str` and should be replaced with it.
For example, replace::

from typing import Text

def f(x: Text) -> Text: ...

with::

def f(x: str) -> str: ...

.. _modernizing-typed-dict:

``typing.TypedDict`` Legacy Forms
=================================

*Alternative available since:* Python 3.6

:class:`TypedDict <typing.TypedDict>` supports two legacy forms for
supporting Python versions that don't support variable annotations.
Replace these two variants::

from typing import TypedDict

FlyingSaucer = TypedDict("FlyingSaucer", {"x": int, "y": str})
FlyingSaucer = TypedDict("FlyingSaucer", x=int, y=str)

with::

class FlyingSaucer(TypedDict):
x: int
y: str

.. _modernizing-generics:

Generics in the ``typing`` Module
=================================

*Alternative available since:* Python 3.0 (quoted), Python 3.9, 3.12 (unquoted)

Originally, the :mod:`typing` module provided aliases for built-in types that
accepted type parameters. Since Python 3.9, these aliases are no longer
necessary, and can be replaced with the built-in types. For example,
replace::

from typing import Dict, List

def f(x: List[int]) -> Dict[str, int]: ...

with::

def f(x: list[int]) -> dict[str, int]: ...

This affects the following types:

* :class:`typing.Dict` (→ :class:`dict`)
* :class:`typing.FrozenSet` (→ :class:`frozenset`)
* :class:`typing.List` (→ :class:`list`)
* :class:`typing.Set` (→ :class:`set`)
* :data:`typing.Tuple` (→ :class:`tuple`)

The :mod:`typing` module also provided aliases for certain standard library
types that accepted type parameters. Since Python 3.9, these aliases are no
longer necessary, and can be replaced with the proper types. For example,
replace::

from typing import DefaultDict, Pattern

def f(x: Pattern[str]) -> DefaultDict[str, int]: ...

with::

from collections import defaultdict
from re import Pattern

def f(x: Pattern[str]) -> defaultdict[str, int]: ...

This affects the following types:

* :class:`typing.Deque` (→ :class:`collections.deque`)
* :class:`typing.DefaultDict` (→ :class:`collections.defaultdict`)
* :class:`typing.OrderedDict` (→ :class:`collections.OrderedDict`)
* :class:`typing.Counter` (→ :class:`collections.Counter`)
* :class:`typing.ChainMap` (→ :class:`collections.ChainMap`)
* :class:`typing.Awaitable` (→ :class:`collections.abc.Awaitable`)
* :class:`typing.Coroutine` (→ :class:`collections.abc.Coroutine`)
* :class:`typing.AsyncIterable` (→ :class:`collections.abc.AsyncIterable`)
* :class:`typing.AsyncIterator` (→ :class:`collections.abc.AsyncIterator`)
* :class:`typing.AsyncGenerator` (→ :class:`collections.abc.AsyncGenerator`)
* :class:`typing.Iterable` (→ :class:`collections.abc.Iterable`)
* :class:`typing.Iterator` (→ :class:`collections.abc.Iterator`)
* :class:`typing.Generator` (→ :class:`collections.abc.Generator`)
* :class:`typing.Reversible` (→ :class:`collections.abc.Reversible`)
* :class:`typing.Container` (→ :class:`collections.abc.Container`)
* :class:`typing.Collection` (→ :class:`collections.abc.Collection`)
* :data:`typing.Callable` (→ :class:`collections.abc.Callable`)
* :class:`typing.AbstractSet` (→ :class:`collections.abc.Set`)
* :class:`typing.MutableSet` (→ :class:`collections.abc.MutableSet`)
* :class:`typing.Mapping` (→ :class:`collections.abc.Mapping`)
* :class:`typing.MutableMapping` (→ :class:`collections.abc.MutableMapping`)
* :class:`typing.Sequence` (→ :class:`collections.abc.Sequence`)
* :class:`typing.MutableSequence` (→ :class:`collections.abc.MutableSequence`)
* :class:`typing.ByteString` (→ :class:`collections.abc.ByteString`), but see :ref:`modernizing-byte-string`
* :class:`typing.MappingView` (→ :class:`collections.abc.MappingView`)
* :class:`typing.KeysView` (→ :class:`collections.abc.KeysView`)
* :class:`typing.ItemsView` (→ :class:`collections.abc.ItemsView`)
* :class:`typing.ValuesView` (→ :class:`collections.abc.ValuesView`)
* :class:`typing.ContextManager` (→ :class:`contextlib.AbstractContextManager`)
* :class:`typing.AsyncContextManager` (→ :class:`contextlib.AbstractAsyncContextManager`)
* :class:`typing.Pattern` (→ :class:`re.Pattern`)
* :class:`typing.Match` (→ :class:`re.Match`)

The following types were made generic in Python 3.12:

* :class:`typing.Hashable` (→ :class:`collections.abc.Hashable`)
* :class:`typing.Sized` (→ :class:`collections.abc.Sized`)

.. _modernizing-union:

``typing.Union`` and ``typing.Optional``
========================================

*Alternative available since:* Python 3.0 (quoted), Python 3.10 (unquoted)

While :data:`Union <typing.Union>` and :data:`Optional <typing.Optional>` are
not considered obsolete, using the ``|`` (pipe) operator is often more
readable. :python:`Union[X, Y]` is equivalent to :python:`X | Y`, while
:python:`Optional[X]` is equivalent to :python:`X | None`.

For example, replace::

from typing import Optional, Union

def f(x: Optional[int]) -> Union[int, str]: ...

with::

def f(x: int | None) -> int | str: ...

.. _modernizing-no-return:

``typing.NoReturn``
===================

*Alternative available since:* Python 3.11, typing-extensions

Python 3.11 introduced :data:`typing.Never` as an alias to
:data:`typing.NoReturn` for use in annotations that are not
return types. For example, replace::

from typing import NoReturn

def f(x: int, y: NoReturn) -> None: ...

with::

from typing import Never # or typing_extensions.Never

def f(x: int, y: Never) -> None: ...

But keep ``NoReturn`` for return types::

from typing import NoReturn

def f(x: int) -> NoReturn: ...

.. _modernizing-type-aliases:

Type Aliases
============

*Alternative available since:* Python 3.12 (keyword); Python 3.10, typing-extensions

Originally, type aliases were defined using a simple assignment::

IntList = list[int]

Python 3.12 introduced the :keyword:`type` keyword to define type aliases::

type IntList = list[int]

Code supporting older Python versions should use
:data:`TypeAlias <typing.TypeAlias>`, introduced in Python 3.10, but also
available in typing-extensions, instead::

from typing import TypeAlias # or typing_extensions.TypeAlias

IntList: TypeAlias = list[int]

.. _modernizing-user-generics:

User Defined Generics
=====================

*Alternative available since:* Python 3.12

Python 3.12 introduced new syntax for defining generic classes. Previously,
generic classes had to derive from :class:`typing.Generic` (or another
generic class) and defined the type variable using :class:`typing.TypeVar`.
For example::

from typing import Generic, TypeVar

T = TypeVar("T")

class Brian(Generic[T]): ...
class Reg(int, Generic[T]): ...

Starting with Python 3.12, the type variable doesn't need to be declared
using ``TypeVar``, and instead of deriving the class from ``Generic``, the
following syntax can be used::

class Brian[T]: ...
class Reg[T](int): ...

.. _modernizing-byte-string:

``typing.ByteString``
=====================

*Alternative available since:* Python 3.0; Python 3.12, typing-extensions

:class:`ByteString <typing.ByteString>` was originally intended to be a type
alias for "byte-like" types, i.e. :class:`bytes`, :class:`bytearray`, and
:class:`memoryview`. In practice, this
is seldom exactly what is needed. Use one of these alternatives instead:

* Just :class:`bytes` is often sufficient, especially when not declaring
a public API.
* For items that accept any type that supports the
:ref:`buffer protocol <bufferobjects>`, use :class:`collections.abc.Buffer`
(available since Python 3.12) or :t-ext:`typing_extensions.Buffer`.
* Otherwise, use a union of :class:`bytes`, :class:`bytearray`,
:class:`memoryview`, and/or any other types that are accepted.

0 comments on commit 760b2e2

Please sign in to comment.