updated 30 Dec, 2021

Python context manager: try / finally in better way

Limited system resources: don't forget to free them.

When we work with the system resources, we should release them in the end, because the number of currently opened files or network connections is limited by OS or by the process that provides them f.e. database connections. Let's close the file after reading its contents:

f = open("notes.txt")
lines = f.readlines()
print(lines)
f.close()
Even better to do this in safer way, to guarantee the closing in case of the errors before the close() is called:
f = open("notes.txt")
try:
  lines = f.readlines()
  print(lines)
finally:
  f.close()
There are lot of code there. Let's do this in shorter way:
with open("notes.txt") as f:
  lines = f.readlines()
  print(lines)
What happens above:
  1. Open the file and create the variable, the same as f = open("notes.txt")
  2. The code inside the try block is moved to the with block
  3. finally block with close() call is disappeared: the with block closes the file instead after the code inside is executed or exception is raised. The same way as try / finally works

The syntax of with Python block:

with context_expression [as context_var]:
  code_block

  1. context_expression is evaluated to an object that the implements the context manager protocol: the class that contains the __enter__(), __exit__() functions
  2. __enter__() is called and the returned object assigned to the context_var local variable accessible inside with block.
    Usually self is returned but it could return None as well.
  3. If there is exception on the context_expression evaluation or __enter__() call, the code inside and __exit__() are NOT called
  4. If __enter__() is called successfully, then the code inside the with block is called and __exit__() always called, even if an exception is raised
  5. If an exception is raised then it's passed into the __exit__() call as argument and it could be supressed with True is returned, or propagated if False is returned
In short, the context manager for a file is something like:
class FileContextManager(object):

    def __init__(self, file_name, method):
        self.file = open(file_name, method)  # get resource: open file
        
    def __enter__(self):
        return self.file  # return file object

    def __exit__(self, type, value, traceback):
        self.file.close()  # release resource: close file

When is it useful?

Working with files, network and database connections i.e. any limited system resource that is acquired then released after.

Can I implement own context manager without a class?

Yes, it could be done with generator function like this:
from contextlib import contextmanager

@contextmanager
def my_open_file(name):
    f = open(name, 'w')
    try:
        yield f  # run the code inside with block
    finally:
        f.close()
        
with my_open_file("some.txt") as f:
    f.readlines()
The library GeneratorContextManager class is used there internally.

Or use closing() decorator, if the resource provides close() method for release:

from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as lines:
    for line in lines:
        print(line)