598

Is it possible to declare more than one variable using a with statement in Python?

Something like:

from __future__ import with_statement

with open("out.txt","wt"), open("in.txt") as file_out, file_in:
    for line in file_in:
        file_out.write(line)

... or is cleaning up two resources at the same time the problem?

martineau
  • 119,623
  • 25
  • 170
  • 301
pufferfish
  • 16,651
  • 15
  • 56
  • 65
  • Maybe like this: with [expr1,expr2] as f: and then use f[0] and f[1]. – jbasko May 21 '09 at 14:55
  • Would have been nice because no need to import something.... but it doesn't work AttributeError: 'list' object has no attribute '__exit__' – pufferfish May 21 '09 at 15:05
  • If python just had closures, you wouldn't need the with statement – B T Feb 08 '12 at 04:38
  • You don't *need* to use a with statement, right? You can just set file_out and file_in to None, then do a try/except/finally where you open them and process them in the try, and then in the finally close them if they are not None. No double-indentation needed for that. – M Katz Dec 12 '13 at 00:27
  • 2
    Many of these answers don't deal with the need for more than two with statements. Theoretically there may be applications that need to open tens of contexts, the nesting falls apart very quickly is any line length limitations are imposed. – ThorSummoner Aug 21 '14 at 00:17
  • is it possible set fields equal to something in with statement as in `with open('./file') as arg.x = file:`? – Charlie Parker Feb 03 '17 at 05:46
  • @ThorSummoner see the answer describing `contextlib.ExitStack`. – timgeb May 10 '18 at 09:45

8 Answers8

951

It is possible in Python 3 since v3.1 and Python 2.7. The new with syntax supports multiple context managers:

with A() as a, B() as b, C() as c:
    doSomething(a,b,c)

Unlike the contextlib.nested, this guarantees that a and b will have their __exit__()'s called even if C() or it's __enter__() method raises an exception.

You can also use earlier variables in later definitions (h/t Ahmad below):

with A() as a, B(a) as b, C(a, b) as c:
    doSomething(a, c)

As of Python 3.10, you can use parentheses:

with (
    A() as a, 
    B(a) as b, 
    C(a, b) as c,
):
    doSomething(a, c)
mlissner
  • 17,359
  • 18
  • 106
  • 169
Rafał Dowgird
  • 43,216
  • 11
  • 77
  • 90
  • 3
    is it possible set fields equal to something in with statement as in `with open('./file') as arg.x = file:`? – Charlie Parker Feb 03 '17 at 05:46
  • 29
    Also, it is possible: with A() as a, B(a) as b, C(a,b) as c: – Ahmad Yoosofan Aug 12 '17 at 08:24
  • class test2: x=1; t2=test2() with open('f2.txt') as t2.x: for l1 in t2.x.readlines(): print(l1); # Charlie Parker # tested in python 3.6 – Ahmad Yoosofan Aug 12 '17 at 08:33
  • 10
    please note, `as` is optional. – Sławomir Lenart Jul 09 '18 at 15:19
  • 10
    to clarify what @SławomirLenart is saying: `as` is required if you need the object `a` or `b`, but the whole `as a` or `as b` is not required – Ciprian Tomoiagă May 19 '20 at 17:56
  • I was confused as to why [PEP 343](https://peps.python.org/pep-0343/) doesn't mention this syntax. It appears that when it was [added in 3.1](https://docs.python.org/3/whatsnew/3.1.html#other-language-changes), there was no PEP for it - someone just [contributed code](https://codereview.appspot.com/53094) and it was accepted. Per the links here, it seems that the same change was integrated into 2.7 at the same time. – Karl Knechtel Jan 14 '23 at 10:59
  • For the parenthesized context manager of Python 3.10, note that it [in fact](https://stackoverflow.com/questions/67808977/parenthesized-context-managers) is available already in (C)Python 3.9. – jmd_dk Mar 30 '23 at 20:25
87

Note that if you split the variables into lines, prior to Python 3.10 you must use backslashes to wrap the newlines.

with A() as a, \
     B() as b, \
     C() as c:
    doSomething(a,b,c)

Parentheses don't work, since Python creates a tuple instead.

with (A(),
      B(),
      C()):
    doSomething(a,b,c)

Since tuples lack a __enter__ attribute, you get an error (undescriptive and does not identify class type):

AttributeError: __enter__

If you try to use as within parentheses, Python catches the mistake at parse time:

with (A() as a,
      B() as b,
      C() as c):
    doSomething(a,b,c)
SyntaxError: invalid syntax

When will this be fixed?

This issue is tracked in https://bugs.python.org/issue12782.

Python announced in PEP 617 that they would replace the original parser with a new one. Because Python's original parser is LL(1), it cannot distinguish between "multiple context managers" with (A(), B()): and "tuple of values" with (A(), B())[0]:.

The new parser can properly parse multiple context managers surrounded by parentheses. The new parser has been enabled in 3.9. It was reported that this syntax will still be rejected until the old parser is removed in Python 3.10, and this syntax change was reported in the 3.10 release notes. But in my testing, it works in trinket.io's Python 3.9.6 as well.

nyanpasu64
  • 2,805
  • 2
  • 23
  • 31
65

contextlib.nested supports this:

import contextlib

with contextlib.nested(open("out.txt","wt"), open("in.txt")) as (file_out, file_in):

   ...

Update:
To quote the documentation, regarding contextlib.nested:

Deprecated since version 2.7: The with-statement now supports this functionality directly (without the confusing error prone quirks).

See Rafał Dowgird's answer for more information.

Community
  • 1
  • 1
Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • 45
    I am sorry to say that, but I think that the `nested` context manager is a mistake and should never be used. In this example, if opening the second file raises an exception, the first file won't be closed at all, thus totally destroying the purpose of using context managers. – Rafał Dowgird May 21 '09 at 15:04
  • Why do you say that? The documentation says that using nested is equivalent to nested 'with's – James Hopkin May 21 '09 at 16:12
  • @Rafal: a glance at the manual seems to indicate that python properly nests the with statements. The real problem is if the second file throws an exception upon closing. – Unknown May 21 '09 at 19:23
  • @Unknown: that's my reading too - and as far as I know, File object won't raise an exception on closing – James Hopkin May 22 '09 at 08:09
  • 12
    @James: No, the equivalent code in the docs at http://docs.python.org/library/contextlib.html#contextlib.nested differs from the standard nested `with` blocks. The managers are created in order *before* entering the with blocks: m1, m2, m3 = A(), B(), C() If B() or C() fails with exception, then your only hope of properly finalizing A() is the garbage collector. – Rafał Dowgird May 22 '09 at 08:58
  • @Rafal: Immediate closing on all non-exceptional exits is good enough in many cases. For example it's a neat way to catch `return` and `continue`. – Tobu Nov 26 '12 at 08:30
  • 10
    [Deprecated since version 2.7](http://docs.python.org/2/library/contextlib.html#contextlib.nested). Note: The with-statement now supports this functionality directly (without the confusing error prone quirks). – miku Jan 14 '13 at 20:43
30

Since Python 3.3, you can use the class ExitStack from the contextlib module.

It can manage a dynamic number of context-aware objects, which means that it will prove especially useful if you don't know how many files you are going to handle.

The canonical use-case that is mentioned in the documentation is managing a dynamic number of files.

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # All opened files will automatically be closed at the end of
    # the with statement, even if attempts to open files later
    # in the list raise an exception

Here is a generic example:

from contextlib import ExitStack

class X:
    num = 1

    def __init__(self):
        self.num = X.num
        X.num += 1

    def __repr__(self):
        cls = type(self)
        return '{cls.__name__}{self.num}'.format(cls=cls, self=self)

    def __enter__(self):
        print('enter {!r}'.format(self))
        return self.num

    def __exit__(self, exc_type, exc_value, traceback):
        print('exit {!r}'.format(self))
        return True

xs = [X() for _ in range(3)]

with ExitStack() as stack:
    print(stack._exit_callbacks)
    nums = [stack.enter_context(x) for x in xs]
    print(stack._exit_callbacks)
print(stack._exit_callbacks)
print(nums)

Output:

deque([])
enter X1
enter X2
enter X3
deque([<function ExitStack._push_cm_exit.<locals>._exit_wrapper at 0x7f5c95f86158>, <function ExitStack._push_cm_exit.<locals>._exit_wrapper at 0x7f5c95f861e0>, <function ExitStack._push_cm_exit.<locals>._exit_wrapper at 0x7f5c95f86268>])
exit X3
exit X2
exit X1
deque([])
[1, 2, 3]
timgeb
  • 76,762
  • 20
  • 123
  • 145
19

I think you want to do this instead:

from __future__ import with_statement

with open("out.txt","wt") as file_out:
    with open("in.txt") as file_in:
        for line in file_in:
            file_out.write(line)
Andrew Hare
  • 344,730
  • 71
  • 640
  • 635
  • 7
    That's how I currently do it, but then the nesting is twice as deep as I want (mean) it to be... – pufferfish May 21 '09 at 14:57
  • I think this is the cleanest approach though - any other approach is going to be harder to read. Alex Martelli's answer seems to be closer to what you want but is much less readable. Why is nesting such a concern? – Andrew Hare May 21 '09 at 14:59
  • 8
    Not a big deal, admittedly, but, per "import this" (aka "Zen of Python"), "flat is better than nested" -- that's why we added contextlib.nested to the standard library. BTW, 3.1 might have a new syntax "with A() as a, B() as b:" (the patch is in, no BDFL pronouncement about it so far though) for more direct support (so clearly the library solution isn't considered perfect... but avoiding unwanted nesting is definitely a widely shared goal among core Python developers). – Alex Martelli May 21 '09 at 15:09
  • 4
    @Alex: Very true but we must also consider that "Readability counts". – Andrew Hare May 21 '09 at 15:12
  • 6
    @Andrew: I think one level of indentation better expresses the intended logic of the program, which is to "atomically" create two variables, and clean them up later together (I realise this isn't actually what happens). Think the exception issue is a deal breaker though – pufferfish May 21 '09 at 16:02
6

From Python 3.10 there is a new feature of Parenthesized context managers, which permits syntax such as:

with (
    A() as a,
    B() as b
):
    do_something(a, b)
Chris_Rands
  • 38,994
  • 14
  • 83
  • 119
3

In Python 3.1+ you can specify multiple context expressions, and they will be processed as if multiple with statements were nested:

with A() as a, B() as b:
    suite

is equivalent to

with A() as a:
    with B() as b:
        suite

This also means that you can use the alias from the first expression in the second (useful when working with db connections/cursors):

with get_conn() as conn, conn.cursor() as cursor:
    cursor.execute(sql)
Eugene Yarmash
  • 142,882
  • 41
  • 325
  • 378
2

You can also separate creating a context manager (the __init__ method) and entering the context (the __enter__ method) to increase readability. So instead of writing this code:

with Company(name, id) as company, Person(name, age, gender) as person, Vehicle(brand) as vehicle:
    pass

you can write this code:

company = Company(name, id)
person = Person(name, age, gender)
vehicle = Vehicle(brand)

with company, person, vehicle:
    pass

Note that creating the context manager outside of the with statement makes an impression that the created object can also be further used outside of the statement. If this is not true for your context manager, the false impression may counterpart the readability attempt.

The documentation says:

Most context managers are written in a way that means they can only be used effectively in a with statement once. These single use context managers must be created afresh each time they’re used - attempting to use them a second time will trigger an exception or otherwise not work correctly.

This common limitation means that it is generally advisable to create context managers directly in the header of the with statement where they are used.

Jeyekomon
  • 2,878
  • 2
  • 27
  • 37
  • 2
    Note that many context managers are not meaningful after the context. Defining their name before the context implies the name is valid regardless of the context, i.e. also after it. While not a binding restriction (the behaviour is the same), this might be seen as a reason to avoid separate definitions in favour of readability and clarity. – MisterMiyagi Oct 21 '20 at 13:23
  • 2
    @MisterMiyagi Ah, great point, thank you. I've read about it more and updated the answer. I sometimes use my context managers after the context: `d = Document('foo'); with d: d.write('bar'); print('OK', d.path)` and I wasn't aware of this detail. – Jeyekomon Oct 22 '20 at 08:31