From 6b9de5c933bc5f0371d8ff2c0c9e6dc1e51285d2 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 1 Feb 2023 11:02:34 -0800 Subject: [PATCH] More improvements to getting started docs (#14572) For the most part, this shortens the Getting Started page, which was getting a little too long to read comfortably and had caveats that aren't super important. The cheat sheet does a really great job of "show, don't tell", so recommend that even more aggressively for beginners. The BankAccount example was nice, and the cheat sheet was missing a discussion on inheritance, so move a version of that over there. Finally, most users of mypy don't need to know the details of typeshed and stub files, especially not when getting started. So reframe as a more generic section about types for third party libraries. Linking #13681 --- docs/source/cheat_sheet_py3.rst | 67 ++++++--- docs/source/getting_started.rst | 237 ++++++++------------------------ docs/source/index.rst | 2 +- docs/source/running_mypy.rst | 5 +- docs/source/stubs.rst | 7 +- 5 files changed, 111 insertions(+), 207 deletions(-) diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index 7179318e31b8..5aa1770512b8 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -34,7 +34,9 @@ Useful built-in types .. code-block:: python - # For most types, just use the name of the type + # For most types, just use the name of the type. + # Note that mypy can usually infer the type of a variable from its value, + # so technically these annotations are redundant x: int = 1 x: float = 1.0 x: bool = True @@ -100,12 +102,18 @@ Functions def show(value: str, excitement: int = 10) -> None: print(value + "!" * excitement) + # Note that arguments without a type are dynamically typed (treated as Any) + # and that functions without any annotations not checked + def untyped(x): + x.anything() + 1 + "string" # no errors + # This is how you annotate a callable (function) value x: Callable[[int, float], float] = f + def register(callback: Callable[[str], int]) -> None: ... # A generator function that yields ints is secretly just a function that # returns an iterator of ints, so that's how we annotate it - def g(n: int) -> Iterator[int]: + def gen(n: int) -> Iterator[int]: i = 0 while i < n: yield i @@ -143,28 +151,49 @@ Classes .. code-block:: python - class MyClass: - # You can optionally declare instance variables in the class body - attr: int - # This is an instance variable with a default value - charge_percent: int = 100 - + class BankAccount: # The "__init__" method doesn't return anything, so it gets return # type "None" just like any other method that doesn't return anything - def __init__(self) -> None: - ... + def __init__(self, account_name: str, initial_balance: int = 0) -> None: + # mypy will infer the correct types for these instance variables + # based on the types of the parameters. + self.account_name = account_name + self.balance = initial_balance # For instance methods, omit type for "self" - def my_method(self, num: int, str1: str) -> str: - return num * str1 + def deposit(self, amount: int) -> None: + self.balance += amount + + def withdraw(self, amount: int) -> None: + self.balance -= amount # User-defined classes are valid as types in annotations - x: MyClass = MyClass() + account: BankAccount = BankAccount("Alice", 400) + def transfer(src: BankAccount, dst: BankAccount, amount: int) -> None: + src.withdraw(amount) + dst.deposit(amount) + + # Functions that accept BankAccount also accept any subclass of BankAccount! + class AuditedBankAccount(BankAccount): + # You can optionally declare instance variables in the class body + audit_log: list[str] + # This is an instance variable with a default value + auditor_name: str = "The Spanish Inquisition" + + def __init__(self, account_name: str, initial_balance: int = 0) -> None: + super().__init__(account_name, initial_balance) + self.audit_log: list[str] = [] + + def deposit(self, amount: int) -> None: + self.audit_log.append(f"Deposited {amount}") + self.balance += amount + + def withdraw(self, amount: int) -> None: + self.audit_log.append(f"Withdrew {amount}") + self.balance -= amount - # You can also declare the type of an attribute in "__init__" - class Box: - def __init__(self) -> None: - self.items: list[str] = [] + audited = AuditedBankAccount("Bob", 300) + transfer(audited, account, 100) # type checks! # You can use the ClassVar annotation to declare a class variable class Car: @@ -172,9 +201,7 @@ Classes passengers: ClassVar[list[str]] # If you want dynamic attributes on your class, have it - # override "__setattr__" or "__getattr__": - # - "__getattr__" allows for dynamic access to names - # - "__setattr__" allows for dynamic assignment to names + # override "__setattr__" or "__getattr__" class A: # This will allow assignment to any A.x, if x is the same type as "value" # (use "value: Any" to allow arbitrary types) diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index db7c18d5e242..bbe2d25d3b03 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -132,8 +132,8 @@ may be useful. See :ref:`getting-to-strict` for how to build up to ``--strict``. See :ref:`command-line` and :ref:`config-file` for a complete reference on configuration options. -Additional types, and the typing module -*************************************** +More complex types +****************** So far, we've added type hints that use only basic concrete types like ``str`` and ``float``. What if we want to express more complex types, @@ -159,28 +159,11 @@ accept one or more *type parameters*. In this case, we *parameterized* :py:class by writing ``list[str]``. This lets mypy know that ``greet_all`` accepts specifically lists containing strings, and not lists containing ints or any other type. -In Python 3.8 and earlier, you can instead import the -:py:class:`~typing.List` type from the :py:mod:`typing` module: - -.. code-block:: python - - from typing import List # Python 3.8 and earlier - - def greet_all(names: List[str]) -> None: - for name in names: - print('Hello ' + name) - - ... - -You can find many of these more complex static types in the :py:mod:`typing` module. - In the above examples, the type signature is perhaps a little too rigid. After all, there's no reason why this function must accept *specifically* a list -- it would run just fine if you were to pass in a tuple, a set, or any other custom iterable. -You can express this idea using the -:py:class:`collections.abc.Iterable` (or :py:class:`typing.Iterable` in Python -3.8 and earlier) type instead of :py:class:`list` : +You can express this idea using :py:class:`collections.abc.Iterable`: .. code-block:: python @@ -190,8 +173,19 @@ You can express this idea using the for name in names: print('Hello ' + name) +This behavior is actually a fundamental aspect of the PEP 484 type system: when +we annotate some variable with a type ``T``, we are actually telling mypy that +variable can be assigned an instance of ``T``, or an instance of a *subtype* of ``T``. +That is, ``list[str]`` is a subtype of ``Iterable[str]``. + +This also applies to inheritance, so if you have a class ``Child`` that inherits from +``Parent``, then a value of type ``Child`` can be assigned to a variable of type ``Parent``. +For example, a ``RuntimeError`` instance can be passed to a function that is annotated +as taking an ``Exception``. + As another example, suppose you want to write a function that can accept *either* -ints or strings, but no other types. You can express this using the :py:data:`~typing.Union` type: +ints or strings, but no other types. You can express this using the +:py:data:`~typing.Union` type. For example, ``int`` is a subtype of ``Union[int, str]``: .. code-block:: python @@ -203,26 +197,12 @@ ints or strings, but no other types. You can express this using the :py:data:`~t else: return user_id -Similarly, suppose that you want the function to accept only strings or ``None``. You can -again use :py:data:`~typing.Union` and use ``Union[str, None]`` -- or alternatively, use the type -``Optional[str]``. These two types are identical and interchangeable: ``Optional[str]`` -is just a shorthand or *alias* for ``Union[str, None]``. It exists mostly as a convenience -to help function signatures look a little cleaner: +The :py:mod:`typing` module contains many other useful types. -.. code-block:: python +For a quick overview, look through the :ref:`mypy cheatsheet `. - from typing import Optional - - def greeting(name: Optional[str] = None) -> str: - # Optional[str] means the same thing as Union[str, None] - if name is None: - name = 'stranger' - return 'Hello, ' + name - -The :py:mod:`typing` module contains many other useful types. You can find a -quick overview by looking through the :ref:`mypy cheatsheet ` -and a more detailed overview (including information on how to make your own -generic types or your own type aliases) by looking through the +For a detailed overview (including information on how to make your own +generic types or your own type aliases), look through the :ref:`type system reference `. .. note:: @@ -250,10 +230,7 @@ mypy will try and *infer* as many details as possible. We saw an example of this in the ``normalize_id`` function above -- mypy understands basic :py:func:`isinstance ` checks and so can infer that the ``user_id`` variable was of -type ``int`` in the if-branch and of type ``str`` in the else-branch. Similarly, mypy -was able to understand that ``name`` could not possibly be ``None`` in the ``greeting`` -function above, based both on the ``name is None`` check and the variable assignment -in that if statement. +type ``int`` in the if-branch and of type ``str`` in the else-branch. As another example, consider the following function. Mypy can type check this function without a problem: it will use the available context and deduce that ``output`` must be @@ -268,114 +245,16 @@ of type ``list[float]`` and that ``num`` must be of type ``float``: output.append(num) return output -Mypy will warn you if it is unable to determine the type of some variable -- -for example, when assigning an empty dictionary to some global value: - -.. code-block:: python - - my_global_dict = {} # Error: Need type annotation for "my_global_dict" - -You can teach mypy what type ``my_global_dict`` is meant to have by giving it -a type hint. For example, if you knew this variable is supposed to be a dict -of ints to floats, you could annotate it using either variable annotations -(introduced in Python 3.6 by :pep:`526`) or using a comment-based -syntax like so: - -.. code-block:: python - - # If you're using Python 3.9+ - my_global_dict: dict[int, float] = {} - - # If you're using Python 3.6+ - my_global_dict: Dict[int, float] = {} - - -Types and classes -***************** - -So far, we've only seen examples of pre-existing types like the ``int`` -or ``float`` builtins, or generic types from ``collections.abc`` and -``typing``, such as ``Iterable``. However, these aren't the only types you can -use: in fact, you can use any Python class as a type! - -For example, suppose you've defined a custom class representing a bank account: - -.. code-block:: python - - class BankAccount: - # Note: It is ok to omit type hints for the "self" parameter. - # Mypy will infer the correct type. - - def __init__(self, account_name: str, initial_balance: int = 0) -> None: - # Note: Mypy will infer the correct types of your fields - # based on the types of the parameters. - self.account_name = account_name - self.balance = initial_balance - - def deposit(self, amount: int) -> None: - self.balance += amount - - def withdraw(self, amount: int) -> None: - self.balance -= amount - - def overdrawn(self) -> bool: - return self.balance < 0 +For more details, see :ref:`type-inference-and-annotations`. -You can declare that a function will accept any instance of your class -by simply annotating the parameters with ``BankAccount``: - -.. code-block:: python - - def transfer(src: BankAccount, dst: BankAccount, amount: int) -> None: - src.withdraw(amount) - dst.deposit(amount) - - account_1 = BankAccount('Alice', 400) - account_2 = BankAccount('Bob', 200) - transfer(account_1, account_2, 50) - -In fact, the ``transfer`` function we wrote above can accept more then just -instances of ``BankAccount``: it can also accept any instance of a *subclass* -of ``BankAccount``. For example, suppose you write a new class that looks like this: - -.. code-block:: python - - class AuditedBankAccount(BankAccount): - def __init__(self, account_name: str, initial_balance: int = 0) -> None: - super().__init__(account_name, initial_balance) - self.audit_log: list[str] = [] - - def deposit(self, amount: int) -> None: - self.audit_log.append(f"Deposited {amount}") - self.balance += amount - - def withdraw(self, amount: int) -> None: - self.audit_log.append(f"Withdrew {amount}") - self.balance -= amount - -Since ``AuditedBankAccount`` is a subclass of ``BankAccount``, we can directly pass -in instances of it into our ``transfer`` function: - -.. code-block:: python - - audited = AuditedBankAccount('Charlie', 300) - transfer(account_1, audited, 100) # Type checks! - -This behavior is actually a fundamental aspect of the PEP 484 type system: when -we annotate some variable with a type ``T``, we are actually telling mypy that -variable can be assigned an instance of ``T``, or an instance of a *subclass* of ``T``. -The same rule applies to type hints on parameters or fields. - -See :ref:`class-basics` to learn more about how to work with code involving classes. - - -.. _stubs-intro: +Types from libraries +******************** -Stubs files and typeshed -************************ +Mypy can also understand how to work with types from libraries that you use. -Mypy also understands how to work with classes found in the standard library. -For example, here is a function which uses the ``Path`` object from the +For instance, mypy comes out of the box with an intimate knowledge of the +Python standard library. For example, here is a function which uses the +``Path`` object from the `pathlib standard library module `_: .. code-block:: python @@ -383,52 +262,43 @@ For example, here is a function which uses the ``Path`` object from the from pathlib import Path def load_template(template_path: Path, name: str) -> str: - # Mypy understands that 'file_path.read_text()' returns a str... + # Mypy knows that `file_path` has a `read_text` method that returns a str template = template_path.read_text() - - # ...so understands this line type checks. + # ...so it understands this line type checks return template.replace('USERNAME', name) -This behavior may surprise you if you're familiar with how -Python internally works. The standard library does not use type hints -anywhere, so how did mypy know that ``Path.read_text()`` returns a ``str``, -or that ``str.replace(...)`` accepts exactly two ``str`` arguments? +If a third party library you use :ref:`declares support for type checking `, +mypy will type check your use of that library based on the type hints +it contains. -The answer is that mypy comes bundled with *stub files* from the -the `typeshed `_ project, which -contains stub files for the Python builtins, the standard library, -and selected third-party packages. +However, if the third party library does not have type hints, mypy will +complain about missing type information. -A *stub file* is a file containing a skeleton of the public interface -of that Python module, including classes, variables, functions -- and -most importantly, their types. +.. code-block:: text -Mypy complains if it can't find a stub (or a real module) for a -library module that you import. Some modules ship with stubs or inline -annotations that mypy can automatically find, or you can install -additional stubs using pip (see :ref:`fix-missing-imports` and -:ref:`installed-packages` for the details). For example, you can install -the stubs for the ``requests`` package like this: + prog.py:1: error: Library stubs not installed for "yaml" + prog.py:1: note: Hint: "python3 -m pip install types-PyYAML" + prog.py:2: error: Library stubs not installed for "requests" + prog.py:2: note: Hint: "python3 -m pip install types-requests" + ... -.. code-block:: shell +In this case, you can provide mypy a different source of type information, +by installing a *stub* package. A stub package is a package that contains +type hints for another library, but no actual code. - $ python3 -m pip install types-requests +.. code-block:: shell -The stubs are usually packaged in a distribution named -``types-``. Note that the distribution name may be -different from the name of the package that you import. For example, -``types-PyYAML`` contains stubs for the ``yaml`` package. Mypy can -often suggest the name of the stub distribution: + $ python3 -m pip install types-PyYAML types-requests -.. code-block:: text +Stubs packages for a distribution are often named ``types-``. +Note that a distribution name may be different from the name of the package that +you import. For example, ``types-PyYAML`` contains stubs for the ``yaml`` +package. - prog.py:1: error: Library stubs not installed for "yaml" - prog.py:1: note: Hint: "python3 -m pip install types-PyYAML" - ... +For more discussion on strategies for handling errors about libraries without +type information, refer to :ref:`fix-missing-imports`. -You can also :ref:`create -stubs ` easily. We discuss strategies for handling errors -about missing stubs in :ref:`ignore-missing-imports`. +For more information about stubs, see :ref:`stub-files`. Next steps ********** @@ -463,5 +333,8 @@ resources: `mypy issue tracker `_ and typing `Gitter chat `_. +* For general questions about Python typing, try posting at + `typing discussions `_. + You can also continue reading this document and skip sections that aren't relevant for you. You don't need to read sections in order. diff --git a/docs/source/index.rst b/docs/source/index.rst index 27b3a078af6c..3546e1f4efa5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -52,8 +52,8 @@ Contents :caption: First steps getting_started - existing_code cheat_sheet_py3 + existing_code .. _overview-type-system-reference: diff --git a/docs/source/running_mypy.rst b/docs/source/running_mypy.rst index 4a7b5dcf4093..ffc04e6ea14c 100644 --- a/docs/source/running_mypy.rst +++ b/docs/source/running_mypy.rst @@ -281,8 +281,9 @@ will continue to be of type ``Any``. line containing the import. 2. To suppress *all* missing import errors from a single library, add - a section to your :ref:`mypy config file ` for that library setting - :confval:`ignore_missing_imports` to True. For example, suppose your codebase + a per-module section to your :ref:`mypy config file ` setting + :confval:`ignore_missing_imports` to True for that library. For example, + suppose your codebase makes heavy use of an (untyped) library named ``foobar``. You can silence all import errors associated with that library and that library alone by adding the following section to your config file:: diff --git a/docs/source/stubs.rst b/docs/source/stubs.rst index af47a0e2afdd..7c84a9718b3e 100644 --- a/docs/source/stubs.rst +++ b/docs/source/stubs.rst @@ -3,12 +3,15 @@ Stub files ========== +A *stub file* is a file containing a skeleton of the public interface +of that Python module, including classes, variables, functions -- and +most importantly, their types. + Mypy uses stub files stored in the `typeshed `_ repository to determine the types of standard library and third-party library functions, classes, and other definitions. You can also create your own stubs that will be -used to type check your code. The basic properties of stubs were introduced -back in :ref:`stubs-intro`. +used to type check your code. Creating a stub ***************