What is the correct way to close a file opened inside object initialization?

Viewed 77

I would like to have a class that gets passed either a string or an already opened file during initialization. If it gets a string, it opens the file.

from typing import IO

class Parser:
  def __init__(self, fin: str|IO[str]) -> None:
    if isinstance(fin, str):
      self.fin = open(fin, 'r')
    else:
      if not fin.readable():
        raise ValueError("Input file must be readable.")
      else:
        self.fin = fin

My question is, what is the correct way to close the file if it gets opened. I imagined it could be closed in the __del__ method, but after reading up on it, it seems to be the consensus that using __del__ is not a great idea. Is there a better way to do this?

2 Answers

Provide a separate class method to deal with opening and closing the file using a context manager.

from contextlib import contextmanager


class Parser:
    def __init__(self, fh: IO[str]) -> None:
        if not fh.readable():
            raise ValueError(...)
        self.fh = fh

    # Typing-hinting is out of scope for this question, so I'm
    # just using the Python-3.11-style Self hint for simplicity.
    @classmethod
    @contextmanager
    def from_filename(cls: Self, name: str) -> Self:
        with open(name) as fh:
            yield Parser(fh)

Now you can use either

with open("some_file.txt") as fh:
    p = Parser(fh)
    ...

or

with Parser.from_filename("some_file.txt") as p:
    ...

Indeed __del__ should NOT be used for this purpose, since it is highly unpredictable when it is called.

Also, best is to avoid side-effects (such as opening files) in __init__, so I really like the solution by chepner. When possible, that's the method I would choose.

Just to add an alternative: you could also implement a close method and then use contextlib.closing like this:

import contextlib

class Parser:

    # your __init__ here

    def close(self):
        self.fin.close()


parser = Parser('test.txt')
with contextlib.closing(parser):
    # do something with the object here
    # the file will be automatically closed afterwards

Even better, you could make the class a context manager itself like below, which is a bit similar to the answer of chepner. (You could even combine a number of these methods, depending on your needs).

class Parser:

    # your __init__ here

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.fin.close()


with Parser('test.txt') as parser:
    # do some stuff here...
Related