You Can Use Parentheses with Multiple Context Managers

I didn’t know this until today, and I found it pretty nice.

Usually, when you chain multiple with statements in Python, you either cram them onto one line:

with open("in.txt") as f, open("out.txt", "w") as g:
    g.write(f.read())

or you end up indenting like a staircase:

with open("in.txt") as f:
    with open("out.txt", "w") as g:
        g.write(f.read())

or you end up indenting with \:

with open("in.txt") as f, \
	open("out.txt", "w") as g:
    g.write(f.read())

All work fine, but the first one can get unreadable if you’re juggling more than two or three resources, the second one leaves you with a very long indentation, and the third one… is fine, actually.

The “Cleaner Way” (Python 3.10+)

Starting with Python 3.10, you can now do this:

with (
    open("in.txt") as f,
    open("out.txt", "w") as g,
    acquire_lock() as lock,
):
    process(f, g, lock)

That’s perfectly valid syntax. Sweet! The parentheses just group the context managers without forcing everything into a single long line. No need for backslashes, no deep indentation.

More context (pun intended)

The contexts are entered left-to-right and exited right-to-left.
That means in this example:

  1. open("in.txt") runs first. Its __enter__() is called.
  2. Then open("out.txt").
  3. Then acquire_lock().

When the block exits, whether normally or because of an exception:

  • acquire_lock().__exit__() runs first,
  • then out.txt’s context closes,
  • then finally the input file closes.

It’s strict, predictable and the cleanup happens in reverse order, exactly how you’d (I’d) expect.

How I Find it Useful

I can easily manage multiple things cleanly (“cleanly” being subjective). Examples that actually make sense:

1. Database connections + cursors

with (
    psycopg.connect(dsn) as conn,
    conn.cursor() as cur,
):
    cur.execute("SELECT * FROM users")

2. File I/O + locks

with (
    file_lock("data.json"),
    open("data.json") as f,
):
    data = json.load(f)

3. Temporary directories + logs

with (
    tempfile.TemporaryDirectory() as tmp,
    open("log.txt", "a") as log,
):
    log.write(f"Working in {tmp}\n")

4. Writing tests

# Testing that the current user can get all users from the db.
# The overrides are part of FastAPI's dependency injection
with (
    override_get_db(),
    override_get_current_user(),
):
    res = client.get("/users")
	assert res.status_code == 200

No more nested with statements or the backslashes. No extra indentation just to manage cleanup.

Note

This only works in Python 3.10 and up. Try it in 3.9 or older, and you’ll get a SyntaxError.