diff options
Diffstat (limited to 'docs/source/libraries.rst')
-rw-r--r-- | docs/source/libraries.rst | 602 |
1 files changed, 602 insertions, 0 deletions
diff --git a/docs/source/libraries.rst b/docs/source/libraries.rst new file mode 100644 index 0000000..96e5370 --- /dev/null +++ b/docs/source/libraries.rst @@ -0,0 +1,602 @@ +.. _libraries: + +*********************** +Typing Python Libraries +*********************** + +Much of Python’s popularity can be attributed to the rich collection of +Python libraries available to developers. Authors of these libraries +play an important role in improving the experience for Python +developers. This document provides some recommendations and guidance for +Python library authors. + +These recommendations are intended to provide the following benefits: + +1. Consumers of libraries should have a great coding experience with + fast and accurate completion suggestions, class and function + documentation, signature help (including parameter default values), + hover text, and auto-imports. This should happen by default without + needing to download extra packages and without any special + configuration. These features should be consistent across the Python + ecosystem regardless of a developer’s choice of editor, IDE, notebook + environment, etc. +2. Consumers of libraries should be able to rely on complete and + accurate type information so static type checkers can detect and + report type inconsistencies and other violations of the interface + contract. +3. Library authors should be able to specify a well-defined interface + contract that is enforced by tools. This allows a library + implementation to evolve and improve without breaking consumers of + the library. +4. Library authors should have the benefits of static type checking to + produce high-quality, bug-free implementations. + +Inlined Type Annotations and Type Stubs +======================================= + +`PEP 561 <https://www.python.org/dev/peps/pep-0561/>`__ documents +several ways type information can be delivered for a library: inlined +type annotations, type stub files included in the package, a separate +companion type stub package, and type stubs in the typeshed repository. +Some of these options fall short on delivering the benefits above. We +therefore provide the following more specific guidance to library +authors. + +.. note:: + All libraries should include inlined type annotations for the + functions, classes, methods, and constants that comprise the public + interface for the library. + +Inlined type annotations should be included directly within the source +code that ships with the package. Of the options listed in PEP 561, +inlined type annotations offer the most benefits. They typically require +the least effort to add and maintain, they are always consistent with +the implementation, and docstrings and default parameter values are +readily available, allowing language servers to enhance the development +experience. + +There are cases where inlined type annotations are not possible — most +notably when a library’s exposed functionality is implemented in a +language other than Python. + +.. note:: + Libraries that expose symbols implemented in languages other than + Python should include stub (``.pyi``) files that describe the types for + those symbols. These stubs should also contain docstrings and default + parameter values. + +In many existing type stubs (such as those found in typeshed), default +parameter values are replaced with with ``...`` and all docstrings are +removed. We recommend that default values and docstrings remain within +the type stub file so language servers can display this information to +developers. + +Library Interface +================= + +`PEP 561 <https://www.python.org/dev/peps/pep-0561/>`__ indicates that a +``py.typed`` marker file must be included in the package if the author +wishes to support type checking of their code. + +If a ``py.typed`` module is present, a type checker will treat all modules +within that package (i.e. all files that end in ``.py`` or ``.pyi``) as +importable unless the file name begins with an underscore. These modules +comprise the supported interface for the library. + +Each module exposes a set of symbols. Some of these symbols are +considered "private” — implementation details that are not part of the +library’s interface. Type checkers can use the following rules +to determine which symbols are visible outside of the package. + +- Symbols whose names begin with an underscore (but are not dunder + names) are considered private. +- Imported symbols are considered private by default. If they use the + ``import A as A`` (a redundant module alias), ``from X import A as A`` (a + redundant symbol alias), or ``from . import A`` forms, symbol ``A`` is + not private unless the name begins with an underscore. If a file + ``__init__.py`` uses form ``from .A import X``, symbol ``A`` is treated + likewise. If a wildcard import (of the form ``from X import *``) is + used, all symbols referenced by the wildcard are not private. +- A module can expose an ``__all__`` symbol at the module level that + provides a list of names that are considered part of the interface. + This overrides all other rules above, allowing imported symbols or + symbols whose names begin with an underscore to be included in the + interface. +- Local variables within a function (including nested functions) are + always considered private. + +The following idioms are supported for defining the values contained +within ``__all__``. These restrictions allow type checkers to statically +determine the value of ``__all__``. + +- ``__all__ = ('a', b')`` +- ``__all__ = ['a', b']`` +- ``__all__ += ['a', b']`` +- ``__all__ += submodule.__all__`` +- ``__all__.extend(['a', b'])`` +- ``__all__.extend(submodule.__all__)`` +- ``__all__.append('a')`` +- ``__all__.remove('a')`` + +Type Completeness +================= + +A “py.typed” library should aim to be type complete so that type +checking and inspection can work to their full extent. Here we say that a +library is “type complete” if all of the symbols +that comprise its interface have type annotations that refer to types +that are fully known. Private symbols are exempt. + +The following are best practice recommendations for how to define “type complete”: + +Classes: + +- All class variables, instance variables, and methods that are + “visible” (not overridden) are annotated and refer to known types +- If a class is a subclass of a generic class, type arguments are + provided for each generic type parameter, and these type arguments + are known types + +Functions and Methods: + +- All input parameters have type annotations that refer to known types +- The return parameter is annotated and refers to a known type +- The result of applying one or more decorators results in a known type + +Type Aliases: + +- All of the types referenced by the type alias are known + +Variables: + +- All variables have type annotations that refer to known types + +Type annotations can be omitted in a few specific cases where the type +is obvious from the context: + +- Constants that are assigned simple literal values + (e.g. ``RED = '#F00'`` or ``MAX_TIMEOUT = 50`` or + ``room_temperature: Final = 20``). A constant is a symbol that is + assigned only once and is either annotated with ``Final`` or is named + in all-caps. A constant that is not assigned a simple literal value + requires explicit annotations, preferably with a ``Final`` annotation + (e.g. ``WOODWINDS: Final[List[str]] = ['Oboe', 'Bassoon']``). +- Enum values within an Enum class do not require annotations because + they take on the type of the Enum class. +- Type aliases do not require annotations. A type alias is a symbol + that is defined at a module level with a single assignment where the + assigned value is an instantiable type, as opposed to a class + instance + (e.g. ``Foo = Callable[[Literal["a", "b"]], Union[int, str]]`` or + ``Bar = Optional[MyGenericClass[int]]``). +- The “self” parameter in an instance method and the “cls” parameter in + a class method do not require an explicit annotation. +- The return type for an ``__init__`` method does not need to be + specified, since it is always ``None``. +- The following module-level symbols do not require type annotations: + ``__all__``,\ ``__author__``, ``__copyright__``, ``__email__``, + ``__license__``, ``__title__``, ``__uri__``, ``__version__``. +- The following class-level symbols do not require type annotations: + ``__class__``, ``__dict__``, ``__doc__``, ``__module__``, + ``__slots__``. + +Examples of known and unknown types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + + # Variable with unknown type + a = [3, 4, 5] + + # Variable with known type + a: List[int] = [3, 4, 5] + + # Type alias with partially unknown type (because type + # arguments are missing for list and dict) + DictOrList = Union[list, dict] + + # Type alias with known type + DictOrList = Union[List[Any], Dict[str, Any]] + + # Generic type alias with known type + _T = TypeVar("_T") + DictOrList = Union[List[_T], Dict[str, _T]] + + # Function with known type + def func(a: Optional[int], b: Dict[str, float] = {}) -> None: + pass + + # Function with partially unknown type (because type annotations + # are missing for input parameters and return type) + def func(a, b): + pass + + # Function with partially unknown type (because of missing + # type args on Dict) + def func(a: int, b: Dict) -> None: + pass + + # Function with partially unknown type (because return type + # annotation is missing) + def func(a: int, b: Dict[str, float]): + pass + + # Decorator with partially unknown type (because type annotations + # are missing for input parameters and return type) + def my_decorator(func): + return func + + # Function with partially unknown type (because type is obscured + # by untyped decorator) + @my_decorator + def func(a: int) -> str: + pass + + + # Class with known type + class MyClass: + height: float = 2.0 + + def __init__(self, name: str, age: int): + self.age: int = age + + @property + def name(self) -> str: + ... + + # Class with partially unknown type + class MyClass: + # Missing type annotation for class variable + height = 2.0 + + # Missing input parameter annotations + def __init__(self, name, age): + # Missing type annotation for instance variable + self.age = age + + # Missing return type annotation + @property + def name(self): + ... + + # Class with partially unknown type + class BaseClass: + # Missing type annotation + height = 2.0 + + # Missing type annotation + def get_stuff(self): + ... + + # Class with known type (because it overrides all symbols + # exposed by BaseClass that have incomplete types) + class DerivedClass(BaseClass): + height: float + + def get_stuff(self) -> str: + ... + + # Class with partially unknown type because base class + # (dict) is generic, and type arguments are not specified. + class DictSubclass(dict): + pass + +Best Practices for Inlined Types +================================ + +Wide vs. Narrow Types +~~~~~~~~~~~~~~~~~~~~~ + +In type theory, when comparing two types that are related to each other, +the “wider” type is the one that is more general, and the “narrower” +type is more specific. For example, ``Sequence[str]`` is a wider type +than ``List[str]`` because all ``List`` objects are also ``Sequence`` +objects, but the converse is not true. A subclass is narrower than a +class it derives from. A union of types is wider than the individual +types that comprise the union. + +In general, a function input parameter should be annotated with the +widest possible type supported by the implementation. For example, if +the implementation requires the caller to provide an iterable collection +of strings, the parameter should be annotated as ``Iterable[str]``, not +as ``List[str]``. The latter type is narrower than necessary, so if a +user attempts to pass a tuple of strings (which is supported by the +implementation), a type checker will complain about a type +incompatibility. + +As a specific application of the “use the widest type possible” rule, +libraries should generally use immutable forms of container types +instead of mutable forms (unless the function needs to modify the +container). Use ``Sequence`` rather than ``List``, ``Mapping`` rather +than ``Dict``, etc. Immutable containers allow for more flexibility +because their type parameters are covariant rather than invariant. A +parameter that is typed as ``Sequence[Union[str, int]]`` can accept a +``List[int]``, ``Sequence[str]``, and a ``Sequence[int]``. But a +parameter typed as ``List[Union[str, int]]`` is much more restrictive +and accepts only a ``List[Union[str, int]]``. + +Overloads +~~~~~~~~~ + +If a function or method can return multiple different types and those +types can be determined based on the presence or types of certain +parameters, use the ``@overload`` mechanism defined in `PEP +484 <https://www.python.org/dev/peps/pep-0484/#id45>`__. When overloads +are used within a “.py” file, they must appear prior to the function +implementation, which should not have an ``@overload`` decorator. + +Keyword-only Parameters +~~~~~~~~~~~~~~~~~~~~~~~ + +If a function or method is intended to take parameters that are +specified only by name, use the keyword-only separator (``*``). + +.. code:: python + + def create_user(age: int, *, dob: Optional[date] = None): + ... + +Annotating Decorators +~~~~~~~~~~~~~~~~~~~~~ + +Decorators modify the behavior of a class or a function. Providing +annotations for decorators is straightforward if the decorator retains +the original signature of the decorated function. + +.. code:: python + + _F = TypeVar("_F", bound=Callable[..., Any]) + + def simple_decorator(_func: _F) -> _F: + """ + Simple decorators are invoked without parentheses like this: + @simple_decorator + def my_function(): ... + """ + ... + + def complex_decorator(*, mode: str) -> Callable[[_F], _F]: + """ + Complex decorators are invoked with arguments like this: + @complex_decorator(mode="easy") + def my_function(): ... + """ + ... + +Decorators that mutate the signature of the decorated function present +challenges for type annotations. The ``ParamSpec`` and ``Concatenate`` +mechanisms described in `PEP +612 <https://www.python.org/dev/peps/pep-0612/>`__ provide some help +here, but these are available only in Python 3.10 and newer. More +complex signature mutations may require type annotations that erase the +original signature, thus blinding type checkers and other tools that +provide signature assistance. As such, library authors are discouraged +from creating decorators that mutate function signatures in this manner. + +Generic Classes and Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Classes and functions that can operate in a generic manner on various +types should declare themselves as generic using the mechanisms +described in `PEP 484 <https://www.python.org/dev/peps/pep-0484/>`__. +This includes the use of ``TypeVar`` symbols. Typically, a ``TypeVar`` +should be private to the file that declares it, and should therefore +begin with an underscore. + +Type Aliases +~~~~~~~~~~~~ + +Type aliases are symbols that refer to other types. Generic type aliases +(those that refer to unspecialized generic classes) are supported by +most type checkers. + +`PEP 613 <https://www.python.org/dev/peps/pep-0613/>`__ provides a way +to explicitly designate a symbol as a type alias using the new TypeAlias +annotation. + +.. code:: python + + # Simple type alias + FamilyPet = Union[Cat, Dog, GoldFish] + + # Generic type alias + ListOrTuple = Union[List[_T], Tuple[_T, ...]] + + # Recursive type alias + TreeNode = Union[LeafNode, List["TreeNode"]] + + # Explicit type alias using PEP 613 syntax + StrOrInt: TypeAlias = Union[str, int] + +Abstract Classes and Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Classes that must be subclassed should derive from ``ABC``, and methods +or properties that must be overridden should be decorated with the +``@abstractmethod`` decorator. This allows type checkers to validate +that the required methods have been overridden and provide developers +with useful error messages when they are not. It is customary to +implement an abstract method by raising a ``NotImplementedError`` +exception. + +.. code:: python + + from abc import ABC, abstractmethod + + class Hashable(ABC): + @property + @abstractmethod + def hash_value(self) -> int: + """Subclasses must override""" + raise NotImplementedError() + + @abstractmethod + def print(self) -> str: + """Subclasses must override""" + raise NotImplementedError() + +Final Classes and Methods +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Classes that are not intended to be subclassed should be decorated as +``@final`` as described in `PEP +591 <https://www.python.org/dev/peps/pep-0591/>`__. The same decorator +can also be used to specify methods that cannot be overridden by +subclasses. + +Literals +~~~~~~~~ + +Type annotations should make use of the Literal type where appropriate, +as described in `PEP 586 <https://www.python.org/dev/peps/pep-0586/>`__. +Literals allow for more type specificity than their non-literal +counterparts. + +Constants +~~~~~~~~~ + +Constant values (those that are read-only) can be specified using the +Final annotation as described in `PEP +591 <https://www.python.org/dev/peps/pep-0591/>`__. + +Type checkers will also typically treat variables that are named using +all upper-case characters as constants. + +In both cases, it is OK to omit the declared type of a constant if it is +assigned a literal str, int, float, bool or None value. In such cases, +the type inference rules are clear and unambiguous, and adding a literal +type annotation would be redundant. + +.. code:: python + + # All-caps constant with inferred type + COLOR_FORMAT_RGB = "rgb" + + # All-caps constant with explicit type + COLOR_FORMAT_RGB: Literal["rgb"] = "rgb" + LATEST_VERSION: Tuple[int, int] = (4, 5) + + # Final variable with inferred type + ColorFormatRgb: Final = "rgb" + + # Final variable with explicit type + ColorFormatRgb: Final[Literal["rgb"]] = "rgb" + LATEST_VERSION: Final[Tuple[int, int]] = (4, 5) + +Typed Dictionaries, Data Classes, and Named Tuples +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your library runs only on newer versions of Python, you are +encouraged to use some of the new type-friendly classes. + +NamedTuple (described in `PEP +484 <https://www.python.org/dev/peps/pep-0484/>`__) is preferred over +namedtuple. + +Data classes (described in `PEP +557 <https://www.python.org/dev/peps/pep-0557/>`__) is preferred over +untyped dictionaries. + +TypedDict (described in `PEP +589 <https://www.python.org/dev/peps/pep-0589/>`__) is preferred over +untyped dictionaries. + +Compatibility with Older Python Versions +======================================== + +Each new version of Python from 3.5 onward has introduced new typing +constructs. This presents a challenge for library authors who want to +maintain runtime compatibility with older versions of Python. This +section documents several techniques that can be used to add types while +maintaining backward compatibility. + +Quoted Annotations +~~~~~~~~~~~~~~~~~~ + +Type annotations for variables, parameters, and return types can be +placed in quotes. The Python interpreter will then ignore them, whereas +a type checker will interpret them as type annotations. + +.. code:: python + + # Older versions of Python do not support subscripting + # for the OrderedDict type, so the annotation must be + # enclosed in quotes. + def get_config(self) -> "OrderedDict[str, str]": + return self._config + +Type Comment Annotations +~~~~~~~~~~~~~~~~~~~~~~~~ + +Python 3.0 introduced syntax for parameter and return type annotations, +as specified in `PEP 484 <https://www.python.org/dev/peps/pep-0484/>`__. +Python 3.6 introduced support for variable type annotations, as +specified in `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`__. + +If you need to support older versions of Python, type annotations can +still be provided as “type comments”. These comments take the form # +type: . + +.. code:: python + + class Foo: + # Variable type comments go at the end of the line + # where the variable is assigned. + timeout = None # type: Optional[int] + + # Function type comments can be specified on the + # line after the function signature. + def send_message(self, name, length): + # type: (str, int) -> None + ... + + # Function type comments can also specify the type + # of each parameter on its own line. + def receive_message( + self, + name, # type: str + length # type: int + ): + # type: () -> Message + ... + +typing_extensions +~~~~~~~~~~~~~~~~~ + +New type features that require runtime support are typically included in +the stdlib ``typing`` module. Where possible, these new features are +back-ported to a runtime library called ``typing_extensions`` that works +with older Python runtimes. + +TYPE_CHECKING +~~~~~~~~~~~~~ + +The ``typing`` module exposes a variable called ``TYPE_CHECKING`` which +has a value of False within the Python runtime but a value of True when +the type checker is performing its analysis. This allows type checking +statements to be conditionalized. + +Care should be taken when using ``TYPE_CHECKING`` because behavioral +changes between type checking and runtime could mask problems that the +type checker would otherwise catch. + +Non-Standard Type Behaviors +=========================== + +Type annotations provide a way to annotate typical type behaviors, but +some classes implement specialized, non-standard behaviors that cannot +be described using standard type annotations. For now, such types need +to be annotated as Any, which is unfortunate because the benefits of +static typing are lost. + +Docstrings +========== + +Docstrings should be provided for all classes, functions, and methods in +the interface. They should be formatted according to `PEP +257 <https://www.python.org/dev/peps/pep-0257/>`__. + +There is currently no single agreed-upon standard for function and +method docstrings, but several common variants have emerged. We +recommend using one of these variants. |