Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Apr 10, 2022. It is now read-only.
This repository was archived by the owner on Apr 10, 2022. It is now read-only.

High-level outline of the first draft #2

Copy link
Copy link
Open
@1st1

Description

@1st1
Issue body actions

@njsmith @ambv feel free to edit this message.


Introduction

This should be a relatively high-level and fun to read section.

  • Talk about exceptions in an abstract way. I.e. why it is important to have them in Python, why one of the key Python design principle is that errors should never happen silently.

  • Why isn't one exception enough? Sometimes you have multiple errors that are all equally important:

    • Main reason: Concurrency (async/await, threads, multiprocessing, ...): If you're doing multiple things at once, then multiple things can fail at once. Only options are to catch them, throw some away, or have some way to propagate multiple exceptions at once. We can't force users to catch them, so right now we have to throw them away, which breaks "Errors should never pass silently".

    • Hypothesis runs a single test lots of times with random input. Sometimes it detects multiple distinct failures, on different inputs. Currently it has no good way to report that.

    • A unit test: the test failed and also failed its tear down code.

  • Why aren't __context__ and __cause__ good enough? (They give more details about a single exception, but in these cases there are multiple independent errors, that need to be handled or propagated independently. Maybe a concrete example, e.g., you have both a network error + an assertion error, some routine retry code handles the network error, that shouldn't cause the assertion error to be lost?)

Motivation

Here we should talk about exception groups in more detail explaining "why" and "why now".

  • Why we are looking at this problem now? The answer is async/await. Here we need to talk about asyncio.gather() and Trio/nurseries.

    • asyncio.gather(t1, t2, t3) is the primitive one uses in asyncio to wait until multiple concurrent tasks are finished. It is flawed. If t1 is failed then we'd propagate its error and try to shutdown both t2 and t3, but shutting them down can also result in errors. Those errors are currently lost.

    • There's a return_exceptions=True flag for asyncio.gather that changes its behavior entirely. Exceptions are returned along with the results. This is a slightly better approach, but: it's an opt-in; handling exceptions this way is cumbersome -- you suddenly can't use try..except block.

    • Controlling how things run concurrently is the key thing in asyncio. Current APIs are bad and we need a replacement ASAP.

    • Trio has a concept called nurseries and it's great. This is what users want in asyncio—literally the most requested feature. Nurseries are context managers that let you structure your async code in a very visual and obvious way.

    • The problem is that a nursery with block needs to propagate more than one exception. Boom, we need exception groups (EG) to implement them for asyncio.

  • To sum up: there were always use cases that required a concept like EGs. It's just that they were not as pronounced as they are now with async/await.

  • Why does it need to be in the language/stdlib? Can it be a third party library?

    1. asyncio is in the stdlib, so everything asyncio uses has to be in the stdlib
    2. Also, Trio tried that, and it kind of works but there's a lot of unfixable limitations:
      • lots of basic exception stuff lives in stdlib, e.g. sys.excepthook needs to know about these, the traceback and logging modules need to know about these, etc.
      • exceptions are often raised and caught in different pieces of code, e.g. pytest/ipython/sentry/ubuntu's apport/web frameworks/... all want to handle arbitrary exceptions and do something useful with them, so they and the async libraries all need to agree on how to represent these new objects. Ecosystem-wide coordination is what PEPs are for.
      • Currently trio handles this by monkeypatching all the libraries it knows about...

What does an EG look like?

  • Summarize our answer: a concrete (final?) class ExceptionGroup that inherits from BaseException, and holds a list of exceptions + for each one an string tag giving some human-readable info about where this exception came from to enter this group. Show examples. Show nested examples.

Then walk through the rationale for these choices:

  • The EG type must be an exception itself. It cannot be a tuple or list. Why: we want try..finally to work correctly when an exception group is propagated. Also, making sys.exc_info() / PyThreadState->exc_info start holding non-exception objects would be super invasive and probably break lots of things.

  • The EG type must be a BaseException as it can potentially contain multiple BaseExceptions.

  • To give useful tracebacks, we want to preserve the path the exception took, so these need to be nested (show an example of a Trio nested traceback to illustrate)

  • We want to attach tags / titles to exceptions within EGs. Tasks in asyncio/trio and threads in Python have names -- we want to attach a bit of information to every exception within a EG saying what's its origin.

  • But semantically, they represent an unstructured set of exceptions; we just use the tree structure to hold traceback info and to get a single object representing the set

  • Discuss why we allow single-element ExceptionGroups

    • Gives opportunity to attach tags to show which tasks an exception passed through as it propagated

    • In current prototypes, catching exceptions inside an ExceptionGroup requires special ceremony. If this ceremony is needed sometimes, then we want to make it needed always, so that users don't accidentally use regular try/except and have it seem to work until they get unlucky and multiple exceptions happen at the same time. Therefore, exceptions that pass through a Trio nursery/asyncio TaskGroup should be unconditionally wrapped in an ExceptionGroup. But, this rationale may or may not apply to a "native" version of ExceptionGroups, depending on what design we end up with for catching them.

Working with exception groups

  • Core primitives: split and leaves

  • Semantics

  • Rationale

Catching exceptions in ExceptionGroups

  • Explain basic desired semantics: can have multiple handler "arms", multiple arms can match the same group, they're executed in sequence, then any unhandled exceptions + new exceptions are bundled up into a new ExceptionGroup

  • How should this be spelled? We're not sure. Trade-offs are extremely messy; we're not even going to try doing a full discussion in this first draft. Some options we see:

    • Modifying the behavior of try..except to have these semantics. (Downside: major change to the language!)

    • Leave try/except alone, add new syntax (grouptry/groupexcept or whatever). (Downside: you always want grouptry/groupexcept, never try/except!)

    • No new syntax, use awkward circumlocutions instead of try/except. (Downside: they're extremely awkward!)

Since the design space and trade-offs are super complex, we're leaving a full discussion for a later draft / follow-up PEP.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.