Clean way to open/close an optional file in python?

Question:

My code:

fh = None
if args.optional_input_file_path is not None:
    fh = open(args.optional_input_file_path)

my_function(foo, bar, fh)

if args.optional_input_file_path is not None:
    fh.close()

I don’t like how I need to write if args.optional_input_file is not None: twice.

Moving the conditional logic inside my_function also isn’t ideal because I’m intentionally passing in IO objects to the function instead of file paths to make it easier to test.

Is there a cleaner way to achieve the same thing?

I want to be able to write my_function(foo, bar, fh) exactly once, and have fh be either an IO object or None, depending on the value of args.optional_input_file_path.

Asked By: Tan Wang

||

Answers:

Try this

my_function(foo, bar, open(args.file_path) if args.file_path else None)

As there is no reference to file open, it will auto close in the next GC

Answered By: itzMEonTV

Use a custom context manager that propagates None but otherwise behaves like open:

import contextlib


@contextlib.contextmanager
def open_not_none(file, *args, **kwargs):
    if file is not None:
        with open(file, *args, **kwargs) as f:
            yield f
    else:
        yield None

which can be used as

with open_not_none(args.optional_input_file_path, "r") as fh:
    my_function(foo, bar, fh)

This lets you keep all the benefits of opening the file in a with statement while avoiding duplicating the call to my_function for the None and not-None cases. You may also richly type hint it with typing.overload if you so choose.


edit: after seeing Frank Yellin’s answer, I realized that this could be implemented more simply using contextlib.nullcontext as

def open_not_none(file, *args, **kwargs):
    if file is None:
        return contextlib.nullcontext()
    return open(file, *args, **kwargs)

The usage is exactly the same.

The original approach works by creating and activating a new a context manager that either wraps the open-context-manager or does nothing. In this second approach, we get rid of that extra layer of context-manager indirection, and substitute with either returning the open-context-manager directly or returning a context manager that does nothing.

Answered By: Brian61354270

The documentation for contextlib suggests the following for handling optional contexts:

def process_file(filename):
    if filename is not None:
        cm = open(filename)
    else:
        cm = nullcontext()

    with cm as file:  # file will be None if no filename
        my_function(...., file)
Answered By: Frank Yellin
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.