skip to content

How does `with A() as a, B() as b:` differ from two nested `with` statements?

level: middleimportance: should knowfreq 46%

answer

  1. Sugar, not a new construct
  2. Rewrite it as nesting and re-derive
  3. Left to right in, right to left out
  4. Later managers may use earlier bindings
  5. Parentheses across lines are 3.10 material

basics

~20 s

It does not differ semantically — the comma form is defined as exactly equivalent to nesting. Managers are entered left to right and exited in reverse, and each one's __exit__ still runs if a later manager fails.

solid answer

~40 s

A comma-separated `with` is pure sugar: `with A() as a, B() as b:` is defined to mean `with A() as a:` wrapping `with B() as b:`. So `A.__enter__` runs first, `B.__enter__` second, and on the way out `B.__exit__` runs before `A.__exit__` — reverse order, like unwinding a stack. Because it is genuine nesting, `b` can be built from `a`, and if `B()`'s entry raises then `A.__exit__` still runs while `B.__exit__` never does. The one real difference is syntactic: the comma form is one long line, and wrapping it in parentheses across several lines is officially supported from **Python 3.10** (the PEG parser landed in 3.9 and accepted it unofficially). When the number of managers is only known at runtime, neither form fits and you need a dynamic stack helper from `contextlib`.

code

python · 17 lines
python
class Stage:
    def __init__(self, n):
        self.n = n

    def __enter__(self):
        print("enter", self.n)
        return self

    def __exit__(self, exc_type, exc_value, tb):
        print("exit", self.n)


with (
    Stage("align") as a,
    Stage("annotate") as b,
):
    print("body", a.n, b.n)

go deeper

for a junior

Know that you can list several managers separated by commas in one with, and that it means the same as nesting them. Reading the comma form without confusion is enough at this level.

for a middle

State the equivalence explicitly and derive the ordering from it: left-to-right entry, reverse exit, and everything already entered gets torn down if a later entry raises.

for a senior

Show taste about when to use it. Flat chains for peer resources, real nesting when logic sits between acquisitions, and a dynamic stack helper when the count is not known until runtime.

for a principal

Own the codebase convention: a target-version floor that permits the parenthesized form, and a rule that long chains get folded into one composite manager so acquisition order and partial-failure behaviour live in one tested place.

### The equivalence, stated exactly The language reference defines the multi-manager form recursively. This: ```python with A() as a, B() as b: body ``` is specified to be identical to this: ```python with A() as a: with B() as b: body ``` Not "similar to" — identical. Every consequence of nesting is therefore a consequence of the comma form, and an interviewer asking this question is usually probing whether you know that, or whether you imagine the managers are somehow acquired together. ### Ordering, and why it matters Entry is **left to right**; exit is **right to left**. That is exactly the nesting order, and it is the correct order for resources that depend on each other: if the second manager was built from something the first produced, tearing the second down first is the only safe sequence. Because each `as` binding happens before the next manager's expression is evaluated, later managers may use earlier ones: ```python with open_index("contigs") as idx, batch_writer(idx) as out: ... ``` That one property already rules out any "acquire all at once" reading of the syntax, and it is the reason the equivalence has to be nesting rather than a parallel construct. ### Failure part-way through entry Suppose `A.__enter__` succeeds and then evaluating `B()` — or calling `B.__enter__` — raises. Then: * `A.__exit__` **is** called, with the exception triple, because control is already inside `A`'s block. * `B.__exit__` is **not** called, because `B` was never successfully entered. That asymmetry is worth internalising: a manager only owes you teardown once its `__enter__` has returned. It is also the reason `__enter__` should do its risky work last, or be written so that a failure part-way through leaves nothing to clean up — nobody will call `__exit__` to tidy after it. ### The parenthesized form A long comma chain used to be painful to wrap. Before Python 3.10 the only ways to break the line were a backslash continuation or nesting the statements. **Python 3.10 officially supports parenthesized context managers**: ```python with ( stage_lock("align") as lock, checkpoint("annotate") as cp, ): ... ``` including a trailing comma, which makes diffs clean when a manager is added. The new PEG parser introduced in 3.9 (PEP 617) already accepted the form, but it was neither documented nor guaranteed there; treat 3.10 as the floor if the code must run on older interpreters. Note the parentheses are part of the statement grammar here — they are not a tuple, and the form is not an expression you can assign. ### When neither form fits Both shapes hard-code the number of managers at compile time. A pipeline stage that opens *n* outputs, where *n* comes from configuration, cannot be written as a comma chain and should not be written as recursion over nested `with` statements. That case is what `contextlib.ExitStack` exists for — enter managers in a loop, and let the stack unwind them in reverse. ### Style: comma chain or nesting? The semantics being identical, the choice is readability: * **Comma chain (parenthesized)** when the managers are peers acquired for the same block — a lock and a timer, two files, a transaction and a temporary directory. It keeps indentation flat. * **Nesting** when there is real logic between the acquisitions, when only the inner block needs the second resource, or when the second manager is conditional. `with a_thing():` followed by a computation and *then* a second `with` is clearer than pretending the two are simultaneous. A long comma chain — four or five managers — is usually a smell either way: it often means one composite manager should exist that encapsulates the set, which is both easier to test and easier to reuse. ### Common misreadings to avoid 1. **"They are entered at the same time."** No — strictly sequential, left to right. 2. **"Exit order matches enter order."** No — it reverses. 3. **"If the second fails, nothing is cleaned up."** No — everything already entered is unwound. 4. **"The parentheses make a tuple of managers."** No — it is grammar, not an expression; you cannot pass that parenthesized list around as a value. All four fall out of one sentence: the comma form *is* nesting. If you remember only that, you can re-derive every answer above on the spot.

  • Can the second manager in a comma chain be built from the first one's `as` binding?
    Yes, and that is the clearest proof the form is nesting. Each manager expression is evaluated and entered, and its `as` target bound, before the next expression is evaluated at all — so `with open_index() as idx, writer(idx) as out:` is legal and well-defined. Any reading where the managers are acquired simultaneously would make that impossible.
  • How would you write this when the number of managers is decided at runtime?
    Neither the comma chain nor hand-nesting can express a count known only at runtime, since both fix it in the source. The answer is a dynamic stack from `contextlib` — enter each manager in a loop, register it with the stack, and let a single block exit unwind them all in reverse order, including on the failure path.
  • Why keep a comma chain to two or three managers?
    Readability, mostly: a five-manager chain hides which resources depend on which, and a failure part-way through entry becomes hard to reason about at a glance. It is usually a signal that a single composite manager should own the set — one `__enter__` that acquires them in order, one `__exit__` that releases them in reverse — which is easier to test and to reuse.

saying these in an interview costs you the question

  • Says the managers are entered simultaneously
  • Thinks `__exit__` order matches `__enter__` order
  • Believes nothing is cleaned up if a later entry fails
  • Reads the parentheses as building a tuple
  • Claims the comma form has different semantics from nesting
  • Dates the parenthesized syntax to Python 2 or 3.6

context