Skip to content

Handling exceptions within a @contextmanager function doesn't clear sys.exception() #111375

Open
@pR0Ps

Description

@pR0Ps

Bug report

Bug description:

The issue

I would expect that handling exceptions within a contextlib.contextmanager-created function would work in the same way as other functions and clear the sys.exception() after an error is handled.

import contextlib
import sys

def p(msg):
    print(msg, repr(sys.exception()), sep=": ")

def ctx_gen():
    p("before yield")
    try:
        yield
    except:
        p("during handling")
    p("after handling")

ctx = contextlib.contextmanager(ctx_gen)

with ctx():
    1/0

The above example prints:

before yield: None
during handling: ZeroDivisionError('division by zero')
after handling: ZeroDivisionError('division by zero')

Whereas since the error was handled by the except: block, my expectation was:

before yield: None
during handling: ZeroDivisionError('division by zero')
after handling: None

Just working as designed?

From doing some digging, it seems like this is happening because the exception is still being handled by the _GeneratorContextManager.__exit__ function (added by the @contextlib.contextmanager decorator) that's driving the ctx_gen generator, even after the ctg_gen has handled it.

The following is a very rough approximation of how @contextlib.contextmanager drives ctx_gen:

c = ctx_gen()
next(c)  # __enter__()
try:
    # code inside the with block
    1/0
except Exception as e:  # __exit__(typ, exc, tb) for e
    # throw exception into generator and expect to run to end
    try:
        c.throw(e) 
    except StopIteration:
        pass
else:  # __exit__(None, None, None)
    # expect to run to end
    try:
        next(e)
    except StopIteration:
        pass

Running the above (including the definitions from the first code block) also prints:

before yield: None
during handling: ZeroDivisionError('division by zero')
after handling: ZeroDivisionError('division by zero')

In the above code, it's more clear that the exception is still being handled by the except Exception as e: block until c.throw() returns/raises, which only happens after the generator exits. Therefore the exception is still being handled the entire time ctx_gen is running all the code after the first yield.

The fix?

Even though this behavior looks to be technically correct, it still seems unexpected and a bit of an abstraction leak.

Is this something that can be/should be fixed? Or should the behavior just be documented?

CPython versions tested on:

3.8, 3.9, 3.10, 3.11, CPython main branch

Operating systems tested on:

macOS

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibPython modules in the Lib dirtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions