Returning value when exiting python context manager

Yes. It is impossible to alter the return value of the context from inside __exit__.

If the context is exited with a return statement, you cannot alter the return value with your context_manager.__exit__. This is different from a try ... finally ... clause, because the code in finally still belongs to the parent function, while context_manager.__exit__ runs in its own scope .

In fact, __exit__ can return a boolean value (True or False) and it will be understood by Python. It tells Python whether the exception that exits the context (if any) should be suppressed (not propagate to outside the context).

See this example of the meaning of the return value of __exit__:

>>> class MyContextManager:
...  def __init__(self, suppress):
...   self.suppress = suppress
...  
...  def __enter__(self):
...   return self
...  
...  def __exit__(self, exc_type, exc_obj, exc_tb):
...   return self.suppress
... 
>>> with MyContextManager(True):  # suppress exception
...  raise ValueError
... 
>>> with MyContextManager(False):  # let exception pass through
...  raise ValueError
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError
>>>

In the above example, both ValueErrors will cause the control to jump out of the context. In the first block, the __exit__ method of the context manager returns True, so Python suppresses this exception and it's not reflexed in the REPL. In the second block, the context manager returns False, so Python let the outer code handle the exception, which gets printed out by the REPL.


The workaround is to store the result in an attribute instead of returning it, and access it later. That is if you intend to use that value in more than a print.

For example, take this simple context manager:

class time_this_scope():
    """Context manager to measure how much time was spent in the target scope."""

    def __init__(self, allow_print=False):
        self.t0 = None
        self.dt = None
        self.allow_print = allow_print

    def __enter__(self):
        self.t0 = time.perf_counter()

    def __exit__(self, type=None, value=None, traceback=None):
        self.dt = (time.perf_counter() - self.t0) # Store the desired value.
        if self.allow_print is True:
            print(f"Scope took {self.dt*1000: 0.1f} milliseconds.")

It could be used this way:

with time_this_scope(allow_print=True):
    time.sleep(0.100)

>>> Scope took 100 milliseconds.

or like so:

timer = time_this_scope()
with timer:
    time.sleep(0.100)
dt = timer.dt 

Not like what is shown below since the timer object is not accessible anymore as the scope ends. We need to modify the class as described here and add return self value to __enter__. Before the modification, you would get an error:

with time_this_scope() as timer:
    time.sleep(0.100)
dt = timer.dt 

>>> AttributeError: 'NoneType' object has no attribute 'dt'

Finally, here is a simple use example:

"""Calculate the average time spent sleeping."""
import numpy as np
import time

N = 100
dt_mean = 0
for n in range(N)
    timer = time_this_scope()
    with timer:
        time.sleep(0.001 + np.random.rand()/1000) # 1-2 ms per loop.
    dt = timer.dt
    dt_mean += dt/N
    print(f"Loop {n+1}/{N} took {dt}s.")
print(f"All loops took {dt_mean}s on average.)