Skip to content

Commit

Permalink
Split Best Practics document from Stubs document (#1193)
Browse files Browse the repository at this point in the history
Move all "style guide" items over that apply to both stubs as well as
implementation. The items are reordered and the "Types" section was
split, but the text itself is unchanged.

Part of #851
  • Loading branch information
srittau authored May 21, 2022
1 parent bc18c21 commit 831742f
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 94 deletions.
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Indices and tables
* :ref:`genindex`
* :ref:`search`

.. _contact:

Discussions and Support
=======================

Expand Down
130 changes: 130 additions & 0 deletions docs/source/best_practices.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
.. _best-practices:

*********************
Typing Best Practices
*********************

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

Over time, some best practices have proven themselves as useful when working
with type hints in Python. Not all practices are applicable in all situations
and some practices come down to personal style and preference, but they
are a good default set of recommendations to fall back to, unless there is
a specific reason to deviate.

These best practices are constantly evolving, especially as the typing
capabilities and ecosystem grow. So expect new best practices to be added
and existing best practices to be modified or even removed as better practices
evolve. That is why we would love to hear from your experiences with typing.
Please see :ref:`contact` on how to join the discussion.

Typing Features
===============

Type Aliases
------------

Use ``TypeAlias`` for type aliases (but not for regular aliases).

Yes::

_IntList: TypeAlias = list[int]
g = os.stat
Path = pathlib.Path
ERROR = errno.EEXIST

No::

_IntList = list[int]
g: TypeAlias = os.stat
Path: TypeAlias = pathlib.Path
ERROR: TypeAlias = errno.EEXIST

Ergonomic Practices
===================

Using `Any`
-----------

Generally, use ``Any`` when a type cannot be expressed appropriately
with the current type system or using the correct type is unergonomic.

Arguments and Return Types
--------------------------

For arguments, prefer protocols and abstract types (``Mapping``,
``Sequence``, ``Iterable``, etc.). If an argument accepts literally any value,
use ``object`` instead of ``Any``.

For return values, prefer concrete types (``list``, ``dict``, etc.) for
concrete implementations. The return values of protocols
and abstract base classes must be judged on a case-by-case basis.

Yes::

def map_it(input: Iterable[str]) -> list[int]: ...
def create_map() -> dict[str, int]: ...
def to_string(o: object) -> str: ... # accepts any object

No::

def map_it(input: list[str]) -> list[int]: ...
def create_map() -> MutableMapping[str, int]: ...
def to_string(o: Any) -> str: ...

Maybe::

class MyProto(Protocol):
def foo(self) -> list[int]: ...
def bar(self) -> Mapping[str]: ...

Avoid union return types, since they require ``isinstance()`` checks.
Use ``Any`` or ``X | Any`` if necessary.

Stylistic Practices
===================

Shorthand Syntax
----------------

Where possible, use shorthand syntax for unions instead of
``Union`` or ``Optional``. ``None`` should be the last
element of an union.

Yes::

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

No::

def foo(x: Union[str, int]) -> None: ...
def bar(x: Optional[str]) -> Optional[int]: ...
def baz(x: None | str) -> None: ...

Types
-----

Use ``float`` instead of ``int | float``.
Use ``None`` instead of ``Literal[None]``.

Built-in Generics
-----------------

Use built-in generics instead of the aliases from ``typing``,
where possible.

Yes::

from collections.abc import Iterable

def foo(x: type[MyClass]) -> list[str]: ...
def bar(x: Iterable[str]) -> None: ...

No::

from typing import Iterable, List, Type

def foo(x: Type[MyClass]) -> List[str]: ...
def bar(x: Iterable[str]) -> None: ...
1 change: 1 addition & 0 deletions docs/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Type System Reference
:caption: Contents:

stubs
best_practices
quality
typing Module Documentation <https://docs.python.org/3/library/typing.html>

Expand Down
97 changes: 3 additions & 94 deletions docs/source/stubs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ should not reject stubs that do not follow these recommendations, but
linters can warn about them.

Stub files should generally follow the Style Guide for Python Code (PEP 8)
[#pep8]_. There are a few exceptions, outlined below, that take the
[#pep8]_ and the :ref:`best-practices`. There are a few exceptions, outlined below, that take the
different structure of stub files into account and are aimed to create
more concise files.

Expand Down Expand Up @@ -810,24 +810,6 @@ No::
x: int
class MyError(Exception): ... # leave an empty line between the classes

Shorthand Syntax
----------------

Where possible, use shorthand syntax for unions instead of
``Union`` or ``Optional``. ``None`` should be the last
element of an union.

Yes::

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

No::

def foo(x: Union[str, int]) -> None: ...
def bar(x: Optional[str]) -> Optional[int]: ...
def baz(x: None | str) -> None: ...

Module Level Attributes
-----------------------

Expand All @@ -846,24 +828,7 @@ No::
z = 0 # type: int
a = ... # type: int

Type Aliases
------------

Use ``TypeAlias`` for type aliases (but not for regular aliases).

Yes::

_IntList: TypeAlias = list[int]
g = os.stat
Path = pathlib.Path
ERROR = errno.EEXIST

No::

_IntList = list[int]
g: TypeAlias = os.stat
Path: TypeAlias = pathlib.Path
ERROR: TypeAlias = errno.EEXIST
.. _stub-style-classes:

Classes
-------
Expand Down Expand Up @@ -998,67 +963,11 @@ No::
forward_reference: 'OtherClass'
class OtherClass: ...

Types
-----

Generally, use ``Any`` when a type cannot be expressed appropriately
with the current type system or using the correct type is unergonomic.

Use ``float`` instead of ``int | float``.
Use ``None`` instead of ``Literal[None]``.

For arguments, prefer protocols and abstract types (``Mapping``,
``Sequence``, ``Iterable``, etc.). If an argument accepts literally any value,
use ``object`` instead of ``Any``.

For return values, prefer concrete types (``list``, ``dict``, etc.) for
concrete implementations. The return values of protocols
and abstract base classes must be judged on a case-by-case basis.

Yes::

def map_it(input: Iterable[str]) -> list[int]: ...
def create_map() -> dict[str, int]: ...
def to_string(o: object) -> str: ... # accepts any object

No::

def map_it(input: list[str]) -> list[int]: ...
def create_map() -> MutableMapping[str, int]: ...
def to_string(o: Any) -> str: ...

Maybe::

class MyProto(Protocol):
def foo(self) -> list[int]: ...
def bar(self) -> Mapping[str]: ...

Avoid union return types, since they require ``isinstance()`` checks.
Use ``Any`` or ``X | Any`` if necessary.

Use built-in generics instead of the aliases from ``typing``,
where possible. See the section `Built-in Generics`_ for cases,
where it's not possible to use them.

Yes::

from collections.abc import Iterable

def foo(x: type[MyClass]) -> list[str]: ...
def bar(x: Iterable[str]) -> None: ...

No::

from typing import Iterable, List, Type

def foo(x: Type[MyClass]) -> List[str]: ...
def bar(x: Iterable[str]) -> None: ...

NamedTuple and TypedDict
------------------------

Use the class-based syntax for ``typing.NamedTuple`` and
``typing.TypedDict``, following the Classes section of this style guide.
``typing.TypedDict``, following the :ref:`stub-style-classes` section of this style guide.

Yes::

Expand Down

0 comments on commit 831742f

Please sign in to comment.