From 50b3cff5d55d6246382b669ada401f8eae3967b0 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 14 Apr 2024 22:33:09 -0700 Subject: [PATCH 1/3] =?UTF-8?q?Added=20typing=20spec=20chapter=20focused?= =?UTF-8?q?=20on=20exception=20behavior=20=E2=80=94=20in=20particular,=20c?= =?UTF-8?q?ontext=20managers=20and=20whether=20they=20suppress=20exception?= =?UTF-8?q?s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/spec/exceptions.rst | 42 ++++++++++++++++++++++++++++++++++++++++ docs/spec/index.rst | 1 + 2 files changed, 43 insertions(+) create mode 100644 docs/spec/exceptions.rst diff --git a/docs/spec/exceptions.rst b/docs/spec/exceptions.rst new file mode 100644 index 000000000..8759491d7 --- /dev/null +++ b/docs/spec/exceptions.rst @@ -0,0 +1,42 @@ +Exceptions +========== + +Some type checking behaviors, such as type narrowing and reachability analysis, +require a type checker to understand code flow. Code flow normally proceeds +from one statement to the next, but some statements such as ``for``, ``while`` +and ``return`` can change the code flow. Similarly, ``try``/``except``/``finally`` +statements affect code flow and therefore can affect type evaluation. For example:: + + x = None + try: + some_function() + x = 1 + except: + pass + + # The type of `x` at this point could be None if `some_function` raises + # an exception or `Literal[1]` if it doesn't, so a type checker may + # choose to narrow its type based on this analysis. + reveal_type(x) # Literal[1] | None + + +Context Managers +---------------- + +Context managers may optionally "suppress" exceptions. When such a context +manager is used, any exceptions that are raised and otherwise uncaught within +the ``with`` block are caught by the context manager, and control continues +immediately after the ``with`` block. If a context manager does not suppress +exceptions (as is typically the case), any exceptions that are raised and +otherwise uncaught within the ``with`` block propagate beyond the ``with`` +block. + +Type checkers that employ code flow analysis must be able to distinguish +between these two cases. This is done by examining the return type +annotation of the ``__exit__`` method of the context manager. + +If the return type of the ``__exit__`` method is specifically ``bool`` or +``Literal[True]``, a type checker should assume that exceptions *are* +suppressed. For any other return type, a type checker should assume that +exceptions *are not* suppressed. Examples include: ``Any``, ``Literal[False]``, +``None``, and ``bool | None``. diff --git a/docs/spec/index.rst b/docs/spec/index.rst index 3e77898ce..6e5e5c00b 100644 --- a/docs/spec/index.rst +++ b/docs/spec/index.rst @@ -19,6 +19,7 @@ Specification for the Python type system callables constructors overload + exceptions dataclasses typeddict tuples From 2213b5db3b2ccca4d6f8566fa99c4369dc9e171a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 15 Apr 2024 09:05:21 -0700 Subject: [PATCH 2/3] Incorporated PR feedback. --- docs/spec/exceptions.rst | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/spec/exceptions.rst b/docs/spec/exceptions.rst index 8759491d7..7028f0d66 100644 --- a/docs/spec/exceptions.rst +++ b/docs/spec/exceptions.rst @@ -36,7 +36,27 @@ between these two cases. This is done by examining the return type annotation of the ``__exit__`` method of the context manager. If the return type of the ``__exit__`` method is specifically ``bool`` or -``Literal[True]``, a type checker should assume that exceptions *are* +``Literal[True]``, a type checker should assume that exceptions *can be* suppressed. For any other return type, a type checker should assume that exceptions *are not* suppressed. Examples include: ``Any``, ``Literal[False]``, ``None``, and ``bool | None``. + +This convention was chosen because most context managers do not suppress +exceptions, and it is common for their ``__exit__`` method to be annotated as +returning ``bool | None``. Context managers that suppress exceptions are +relatively rare, so they are considered a special case. + +For example, the following context manager suppresses exceptions:: + + class Suppress: + def __enter__(self) -> None: + pass + + def __exit__(self, exc_type, exc_value, traceback) -> bool: + return True + + with Suppress(): + raise ValueError("This exception is suppressed") + + # The exception is suppressed, so this line is reachable. + print("Code is reachable") From 00b9a417f9229ac8a915fca0c52f55245afeb7cd Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 21 Apr 2024 17:08:39 -0700 Subject: [PATCH 3/3] Incorporated feedback from Guido. --- docs/spec/exceptions.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/spec/exceptions.rst b/docs/spec/exceptions.rst index 7028f0d66..aae3fe2c1 100644 --- a/docs/spec/exceptions.rst +++ b/docs/spec/exceptions.rst @@ -11,7 +11,7 @@ statements affect code flow and therefore can affect type evaluation. For exampl try: some_function() x = 1 - except: + except NotImplementedError: pass # The type of `x` at this point could be None if `some_function` raises @@ -23,7 +23,8 @@ statements affect code flow and therefore can affect type evaluation. For exampl Context Managers ---------------- -Context managers may optionally "suppress" exceptions. When such a context +A context manager may optionally "suppress" exceptions by returning ``True`` +(or some other truthy value) from its ``__exit__`` method. When such a context manager is used, any exceptions that are raised and otherwise uncaught within the ``with`` block are caught by the context manager, and control continues immediately after the ``with`` block. If a context manager does not suppress