Burak's Blog

Blog about various subjects


Pythonic

Table of Contents

  1. Pythonic
  2. Context Managers
  3. Slicing
  4. Conclusion

Pythonic

Writing code isn’t hard but writing code that is clean, concise, and maintainable that follows the convention of the Python community is harder. They are a lot of different ways to implement the same function, class, or feature. Some of them are clean, sometimes confusing, or really bad writing that is hard to understand what it does exactly but in the end, the goal is to have a code that follows and naturally flows more in Python conventions and good practices. That is the kind of code we want to write or wish often when we work and are satisfied at the end of the day with our work. That’s what I’m going to try to help to write pythonic code and some python mechanics that can be very helpful.

Context Managers

In other languages like Java or Ruby, it’s common to use def/ensure or try/finally to acquire a lock on a resource and free it later if exceptions occur. Python has the try/except but there’s a better way to do it. As an example, I’m going to use a virus scanner pyclamd to scan a file.

# bad
try:
    virus_scanner = pyclamd.ClamdUnixSocket()
except pyclamd.pyclamd.ConnectionError as msg:
    logger.error(f"Error while trying to scan the file")

result = virus_scanner.scan_file(path)

class VirusScan:
    def __enter__(self):
        self.clamd = clamd.ClamdUnixSocket()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type == clamd.ConnectionError:
            print("No virus scanner found")
        else:
            self.clamd._close_socket()

    def scan_files(self, path):
        return self.clamd.scan(path)

with VirusScan() as virus_sanner:
    results = virus_scanner.scan_files("/tmp")
    for file in results:
        print(file)

Code above shows the usage of what is called in Python With Statement Context Managers that defines the entry and the exit from. By encapsulating the virus scanner in class and defining the __enter_~_ and ~__exit__ methods, the object can be used with the statement that will open and close a connection to the virus scanner without adding extra code but also avoid using unnecessarily a try/except statement.

Slicing

In the previous code, the virus scanner return a dictionary with the file name as key and tuple as value with other information about the scanned file. Getting the data isn’t clean, simple, and can be confusing for developers if they aren’t familiar with the pyclamd tuple structure. There’s a simple way to transform the return data structure to be more pythonic and easier to manipulate.

class VirusScan:
    def __enter__(self):
        self.clamd = clamd.ClamdUnixSocket()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type == clamd.ConnectionError:
            print("No virus scanner found")
        else:
            self.clamd._close_socket()

    def scan_files(self, path):
        result = self.clamd.scan(path)
        return VirusScanFiles(result)


class VirusScanFiles:
    def __init__(self, result):
        results = [
            VirusScanFileStatus(filename=k, reason=v[1], status=v[0])
            for k, v in result.items()
            ]
        self.results = results

    def __len__(self):
        return len(self.results)

    def __getitem__(self, index):
        if index >= len(self):
            raise IndexError
        return self.results[index]


@dataclass
class VirusScanFileStatus:
    filename: str
    reason: str
    status: str

if __name__ == "__main__":
    with VirusScan() as virus_scanner:
        for result in virus_scanner.scan_files("/tmp"):
            print(result)

Code above shows the VirusScan object returns an instance of VirusScanFiles that contain the files dictionary and transform it into a list of object VirusScanFileStatus with the file data. By defining __len__ and __getitem methods, it becomes possible to loop through the instance without requiring the results. Also, the VirusScanFiles encapsulate the file data that is much easier to access and readable than returning a tuple. Not only this code respect follows the good practices and be more pythonic but it’s now much much easier to write unit tests.

Conclusion

I recommend using those tips in your python code and of course, there’s much more different way to make your code more Pythonic. However, making a code more pythonic doesn’t mean it fits for you what’s works for me. Use the python idioms to make your code readable , concise and not just because it’s Pythonic it’s necessarily good, the end goal is to use python features to improve your code.