Is it possible to have an optional with/as statement in python?
Asked Answered
O

8

29

Instead of this:

file = open(f)
do_something(file)
file.close()

it's better to use this:

with open(f) as file:
    do_something(file)

What if I have something like this?

if f is not None:
    file = open(f)
else:
    file = None
do_something(file)
if file is not None:
    file.close()

Where do_something also has an if file is None clause, and still does something useful in that case - I don't want to just skip do_something if file is None.

Is there a sensible way of converting this to with/as form? Or am I just trying to solve the optional file problem in a wrong way?

Ophthalmitis answered 28/8, 2012 at 22:7 Comment(1)
Does this answer your question? Conditional with statement in PythonScrutator
G
26

For Python 3.7 or higher, then you can use the nullcontext for stand-in purposes:

from contextlib import contextmanager

none_context = contextmanager(lambda: iter([None]))()
# <contextlib.GeneratorContextManager at 0x1021a0110>

with (open(f) if f is not None else none_context) as FILE:
    do_something(FILE)

For older Python versions, you can build one yourself on-the-fly with an optional None:

from contextlib import contextmanager

none_context = contextmanager(lambda: iter([None]))()
# <contextlib.GeneratorContextManager at 0x1021a0110>

with (open(f) if f is not None else none_context) as FILE:
    do_something(FILE)

It creates a context that returns None. The with will either produce FILE as a file object, or None. But the None type will have a proper __exit__


Another solution would be to just write it like this:

if f is not None:
    with open(f) as FILE:
        do_something(FILE)
else:
    do_something(f)

(file is a builtin btw )

Gabar answered 28/8, 2012 at 22:11 Comment(7)
@sberry - I saw that we almost had the same thing, really close to eachother :-)Gabar
I was hoping to avoid repeating do_something, since it's a bigger chunk of code in the real case (though I can stick it in a function, etc). But maybe this is the best I can do.Ophthalmitis
I just added a fun little example of how to do what you want.Gabar
Note that none_context can only be used once. I prefer to make it a callable: def none_context(a=None): return contextmanager(iter)([a])Durance
Also, it fails if an exception is raised (AttributeError: 'listiterator' object has no attribute 'throw'). This is more robust: def none_context(a=None): return contextmanager(lambda: (x for x in [a]))()Durance
None of these solutions, including those in these comments, seem to work for me in all situations with python 2.7.12 - they either raise AttributeError: __exit__ immediately, or AttributeError: 'listiterator' object has no attribute 'throw' when an exception occurs and none_context is the selected context.Kale
contextlib.ExitStack() is a better solution...Inbred
A
20

Since Python 3.7, you can also do

from contextlib import nullcontext

with (open(f) if f else nullcontext()) as file:
    # Do something with `file`
    pass

See the official documentation for more details.

Ambulator answered 10/7, 2019 at 12:57 Comment(0)
N
3

This seems to solve all of your concerns.

if file_name is not None:
    with open(file_name) as fh:
        do_something(fh)
else:
    do_something(None)
Nitrate answered 28/8, 2012 at 22:12 Comment(1)
I was hoping to avoid repeating do_something, since it's a bigger chunk of code in the real case (though I can stick it in a function, etc). But maybe this is the best I can do.Ophthalmitis
D
3

In Python 3.3 and above, you can use contextlib.ExitStack to handle this scenario nicely

with contextlib.ExitStack() as stack:
    file = stack.enter_context(open(f)) if f else None
    do_something(file)
Dustheap answered 6/10, 2021 at 12:28 Comment(0)
B
1

something like:

if file:  # it checks for None, false values no need for "if file is None"
    with open(file) as FILE:
        do_something(FILE)
else:
    FILE=None
Butch answered 28/8, 2012 at 22:13 Comment(1)
Sorry, I didn't make myself very clear. Hopefully the edited version of the question makes more sense. I actually want do_something to run even if FILE is None - it has other useful functionality, the file-related section of it is optional.Ophthalmitis
F
1

Python 3.7 supports contextlib.nullcontext, which can be used to avoid creating your own dummy context manager.

This examples shows how you can conditionally open a file or use the stdout:

import contextlib
import sys

def write_to_file_or_stdout(filepath=None, data):
    with (
            open(filepath, 'w') if filepath is not None else
            contextlib.nullcontext(sys.stdout)
    ) as file_handle:
        file_handle.write(data)

contextlib.nullcontext() can be called without any arguments if the value can be None.

Forbore answered 14/10, 2021 at 0:40 Comment(0)
C
0

For the people like me who came here for the title: Is it possible to have an optional with/as statement in python? wanting to use an optional with clause returning another object.

visit the docs to learn more, but it looks something like this:

obj = 'anything'
with (return_normal_object(obj) if True else open(path)) as file:
   # use file
   assert file == 'anything'
from contextlib import contextmanager

@contextmanager
def return_normal_object(obj):
    yield obj
Colo answered 25/10, 2023 at 20:44 Comment(0)
D
-6

While all of the other answers are excellent, and preferable, note that the with expression may be any expression, so you can do:

with (open(file) if file is not None else None) as FILE:
    pass

Note that if the else clause were evaluated, to yield None this would result in an exception, because NoneType does not support the appropriate operations to be used as a context manager.

Delossantos answered 28/8, 2012 at 22:17 Comment(7)
Wait...are you sure this works? None type would crash because it does not have an AttributeError: __exit__Gabar
@Gabar - damn, you're right, it does crash... Back to the drawing board. :(Ophthalmitis
@jdi: Fair point. I want to emphasise the syntax and possibilities here, though. I'll edit to make clear that this would fail on execution.Delossantos
@Marcin: I ended up coming up with an on-the-fly NoneType context, that works the way you were intending with this, in my answer.Gabar
Not even __exit__, on Python 3.6 this fails with AttributeError: __enter__.Paterfamilias
@jimbo1qaz Read the whole answer, please.Delossantos
@Delossantos I fail to the see the value this answer brings. Can you elaborate?Lat

© 2022 - 2024 — McMap. All rights reserved.