Making Resource Acquisition Easy: Python’s Simple Guide to RAII

Introduction

Resource Acquisition and Initialization means the following:

  1. When we create an object, we make sure it gets everything it needs.
  2. When we don’t need that thing anymore, we clean up and give back any resources it was using.

There are several ways to implement this pattern in Python:

  1. By using context managers, usually using the with statement
  2. Manually using the del statement
  3. Using the try-finally construct.

We will discuss all three in this post. Don’t worry, it’s not as complicated as it sounds.

For our example we will build a very simple window system in three slighly different windows.

Using context managers and the ‘with’ statement.

Let’s look at this short program:

import traceback
from typing import Self


class Window:
    _title: str = None

    def __init__(self, title):
        self._title = title

    @property
    def title(self) -> str:
        return self._title

    def open(self):
        print(f"Opening window with title {self._title}")

    def close(self):
        print(f"Closing window with title {self._title}")

    def __enter__(self) -> Self:
        print(f"Window with title {self._title} acquired")
        return self

    def __exit__(self, exc_type: type | None, exc_val: Exception | None, exc_tb: traceback):
        print(f"Window with title {self._title} released")


if __name__ == "__main__":
    with Window("My windows") as w:
        w.open()
        w.close()

A short summary:

  1. Our Window class only has one attribute, a title
  2. Besides the usual open() and close() methods, there are two new methods: enter(), which is called when you start a ‘with’ block, and exit(), when the block ends. This method is always called, no matter how the block exits, normally or with an exception.
  3. In the main program we use the with statement to make sure the resources are properly acquired and released.

Releasing resources manually using the ‘del’ statement

Let’s have a look at this code:

class Window:
    _title: str = None

    def __init__(self, title):
        self._title = title

    @property
    def title(self) -> str:
        return self._title

    def open(self):
        print(f"Opening window with title {self.title}")

    def close(self):
        print(f"Closing window with title {self.title}")

    def __del__(self):
        print(f"Deleting window with title {self.title}")


if __name__ == "__main__":
    w = Window("My window")
    w.open()
    w.close()
    del w

A short summary:

  1. We define a Window class with a constructor and open() and close() methods
  2. The __del()__ method is called when the del statement is called on the object.
  3. In the main program, we create a window, open and close it, and delete it.

Using the ‘try-finally’ method

This method is a little different but can be elegant in some situations:

class Window:
    _title: str = None

    def __init__(self, title):
        self._title = title

    @property
    def title(self) -> str:
        return self._title

    def open(self):
        print(f"Opening window with title {self.title}")

    def close(self):
        print(f"Closing window with title {self.title}")

    def release(self):
        print(f"Releasing window with title {self.title}")


if __name__ == "__main__":
    w = Window("My window")
    try:
        w.open()
        w.close()
    finally:
        w.release()

A short explanation:

  1. We define the same Window class, but now we add a release() method
  2. In the main program, we create a window
  3. In the try-finally block we open and close the window. After that, even when an exception is raised, the finally block is executed and the window is released.

Conclusion

This pattern is not very hard to understand. It’s important to remember it to make sure you acquire and release resources properly.

As I have shown there are at least three ways to implement this pattern. Which pattern you use depends on your personal preference. My personal preference would be the with-method: it is the clearest, and the flow is the easiest to understand.

In a later pos I will have a look at this pattern in a multi-threaded environment, because the implementation as they stand are not thread-safe.

Leave a Reply

Your email address will not be published. Required fields are marked *