1

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.

Brian61354270
  • 8,690
  • 4
  • 21
  • 43
Tan Wang
  • 811
  • 1
  • 6
  • 16

3 Answers3

2

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.

Brian61354270
  • 8,690
  • 4
  • 21
  • 43
1

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

itzMEonTV
  • 19,851
  • 4
  • 39
  • 49
  • I don't think this is any cleaner because there's also an `else: my_function(foo, bar, None)` needed. 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` – Tan Wang Mar 25 '23 at 22:47
  • 2
    I'm not sure if relying on GC like that is the best but this certainly seems like the simplest approach -- thank you! – Tan Wang Mar 25 '23 at 23:03
1

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)
Frank Yellin
  • 9,127
  • 1
  • 12
  • 22