Clean Code in Python

Traits of clean code

Our code is the most detailed representation of the design.

  • Our ulitmate goal is to make the code as robust as possible
  • And, to write it in a way that minimizes defects or makes them utterly evident, should they occur.

Maintainability of code, reducting technical debt, working effectivley in agile development, and managing a successful project.

Code Structure Principles and Error Handling

Code Structure Principles

Dividing Code by Responsibilities

Some parts of software a meant to be called directly by users and some by other parts of the code. We divide the responsibilities of the applciation into different components or layers, and we have to think about the interaction between them.

We will have to encapsulate some functionality behind each components, and expose an interface to clients who are going to use that functionality, name an Application Programmable Interface (API). When designing an API we document expected input, output, and side-effects should be documented.

Each component is enforicing its own contraints and maintaining some invariants, and the program can be proven correct as long as these invariants are preserved.

Defensive Programming

The main idea here is how are we going to handle errors for scenarios that we might expect to occur, and how to deal with errors that should never occur.

Separation of concerns

This is relevant to low-level design and also higher level of abstraction. The goal of separating concerns in software is to enhance maintainability by minimizing ripple effects. A riple effect means the propogation of a change in the software from a starting point. This could be case of an error or exception triggering a chain of other exceptions, causing failures that will result in a defect on a remote part of the application.

Achieve High Cohesion and Low Coupling

  • Cohesion: objects should have small and well defined purpose, and they should do as little as possible. It should do only on e thing and do it well.
  • Coupling: How two or more objects depends on each other. If two parts of the code are too dependent on each other, they bring with them some some undesired consequences.

EAFP/LBYL

  • EAFP: Easier to Ask Forgiveness than Permission.
  • LBYL: Look Before You Leap

Error handling

The idea behind error handling is to gracefully respond to these expected errors in an attempt to either continue our program execution or decide to fail if the error turns out to be insurmountable.

There are different apporaches by which we can handle errors in our programs. Some of the approaches are

Value Substitution: Replacing erroneous result for a value that is to be considered non-disruptive (e.g. default value). Exception Handling: Stop the program from continuing to run with wrong data, or failing and notifying the caller that something is wrong and this is the case for a precondition that was violoated.

Tips for Error Handling

`Do not use exceptions as a *go-to** mechanism for business logic. Raise exceptions when there is actually something wrong with the code that caller need to be aware of**.

Principle 1

Exceptions should be used carefully because they weaken encapsulation. The more exceptions a function has, the more the caller function will have to anticipate, therefore knowning about the function it is calling. And if a function raises too many exceptions, this means that is not so context-free, because every time we want to invoke it, we will have to keep all of its possible side-effects in mind.

Principle 2

For security considerations, do not expose tracebacks to users.

Principle 3

Avoid empty except blocks.

try:
  process_data()
except: # do not do this
  pass

Instead,

  1. Catch a more specific exception. Pylint warns if broad Exceptions are used
  2. Do some actual error handling on the except block

Include the origin exceptions

As part of error logic, we might decide to raise a different one. In such a case include the original exception.

class SomeDataError(Exception):
  """Some data error exception as part of your domain"""
  
def process(dictionary, row_id):
  try:
    return dictionary[row_id]
  except KeyError as e:
    raise SomeDataError("Record read error") from e

Use assertions

Assertions are to be used for situations that should never happen or “should this condition happen, it means there is a defect in the software”.

result = 11
assert result == 10, "Excepting 10 items, instead got {0}".format(result)

Creating consistency in code

Documenting Code

Docstrings

Docstrings are not comments; they are documentation. If we are explaining why or how we are doing something, then code is probably not good enough. Some exceptions where we cannot avoid comments is when a third-party libarary has an error we are having to circument them. In those cases, placing a small but descriptive comment might be acceptable.

Good documentation provides crucial information for someone who has to learn and understand how a new function works, and how they can take advantage of it.

Practices

Indexes and Slices

Using a negative index number, will start counting from the last

>>> mylist = ["Apple", "Bannana", "Carrots", "Dragon Fruit"]
>>> mylist [-1]
'Dragon Fruit'
>>> mylist[-2]
'Carrots'

Example for obtaining many by using a slice

>>> mylist = ["Apple", "Bannana", "Carrots", "Dragon Fruit"]
>>> mylist [1:3]
['Bannana', 'Carrots']

Creating your own Sequence

A sequence is an object that implments the getitem and _len** menthods. This enables the iteration. Lists, tuples, and strings are examples of sequence objects in the standard libarary.

In case your class is wrapping standard python library object, delegate the behaviour as much as possible to the underlying object. This means that if your class is a wrapper on the list, call all the same methods on that list to make sure that it remains compatbile.

An example of encapsulation

class Items:

  def __init__(self, *values*):
    self._values = list(values)_
    
  def __len__(self):
    return len(self._values)
    
  def __getitem__(self, item):
    return self.__values.__getitem__(item)

When indexing by a range, the result should be an instance of the same type of the class

When to inherit?

///TODO

When to encapsule? ///TODO

Context Managers

Context Managers provide a pattern where we want to run some code, and has preconditions and postconditions, meaning that we want to run things before and after a certain main action.

For examples

fd=open(filename)
try:
  process_file(rd)
finally: 
  fd.close()

An elegant alternative is

with open(filename) as fd:
  process_file(fd)

The context managers consists of two methods: enter and exit. On the first line of the context manager, the with statement will call the first method, enter, and whater this method returns will be assigned to the variable after *as**.

Writing your own Context Manager examples

def stop_db_service():
  print("systemctrl stop service cassandra")
  
def start_db_service():
  print("systemctrl start service cassandra")
  

class DbHandler:
  def __exit__(self):
    stop_database()
    return self
    
  def __enter__(self):
    start_database()
    

def db_print_keyspaces():
  print_desc_keyspace("localhost:6042")
  

def main():
  with DbHander():
    db_print_keyspaces
    

As a general rule, always return something on the enter. The return value of exit is something to consider. Normally, we would want to leave the method as it is, without returning anything in particular.

Another alternative is to use the contextlib module. An equivalent code to the above example is

import contextlib

@contextlib.contextmanager
def db_handler():
  stop_database()
  yield
  start_database()
  
with db_hander():
  db_backup()

Everything before the yield statement will be run as if it were part of the enter method.

Underscores in Python

In Ptyhon all default attributes of an object are public. Objects should only expose those attributes and methods that are relevant to an external caller object. Everything that is not strictly part of an object’s interfact should be kept prefixed with a single underscope.

Double underscores are non-Pythonic approach. If you need to define attributes as private, use a single underscope, and respect the Pythonic convention that it is a private attribute.

Creating iterable objects

When we try to iterate an object, Python will call the iter() function over it.

class DateRangeContainerIterable:
  def __init__(self, start_date, end_date):
    self.start_date = start_date
    self.end_date = end_date

  def __iter__(self):
    current_date = self.start_date
    while current_date < self.end_date:
      yield current_date
      current_date += timedelta(days=1)

Then this can be used assigned

r1 = DateRangeContainerIterable(date(2019, 1, 1), date(2019, 1, 20))
print(", ".join(map(str, r1)))

This iterable object will use less memory, however it takes up to O(n) to get an element.

Callable Objects

It is possbile to define objects that can act as functions. One of the most common applications for this is to create better decorators. The magic method call will be called when we try to execute our object as if it were a regular function. Every argument passed to it will be passed along to the call method. This method is useful when we want to create callable objects that will work as parameterized functions, or in some case functions with memory.

from collections import defaultdict

class CallCount:
  def __init__(self):
    self._counts = defaultdict(int)

  def __call__(self):
    self._counts[argument] += 1
    return self._counts[argument]_

Now

>>> cc = callCount()
>>> cc(1)
1
>>> cc(2)
1
>>> cc(1)
2

Good Tools

  • MyPy is a tool for optional static type checking in Python. It analyzes all of the files in your project, checking for inconsistencies on the use of the types. This aids in detecting actual bugs early, but sometimes it can give false positives.

    $ pip install mypy
    

To ignore a false positive, use the following marker as a comment:

some_result = "this is a result" **#type: ignore**
  • Pylint Tool for checking the structure of code (essentially, PEP-8 compliance).

    $ pip install pylint
    

Reference Makefile

typehint:
mypy src/ tests/

test:
pytest tests/


lint:
pylint src/ tests/

checklist: lint typehint test

.PHONY: typehint test lint checklist

With this we can run the following command

make checklist