Skip to content

Python Typing Best Practices

Mateusz Paprocki edited this page May 28, 2021 · 1 revision

Python Static Typing (typing & mypy)

Lazy annotations

from __future__ import annotations

def f() -> A: # forward reference
  return A(0)

class A:
  def __init__(self, x: int | str): # new syntax not supported at run time
    self._x = x
  def clone() -> A: # self reference
    return A(self._x)

Otherwise all those types would need to be wrapped in quotes to prevent run time failures.

Type alias vs NewType

ID = str
def f(id: ID): pass
f("foo")
from typing import NewType
ID = NewType("ID", str)
x: ID = "foo"
error: Incompatible types in assignment (expression has type "str", variable has type "ID")  [assignment]
x: ID = ID("foo")

Optional imports

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    import pandas as pd
else:
    from bokeh.util.dependencies import import_optional
    pd = import_optional("pandas")

Union[A, B] vs. A | B

Both are equivalent, but the later is Python 3.10 syntax, however usable with lazy annotations:

class A: pass
class B: pass

def f() -> A | B: pass
TypeError: unsupported operand type(s) for |: 'type' and 'type'

from __future__ import annotations
def f() -> A | B: pass

Prefer A | None over Optional[A], because None may not indicate semantic optionality, but a perfectly valid value.

Note A | B syntax pre 3.10 may cause issues with tooling analyzing __annotations__.

Class variables

from typing import ClassVar, Set

class X:
  known_instances: ClassVar[Set[X]] = set()

  def __init__(self):
    self.known_instances.add(self)

TypedDict with missing keys

Imitate TypeScript's plain objects with optional fields, e.g. {a?: int, b?: string | null}:

from typing import TypedDict
class Some(TypedDict, total=False):
  a: int
  b: str | None

assert Some() == {}
assert Some(b=None) == {"b": None}

Sadly can't make a subset of fields optional.

Don't use Optional[A] or A | None unnecessarily

def f(x: Optional[int] = 0):
  pass

If None is not supported by the implementation, then don't use it to indicate optionality, as that's already indicated by providing the default value.

Supporting multiple versions of Python

We currently support Python 3.7, 3.8 and 3.9. For example:

  • in Python 3.7:
>>> from typing import TypedDict
ImportError: cannot import name 'TypedDict' from 'typing'

>>> from typing_extensions import TypedDict
  • in Python 3.8 and 3.9:
>>> from typing import TypedDict

>>> from typing_extensions import TypedDict

Other useful types missing in 3.7 are Literal and Protocol.

conda install -c conda-forge typing_extensions