Is the order of multiple __exit__ calls in a with-block always deterministic?

Viewed 109

Given the following ContextManager classes, here is the order of __exit__ calls in the following example - reverse order of declaration.

Interestingly, __exit__ can be called multiple times. Is this deterministic in all versions of Python from 2.7? If not, from which version is this order reliably deterministic?

a = A()
b = B()
c = C()

with a, b, c, a, a:
    print("Start")

Test harness

from typing import ContextManager

class A(ContextManager):
    def __exit__(self, __exc_type, __exc_value, __traceback):
        print("A exit")

class B(ContextManager):
    def __exit__(self, __exc_type, __exc_value, __traceback):
        print("B exit")

class C(ContextManager):
    def __exit__(self, __exc_type, __exc_value, __traceback):
        print("C exit")

a = A()
b = B()
c = C()

with a, b, c, a, a:
    print("Start")

Python 3.7+ output:

Start
A exit
A exit
C exit
B exit
A exit
2 Answers

Multiple context managers on the same with line are handled as if they are nested. So this:

with a, b, c:
    stuff()

is equivalent to this:

with a:
    with b:
        with c:
            stuff()

That is, the __enter__ methods are called left to right, and when the block ends, the __exit__ methods are called right to left.

Is this deterministic in all versions of Python from 2.7?

Yes, the order is well defined since 2.7 at least:

with A() as a, B() as b:
   ... suite of statements ...

is equivalent to:

with A() as a:
   with B() as b:
       ... suite of statements ...

The order you got (reading from the bottom)

Start
A exit
A exit
C exit
B exit
A exit

is indeed A, B, C, A, A, as expected.

Related