# Using context managers in Python

>If you ever used Python, most likely you used context managers without even knowing what they are and what they do or how they work - you just used them as per the documentation of some 3rd party package. In this article, I will try to explain what exactly are context managers and what are their benefits.

## 1. Context managers (by definition)

I hate to be a bookworm (I really do, trust me), but sometimes good old definitions come in handy. This is the case with **context managers**. So here it goes:

> Context managers are responsible for allocating and releasing resources when needed.

Is a question mark popping up in your head? **Expected**. I will walk you with a practical example :)

### 1.1 Practical example - no context manager

The idea is to open up a file with Python and read the file contents of that specific file. Let's take a look at the code below:

```python
file = open("textfile.txt", "r")
file.read()
file.close()
```

Simple eh? We open up a file called `textfile.txt` in `read` mode (`r`) and we read all of the contents of that file by making use of the `read()` method. Once we are done with injecting the content inside of the memory (or a variable if you want), we close the file.

Now - imagine that you forgot to close the file. Now imagine that you work with tons of files and somehow forget to close all of them. This can lead to **substantial resource leaks** and you want to avoid that.

Enter context managers :)

### 1.2 Practical example - context manager

The idea is once again to open up a file and read its contents. This time we will use context manager. The whole idea of a context manager is to do the following:

- Open up the resource (allocate it)
- Do something with the resource
- Close the resource once it is not needed anymore (release it)

In Python, we use the context managers by making use of the `with` keyword. Let's see what the code looks like now:

```python
with open("textfile.txt", "r") as file:
    file.read()
    # some other code while the resource (file) is allocated
```

**Similar, yet different**. Where did the `file.close()` go? Funny that you ask - we **don't need it** anymore.

>But... but... you said that we need to close the file to avoid resource leaks bla bla...

Yes, I did, and it is true. But what if I told you that context managers **do that for you**. In the above code, the resource (file) is opened as a variable called `file` in a read mode and is made available for usage (additional related code) inside of a block (indentation). Inside of that block, we read the file content. We can also do additional stuff with the file INSIDE of that block, BUT once we leave that block, the **context manager will release the resource** (in this case close the file) and we can no longer access it.

Handy eh? Let's see how it works under the hood.

## 2. Context manager under the hood

Each object (in Python everything is an object) that wants to use context manager, by using the `with` keyword, has to implement two built-in methods - `__enter__` and `__exit__`.

For the above use case, if we were to create a custom context manager class it would look something like this:

```python
from types import TracebackType
from typing import IO, Optional, Type


class CustomClassFile:
    def __init__(self, file_path: str, file_mode: str) -> None:
        self.file = open(file_path, file_mode)

    def __enter__(self) -> IO:
        return self.file

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> None
        self.file.close()


with CustomClassFile("textfile.txt", "r") as file:
    file.read()
```

So, once we create a new instance of `CustomClassFile`, we pass the two arguments - `file_path` and `file_mode`. That is fairly simple.

Inside of the `__enter__` method is where we need to define the resource that we want to allocate once the context manager kicks in. In this case, it will be the `self.file` object.

The `__exit__` method signature is a bit weird, but **I won't bother you with the details of it**. All of those arguments are required by default - so whenever you will create the `__exit__` method, you would use this exact same signature. This is due to the ability to create your own exception handlers (if they occur) upon releasing the resource. What is important is that we call the `self.file.close()` method inside of the `__exit__` method. This means that once we get past the `with` block, the file will be closed automatically by the context manager :)

Let's see some more examples.

### 2.1 Context manager as a class by example

The idea here would be to create the custom class that will allocate not the file but rather the contents of a `JSON` file and releases the file afterward.

```python
import json
from types import TracebackType
from typing import Any, IO, Optional, Type


class JSONFileLoader:
    def __init__(self, file_path: str) -> None:
        self.file: IO = open(file_path, "r")
        self.data: Any = json.load(self.file)

    def __enter__(self) -> Any:
        return self.data

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> None:
        if self.file:
            self.file.close()


with JSONFileLoader("file.json") as json_data:
    print(json_data)
```

This time when we create a new instance of the `JSONFileLoader` we only pass one argument - `file_path`. The `__init__` method then also loads the json contents of the file inside of the `self.data` property.

The `__enter__` method will this time allocate the data (as per the idea above) instead of the file itself. That means that once we enter the context manager instead of the whole file like before we will only have the file data available.

The `__exit__` method remains the same as before - we just close the file.

```python
with JSONFileLoader("file.json") as json_data:
    print(json_data)
```

See that now instead of the file, our `__enter__` method actually is returning the `self.data` property, which means that `json_data` is actually whatever is set inside of the `self.data` property. This makes it simple to just - for this example - print out the data. Once we leave the `with` block, `__exit__` will kick in and close the file for us.

### 2.2 Context manager as a generator by example

This is an alternative approach to creating context managers in Python. **If you don't know what generators are in Python, please take a moment to read about them**.

Let's see how to make use of Python's `contextlib` module to solve the same idea as the one above.

```python
import json
from contextlib import contextmanager
from typing import Any, Generator, IO


@contextmanager
def json_file_loader(file_path: str) -> Generator[Any, None, None]:
    file: IO = open(file_path, "r")
    data: Any = json.load(file)
    yield data
    file.close()


with json_file_loader("file.json") as json_data:
    print(json_data)
```

So the idea is that you will create a function and decorate it by using `@contextmanager` to mark it as a context manager for Python. We create a function that takes the `file_path` as an argument and it opens the file and loads it by using the `json.load()` method.
Once that is done, we `yield` the data. That means we will `pause` the execution of the code below from the function `json_file_loader` until we exit the context manager. This means that `file.close()` will execute ONLY after we exit the context manager as described below.

```python
with json_file_loader("file.json") as json_data:
    # inside of the context manager (yield kicked in)
    print(json_data)
# context manager exited which means that file.close() will execute
```

## 3. Benefits of using context managers

So what would be the perks of using context managers?

- Reduce resource leaks
- Reduce the number of lines of code
- More power over what to expose inside of the `with` block
- Multiple ways to implement
- No need to worry about releasing the resources

I hope you find those benefits handy and start to make use of context managers.

Like always, thanks for reading :)
