How to leave a `with` block without closing the resource?

Viewed 119

I am trying to achieve something similar to

from tempfile import TemporaryFile

def open_head(file_path):
   with open(file_path, 'r') as f,
        TemporaryFile() as tf:
       for i in range(0,10):
           tf.write(f.read_line())
       return tf

such that the caller gets ownership of the temporary file.

In particular, I don't want the with statement to close the TemporaryFile. But if anything goes wrong before the return, I still want the TemporaryFile be closed by the with statement.

Ideally, I would then want to write the caller as

with open_head(file_path):
    # more code here

Is this possible somehow? E.g. by writing return do_not_close(tf) or some other utility functionality?

Or am I approaching this completely wrong and there is a more Pythonic way to return a TemporaryFiles or other resources, between functions while guaranteeing exception safety?

3 Answers

You don't. open_head should take an already opened handle, which the caller is responsible for closing.

from tempfile import TemporaryFile
from itertools import islice


def head(file_path, fh):
    with open(file_path) as f:
        for line in islice(f, 10):
            fh.write(line)


with TemporaryFile() as tf:
    head(file_path, tf)
    # Do other stuff with tf before it gets closed.
    

In general, anytime you are opening a file in an function, ask yourself if you can push the actual open up to the caller and accept a file-like object instead. Aside from making your code more reusable, it makes your code easier to test. head doesn't have to be called with an actual file: it can be called with any file-like object, such as io.StringIO.


Put another way: the with statement enforces the advice

If you open the file, you are responsible for closing it as well.

The contrapositive of that advice is

If you aren't responsible for closing the file, you aren't responsible for opening the file, either.

Just move the TemporaryFile outside the context manager and wrap it in a try except block

from tempfile import TemporaryFile

def open_head(path: str):
    try:
        tf = TemporaryFile()
        with open(path, "r") as f:
            for _ in range(10):
                tf.write(f.readline())
            return tf
    except Exception as e:
        tf.close()
        raise e

I may be misunderstanding your question, but could you explicitly open the temporary file, return it from the function, and then close it when you want to?

def open_head(...):
   with open(file_path, 'r') as f:
      tf = TemporaryFile()
      for i in range(0,10):
         tf.write(f.read_line())
   return tf

tf = open_head(...)

# do some work

tf.close() # close the temporary file

Make sure you understand here though - by returning tf from the open_head function, you yourself are responsible for closing it properly.

The with keyword is used with context managers, which have __enter__ and __exit__ methods. The __exit__ method is called when you leave the with block, which closes the file descriptor.

If you want to handle exceptions, you could yield the temporary file descriptor

@contextlib.contextmanager
def open_head(...):
    tf = TemporaryFile()

    try:
        # do some work that might cause errors
        yield tf
    finally:
        os.close(tf)
Related