diff --git a/docs/guides/writing_stubs.rst b/docs/guides/writing_stubs.rst index bde28632..47f48b40 100644 --- a/docs/guides/writing_stubs.rst +++ b/docs/guides/writing_stubs.rst @@ -93,6 +93,9 @@ Liskov substitutability or detecting problematic overloads. It may be instructive to examine `typeshed `__'s `setup for testing stubs `__. +To suppress type errors in stubs, use ``# type: ignore`` comments. Refer to the :ref:`type-checker-error-suppression` section of the style guide for +error suppression formats specific to individual typecheckers. + .. TODO: consider adding examples and configurations for specific type checkers @@ -113,18 +116,6 @@ Stub Content This section documents best practices on what elements to include or leave out of stub files. -Modules excluded from stubs ---------------------------- - -Not all modules should be included in stubs. - -It is recommended to exclude: - -1. Implementation details, with `multiprocessing/popen_spawn_win32.py `_ as a notable example -2. Modules that are not supposed to be imported, such as ``__main__.py`` -3. Protected modules that start with a single ``_`` char. However, when needed protected modules can still be added (see :ref:`undocumented-objects` section below) -4. Tests - Public Interface ---------------- @@ -138,7 +129,17 @@ The following should always be included: * All objects included in ``__all__`` (if present). Other objects may be included if they are not prefixed with an underscore -or if they are being used in practice. (See the next section.) +or if they are being used in practice. + +Modules excluded from stubs +--------------------------- + +The following should not be included in stubs: + +1. Implementation details, with `multiprocessing/popen_spawn_win32.py `_ as a notable example +2. Modules that are not supposed to be imported, such as ``__main__.py`` +3. Protected modules that start with a single ``_`` char. However, when needed protected modules can still be added (see :ref:`undocumented-objects` section below) +4. Tests .. _undocumented-objects: @@ -212,6 +213,20 @@ to use them freely to describe simple structural types. Incomplete Stubs ---------------- +When writing new stubs, it is not necessary to fully annotate all arguments, +return types, and fields. Some items may be left unannotated or +annotated with ``_typeshed.Incomplete`` (`documentation `_):: + + from _typeshed import Incomplete + + field: Incomplete # unannotated + + def foo(x): ... # unannotated argument and return type + +``_typeshed.Incomplete`` can also be used for partially known types:: + + def foo(x: Incomplete | None = None) -> list[Incomplete]: ... + Partial stubs can be useful, especially for larger packages, but they should follow the following guidelines: @@ -219,16 +234,7 @@ follow the following guidelines: can be left unannotated. * Do not use ``Any`` to mark unannotated or partially annotated values. Leave function parameters and return values unannotated. In all other cases, use - ``_typeshed.Incomplete`` - (`documentation `_):: - - from _typeshed import Incomplete - - field1: Incomplete - field2: dict[str, Incomplete] - - def foo(x): ... - + ``_typeshed.Incomplete``. * Partial classes should include a ``__getattr__()`` method marked with ``_typeshed.Incomplete`` (see example below). * Partial modules (i.e. modules that are missing some or all classes, @@ -253,6 +259,17 @@ annotated function ``bar()``:: def bar(x: str, y, *, z=...): ... +``Any`` vs. ``Incomplete`` +-------------------------- + +While ``Incomplete`` is a type alias of ``Any``, they serve different purposes: +``Incomplete`` is a placeholder where a proper type might be substituted. +It's a "to do" item and should be replaced if possible. + +``Any`` is used when it's not possible to accurately type an item using the current +type system. It should be used sparingly, as described in the :ref:`using-any` +section of the style guide. + Attribute Access ---------------- @@ -475,6 +492,28 @@ and the :ref:`best-practices`. There are a few exceptions, outlined below, that different structure of stub files into account and aim to create more concise files. +Syntax Example +-------------- + +The below is an excerpt from the types for the ``datetime`` module. + + MAXYEAR: int + MINYEAR: int + + class date: + def __new__(cls, year: SupportsIndex, month: SupportsIndex, day: SupportsIndex) -> Self: ... + @classmethod + def fromtimestamp(cls, timestamp: float, /) -> Self: ... + @classmethod + def today(cls) -> Self: ... + @classmethod + def fromordinal(cls, n: int, /) -> Self: ... + @property + def year(self) -> int: ... + def replace(self, year: SupportsIndex = ..., month: SupportsIndex = ..., day: SupportsIndex = ...) -> Self: ... + def ctime(self) -> str: ... + def weekday(self) -> int: ... + Maximum Line Length ------------------- @@ -506,7 +545,7 @@ No:: def time_func() -> None: ... - def date_func() -> None: ... # do no leave unnecessary empty lines + def date_func() -> None: ... # do not leave unnecessary empty lines def ip_func() -> None: ... @@ -563,7 +602,7 @@ Yes:: class Color(Enum): # An assignment with no type annotation is a convention used to indicate - # an enum member. + # an enum member. RED = 1 No:: @@ -633,6 +672,13 @@ No:: ... def to_int3(x: str) -> int: pass +Avoid invariant collection types (``list``, ``dict``) for function parameters, +in favor of covariant types like ``Mapping`` or ``Sequence``. + +Avoid union return types. See https://github.com/python/mypy/issues/1693 + +Use ``float`` instead of ``int | float`` for parameter annotations. See :pep:`484` for more details. + Language Features ----------------- @@ -662,6 +708,14 @@ No:: class OtherClass: ... +Use variable annotations instead of type comments, even for stubs that target +older versions of Python. + +Platform-dependent APIs +----------------------- + +Use :ref:`platform checks` like ``if sys.platform == 'win32'`` to denote platform-dependent APIs. + NamedTuple and TypedDict ------------------------ @@ -689,7 +743,7 @@ No:: Built-in Generics ----------------- -:pep:`585` built-in generics are supported and should be used instead +:pep:`585` built-in generics (such as ``list``, ``dict``, ``tuple``, ``set``) are supported and should be used instead of the corresponding types from ``typing``:: from collections import defaultdict @@ -707,8 +761,115 @@ generally possible and recommended:: Unions ------ -Declaring unions with the shorthand `|` syntax is recommended and supported by +Declaring unions with the shorthand ``|`` syntax is recommended and supported by all type checkers:: def foo(x: int | str) -> int | None: ... # recommended def foo(x: Union[int, str]) -> Optional[int]: ... # ok + +.. _using-any: + +Using ``Any`` and ``object`` +---------------------------- + +When adding type hints, avoid using the ``Any`` type when possible. Reserve +the use of ``Any`` for when: + +* the correct type cannot be expressed in the current type system; and +* to avoid union returns (see above). + +Note that ``Any`` is not the correct type to use if you want to indicate +that some function can accept literally anything: in those cases use +``object`` instead. + +When using ``Any``, document the reason for using it in a comment. Ideally, +document what types could be used. + +The ``Any`` Trick +----------------- + +In cases where a function or method can return ``None``, but where forcing the +user to explicitly check for ``None`` can be detrimental, use +``_typeshed.MaybeNone`` (an alias to ``Any``), instead of ``None``. + +Consider the following (simplified) signature of ``re.Match[str].group``:: + + class Match: + def group(self, group: str | int, /) -> str | MaybeNone: ... + +This avoid forcing the user to check for ``None``:: + + match = re.fullmatch(r"\d+_(.*)", some_string) + assert match is not None + name_group = match.group(1) # The user knows that this will never be None + return name_group.uper() # This typo will be flagged by the type checker + +In this case, the user of ``match.group()`` must be prepared to handle a ``str``, +but type checkers are happy with ``if name_group is None`` checks, because we're +saying it can also be something else than an ``str``. + +This is sometimes called "the Any trick". + +Context Managers +---------------- + +When adding type annotations for context manager classes, annotate +the return type of ``__exit__`` as bool only if the context manager +sometimes suppresses exceptions -- if it sometimes returns ``True`` +at runtime. If the context manager never suppresses exceptions, +have the return type be either ``None`` or ``bool | None``. If you +are not sure whether exceptions are suppressed or not or if the +context manager is meant to be subclassed, pick ``bool | None``. +See https://github.com/python/mypy/issues/7214 for more details. + +``__enter__`` methods and other methods that return ``self`` or ``cls(...)`` +should be annotated with ``typing.Self`` +(`example `_). + +Naming +------ + +Type variables and aliases you introduce purely for legibility reasons +should be prefixed with an underscore to make it obvious to the reader +they are not part of the stubbed API. + +A few guidelines for protocol names below. In cases that don't fall +into any of those categories, use your best judgement. + +* Use plain names for protocols that represent a clear concept + (e.g. ``Iterator``, ``Container``). +* Use ``SupportsX`` for protocols that provide callable methods (e.g. + ``SupportsInt``, ``SupportsRead``, ``SupportsReadSeek``). +* Use ``HasX`` for protocols that have readable and/or writable attributes + or getter/setter methods (e.g. ``HasItems``, ``HasFileno``). + +.. _type-checker-error-suppression: + +Type Checker Error Suppression Formats +-------------------------------------- + +* Use mypy error codes for mypy-specific ``# type: ignore`` annotations, e.g. ``# type: ignore[override]`` for Liskov Substitution Principle violations. +* Use pyright error codes for pyright-specific suppressions, e.g. ``# pyright: ignore[reportGeneralTypeIssues]``. +* If you need both on the same line, mypy's annotation needs to go first, e.g. ``# type: ignore[override] # pyright: ignore[reportGeneralTypeIssues]``. + + +``@deprecated`` +--------------- + +The ``@typing_extensions.deprecated`` decorator (``@warnings.deprecated`` +since Python 3.13) can be used to mark deprecated functionality; see +:pep:`702`. + +Keep the deprecation message concise, but try to mention the projected +version when the functionality is to be removed, and a suggested +replacement. + +Docstrings +---------- + +There are several tradeoffs around including docstrings in type stubs. Consider the intended purpose +of your stubs when deciding whether to include docstrings in your project's stubs. + +* They do not affect type checking results and will be ignored by type checkers. +* Docstrings can improve certain IDE functionality, such as hover info. +* Duplicating docstrings between source code and stubs requires extra work to keep them in sync.