# Suppression of exceptions within context managers vs variable definitions

**URL:** <https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256>\
**Category:** Typing\
**Created:** [February 29, 2024, 10:11pm UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256 "2024-02-29T22:11:20Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![Eneg](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/eneg/32/18177_2.png) [@Eneg](https://discuss.python.org/u/Eneg)\
**Post date:** [February 29, 2024, 10:11pm UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/1 "2024-02-29T22:11:20Z")

</div>

I post this under #Typing as there doesn’t seem to be a tools-specific category.

Context managers which suppress exceptions raised within their body can lead to unexpected errors, which current tooling has few to no ways of detecting.

Given scenario:

```py
from contextlib import suppress

def maybe_throws(throw: bool) -> None:
      # not all codepaths lead to an exception, so it's not (...) -> NoReturn
    if throw:
        raise Exception

with suppress(Exception):
    maybe_throws(True)
    foo = 123

print(foo)

```

Result of testing in [pyright playground](https://pyright-play.net/?pythonVersion=3.10&strict=true&code=GYJw9gtgBAxmB2AXApgD0QGwJYCMpYgAcwREoBnAV0MJGXPIFgAoFgE2WCggEMBPHMgD6iABbgA7uQAUYyQC4oOMGAwBKKAFoAfFAByCZPJZRTZgMRR4YMjwwZYYDoR5jyUDMh5soiMFB54KDQYZEJELAQAGgp-LEQAcndrMmkAOgyNHX0wACVkREoQeBMzLC45MAljZjM6qBAeLHJkKABRVFDwyJLas1osJGkAIgBlRH4A7AA3ZGG1FhYJeNEKalp6GQ6uiIQ1GrrgFSgAXigARgAmAGZF5gGho7A1IA):  
A report is emitted, stating that `foo` at `print` can be unbound. However, removing the call to `maybe_throws` doesn’t get rid of the warning, suggesting that pyright special cases `suppress` and simply emits warnings for any variable definitions originating from within the context manager’s body.  
Running the code in Mypy’s playground produces no warnings.

> **This problem caught me off-guard while working with anyio.**
>
> The linter issues no warnings for the following code:
> 
> ```py
> import anyio
> 
> async def main():
> with anyio.move_on_after(5):
> await anyio.sleep_forever() # do something to exceed time limit
> foo = 123
> print(foo)
> 
> if __name__ == " __main__":
> anyio.run(main)
> 
> ```

I’d like to discuss what could be done to give linters a deterministic solution in catching this problem, in analogous way to how

```py
try:
    maybe_throws(True)
    foo = 123
except Exception:
    pass

print(foo)

```

Has a warning emitted for.

---

<div class="post-metadata">

**Author:** ![Eneg](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/eneg/32/18177_2.png) [@Eneg](https://discuss.python.org/u/Eneg)\
**Post date:** [February 29, 2024, 10:36pm UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/2 "2024-02-29T22:36:47Z")

</div>

I thought of a special form akin to `TypeGuard`, like `Supresses` or `Supressing`. But the usage leaves a lot to ask for:

```py
class CtxManager:
    def __enter__ (self) -> Supresses[Exceptions?, ReturnType?]:
        # should it be on enter or exit?
        ...
    def __exit__ (self, *exc_spec) -> Suppressing[what?]:
        ...

@contextmanager
def supress(*exceptions: type[BaseException]) -> Supressing[abc.Iterator[None]]:
    # annotation only valid in conjunction with contextmanager
    # what'd prevent one from using it without the decorator?
    try:
        yield
    except exceptions:
        pass

```

FWIW, not great.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [February 29, 2024, 11:37pm UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/3 "2024-02-29T23:37:36Z")

</div>

Are you asking for more or less warnings? The `supress` example you provide should provide a warning. Type checkers are not able to detect what exceptions could happen when, so they have to assume than an exception happens as soon as possible (if they want to care at all, `mypy` appears to not care). Adding exception declarations is a highly controversial topic discussed at length in other threads.

To add an extra warning, type checkers could try to figure out if ` __exit__ ` can ever return a truthy value, and if so, treat the body of the with statement as potentially not executed. Anything more granular \[1\] than that would require code path analysis that currently doesn’t exists and isn’t planned (although you could ofcourse start a PEP. But expect a lot of pushback).

* * *

1. e.g. your suggestion of listing Exception types

---

<div class="post-metadata">

**Author:** ![erictraut](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/erictraut/32/15190_2.png) [@erictraut](https://discuss.python.org/u/erictraut)\
**Post date:** [February 29, 2024, 11:42pm UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/4 "2024-02-29T23:42:52Z")

</div>

> [@Eneg](#):
>
> suggesting that pyright special cases `suppress` and simply emits warnings for any variable definitions originating from within the context manager’s body.

No, that’s not what pyright is doing. Pyright assumes that the evaluation of any expression or statement found within a `try` block could potentially generate an exception, and it creates edges in the code flow graph from those locations to the end of the context manager block. Then it performs code flow analysis to determine whether each variable is assigned a value along all possible code flow paths.

Your example includes the statement `foo = 123`. It’s possible (although admittedly very unlikely) that the runtime evaluation of `123` could raise an exception. If that occurs, then the exception will be caught and suppressed, and `foo` will be unbound.

Pyright doesn’t try to distinguish which expressions or statements could possibly result in an exception because almost any expression or statement can theoretically do so.

Pyright has no special-case knowledge of `suppress` or any other context manager. It simply looks at the return result of the ` __exit__ ` method to determine whether the context manager suppresses exceptions. In this regard, the behavior of pyright and mypy should be the same.

The difference you’re seeing above between `contextlib.suppress` and `anyio.move_on_after` is that the form has an ` __exit__ ` method that returns `bool` and the latter returns `bool | None`.

---

<div class="post-metadata">

**Author:** ![Eneg](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/eneg/32/18177_2.png) [@Eneg](https://discuss.python.org/u/Eneg)\
**Post date:** [February 29, 2024, 11:51pm UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/5 "2024-02-29T23:51:35Z")

</div>

Thanks for insight:

> [@erictraut](#):
>
> It simply looks at the return result of the ` __exit__ ` method to determine whether the context manager suppresses exceptions

This would answer my (not well put) question - linters look specifically for `bool`.

Can you tell why is making the bool optional changing this behavior?

---

<div class="post-metadata">

**Author:** ![hauntsaninja](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/hauntsaninja/32/12451_2.png) [@hauntsaninja](https://discuss.python.org/u/hauntsaninja)\
**Post date:** [March 1, 2024, 2:12am UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/6 "2024-03-01T02:12:24Z")

</div>

> [@Eneg](#):
>
> Can you tell why is making the bool optional changing this behavior?

This is inspired by Python’s data model, see [3. Data model — Python 3.12.2 documentation](https://docs.python.org/3/reference/datamodel.html#object. __exit__ )

> If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.

This was originally proposed here: [Bug: false positive with --warn-unreachable · Issue #7214 · python/mypy · GitHub](https://github.com/python/mypy/issues/7214#issuecomment-513618450)

[Specify how type checkers should interpret ` __exit__ ` annotations · Issue #1535 · python/typing · GitHub](https://github.com/python/typing/issues/1535) is the issue tracking enshrining this heuristic in the specification for the Python type system

---

<div class="post-metadata">

**Author:** ![Eneg](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/eneg/32/18177_2.png) [@Eneg](https://discuss.python.org/u/Eneg)\
**Post date:** [March 1, 2024, 3:09am UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/7 "2024-03-01T03:09:36Z")

</div>

> [@hauntsaninja](#):
>
> This is inspired by Python’s data model, see [3. Data model — Python 3.12.2 documentation](https://docs.python.org/3/reference/datamodel.html#object. __exit__ )

Yup, I read it, but it doesn’t explain why going from `bool` to `bool | None` makes the typecheckers stop treating the manager as suppressing (or at least, stop emitting warnings about potential unbounds).

The linked issues did clarify the reason though.

---

<div class="post-metadata">

**Author:** ![alicederyn](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/alicederyn/32/10282_2.png) [@alicederyn](https://discuss.python.org/u/alicederyn)\
**Post date:** [March 2, 2024, 11:45am UTC](https://discuss.python.org/t/suppression-of-exceptions-within-context-managers-vs-variable-definitions/47256/8 "2024-03-02T11:45:22Z")

</div>

> [@Eneg](#):
>
> The linked issues did clarify the reason though.

For reference for anyone else following along:

> One possible pragmatic solution is to assume that contextmanagers don’t swallow exceptions, unless they are annotated as returning bool (but not Optional[bool], because that’s what typing.ContextManager returns and most general context managers don’t swallow exceptions)
