Keeping Python Code Clean

Intro

In this section, I try my best to collect and describe and first of all put down my understanding on how to implement all the techniques that contribute to the clean code of an application.
I touch here both theoretical principles and libraries that assists with feedback on Python's syntax.

Features

App includes following features:

  • Code snippets
  • Pylint

Demo

In this section I describe following concepts:

SOLID

SOLID is the set of good practicies that lead us to properly desing software keeping to the concept of the Object-orinted programming.

Terminology:
  • Abstract class is a class that contains one or more abstract methods. They cannot be instantiated and they require subclasses to provide implementations for their abstract methods.
  • Abstract method is a method that is declared, but contains no implementation. Needs to be implemented by subclasses that inherits them.
    from abc import ABC, abstractmethod
    
    class Save(ABC):
    
      @abstractmethod
      def save_data(self):
        print("Saving data...")
    
    class SaveToDb(Save):
    
      def save_data(self):
        super().do_something()
        print("to database.")
    
    x = SaveToDb()
    x.save_data()
    
    -------------------------
    # output:
    # Saving data...
    # to database.
    - Save abstract class defines abstract method save_data,
    - SaveToDb class implements save_data method and extends it with additional functionality.
  • Inheritance - child class inherits all of the attributes and methods of parent class. However, everything that is inherited by child class can be extended in order to fit requirements that child class has to fulfil. In such case there in a need to re-implement the method in the child class which is called the method overriding. In the above code section, the child class SaveToDb inherits from the parent class Save.
  • Polymorphism - naming methods the same in child class means that the same function can be used by different types/classes that inheritns from a base class .
Single Responsibility Principle (SRP):
  • Every class should only have one responsibility
    and therefore only one reason to change.
  • In other words, a class is responsible for performing only one specified task.
  • Too many tasks within a class would cause to much complexity and change of one task can affect another task's work.
  • When multiple tasks in a class and there is a new requirement from the business, it forces us to modify existing code which violates rest of SOLID principles.
  • With this SRP, maintaining the code is much easier and readable when handing code over.
  • When class perfoms only one task, it is much easiser to test it out.
  • SRP enables flexibility in extending the application with new business requirements.
Open Closed Principle (OCP):
  • Software Entities (classes, functions, modules)
    should be open for extension but closed to change.
  • Without OCP, when a new business requirement comes in, the most common approach is modifing the definition of existing method.
  • Keeping modifying definition of extisting functunality we end up testing the enire scope repeatedly.
  • When following OCP, instead of modifying, we should extend the aplication to satisfy the new requirement.
  • Avoiding modifying, we are less likely to introduce some bugs into the existing code as well as we avoinding redeploying the same code again and again.
  • Extension can be achieved by inheritance from parent class where child class satisfies a new requirement.
    - child class extends existing parent classes,
    - child class can override definition of parent's methods which covers a new requirements.
  • We can be keeping an abstract class - the most general one that will never be instantiated but will be a pattern to inherit from by sub-classes where each sub-class answers specific business requirement.
Liskov's Substitution Principle (LSP):
  • If S is a subtype of T, then objects of type T
    may be replaced with objects of Type S.
  • This principle is strictly related to proper inheritnace where any child class can replace its parent class without breaking functionality.
    class Vehicle:
    def count_wheels(self):
    	pass
    
    class Car(Vehicle):
    def count_wheels(self):
    	pass
    
    class Truck(Vehicle):
    def count_wheels(self):
    	pass
    
    def vehicle_wheels_count(vehicles):
    for viehicle in vehicles:
    	print(viehicle.count_wheels())
    
    def main():
    vehicles = [Car(), Car(), Truck()]
    vehicle_wheels_count(vehicles)
    - Vehicle class defines count_wheels method,
    - every child class that inherits from Vehicle parent class implements count_wheels method,
    - count_wheels method can be extended due to subclass's specification as car's wheels will be counted differently than truck's wheels,
    - the method vehicle_wheels_count doesn't care about type of passed object, it just calls count_wheels method on every object of the passed list,
    - all the method vehicle_wheels_count knows is that each object in the passed list must be of the Vehicle type, regardless of it is from Vehicle parent's class itself or from child class (Car, Truck).
Interface Segregation Principle (ISP):
  • Clients should not be forced to depend upon interfaces that they do not use
  • Interface is a class-like structure that lists what methods needs to be implemented by a class that makes usage of this interface. Like I said, it only indicates the methods, interface doesn't define their implementation - this is job for the class.
  • In Python, there is no interface as such. However, Python allows to implement abstract class with usage of abc module.
    - we cannot instantiate an abstract class,
    - abstract class defines methods that needs to be implemented in all of the child classes that inherits from it,
    - with using abract classes we can be creating Python's interface-like structures.
  • Lets set up scenario where there is a big interace that has multiple abstract methods:
    - class that uses that interface needs to implement all of the abstract methods (in Python, it is required by decorator @abstractmethod),
    - however, there may be some abstract methods that our specific class doesn't need to implement,
    - implementing needless method or implementing it as pass violates SOLID principles,
    - following ISP, we would need to divide one main interface to several ones due to their functionality so that classes tha would use them can implement only methods that they need.
Dependency Inversion Principle (DIP)
  • High-level modules should not depend upon low-level modules.
    Both low and high level classes should depend on the same abstractions.
    Abstractions should not depend on details.
    Details should depend upon abstractions.
  • There are some characteristic terms for a bad code design:
    - Rigidity: when it's hard to do some changes to the code as they affect to many parts of the application. When change necessary, it forces us to modify another dependent part of the code. Furthermore, it causes hardships with time and costs estimation for changes implementation,
    - Fragility: when changes being implemented causes crash of app's parts that we did not expect. It lowers trust in the project that uses the code because each another change can cause avalanche of errors from unknown sides,
    - Immobility: when it's impossible to use the code in another application as it's very difficult to extract it. Difficulties result from too big dependecny of code section against other ones. It happens that finding dependecies and extracting the code for reusing is more time consuming than writing it from scratch.
  • Abstractions need to be organized is such way they don't depend on details (concrete implementations) but rather other way around - the implementations should depend on abstractions.
  • The main assumption over here is that implementations changes more often than the abstraction they were created on.
  • Low-level modules are sub-modules of a high-level module which in turn can be the low-level module for another module.
  • Lets take two classes A and B as the example:
    - A depends on B, when B changes, it crashes the code,
    - we need to invert the dependencies and make B to adopt to A,
    - this can be achieved by implementing interface (abstract class A) and forcing code to depend on defined interface instead of a specific implementation (class B),
    - while we expect implementations to change frequently, the abstraction stays unchanged.

DRY

Don't Repeat Yourself
  • This is general language-independent principle that aims at reducing repititions in the source code.
  • The same code snippet repeated in different places of the code makes project hard to mantian. Even one small change or a new business requirement would force us to come through and update every section that change concerns.
  • The simplest way to keep a code DRY is to delegate as much repetative functionalities as possible to an external reusable function. And then whenever I need this functionality to implement, I simply call the function.
  • With that solution, whenever I need to update some functionality, I just do this once in the function that provides is instead of going to each place it's implemented.
  • Code example:
    def save_to_db() -> None:
      db.commit()
      db.save()
      print("saved to db")
    
    def enter_data() -> None:
      validate_data()
      save_to_db()
    
    def exit_app() -> None:
      save_to_db()
      logout()
    
    - Above method save_to_db is reused in another app's methods where is a need to save some data to database.
    - Hence, only method save_to_db contains all the logic an functionality of saving data to database.

KIS

Keep It Simple
  • Princilple advises to keep simplicity as the highest priority and avoinding complexity when programming.
  • There is a saying, when you come back to the code and you don't know what is happening in there, then it doesn't follow KIS principle.
  • It's very important principle even though it sounds like a very general one. However, keeping it simple makes us aware of code simplicity which we can treat as an inverstment that returns itself when maintaining the code.
  • KIS is even more important when handing over the code to someone else for maintaining it.

YAGNI

You Aren't Going to need it.
  • New code being added, should do as exact things as requirements say at the moment.
  • Writing lines for the future because of any potential requirements that can come in is pretty senseless as we waste the time for futher maintenance of the code that is unused and waits for requirement that, perhaps, will never come.
  • Making an excessive predictions and writing as flexible code as possible is pretty idle as we cannot predict everything. The art here is just adjusting flexibility to the current situation.

Pylint

Pylint library
  • Source code analysis tool which looks for syntax errors and gives refactoring suggestions.
  • All we need to do is:
    pip install pylint
    and then:
    pylint main.py
    where main.py is the module whose code we want to examine for syntax errors
  • Code example:
    #
    # my app version 1.6
    # by Artur
    
    class animal:
    
      def __init__(self,name) :
        self.name = name
    
    def rename_animal(animal1, animal2):
      animal1.name = 'Tom'
    
    animal1 = animal('Gerry')
    
    rename_animal(animal1, my_animal)
    
  • Pylint feedback:
    ************* Module main_for_pylint
    main.py:1:0: C0114: Missing module docstring (missing-module-docstring)
    main.py:5:0: C0103: Class name "animal" doesn't conform to PascalCase naming style (invalid-name)
    main.py:5:0: C0115: Missing class docstring (missing-class-docstring)
    main.py:5:0: R0903: Too few public methods (0/2) (too-few-public-methods)
    main.py:10:0: C0116: Missing function or method docstring (missing-function-docstring)
    main.py:10:18: W0621: Redefining name 'animal1' from outer scope (line 13) (redefined-outer-name)
    main.py:10:27: W0613: Unused argument 'animal2' (unused-argument)
    main.py:15:23: E0602: Undefined variable 'my_animal' (undefined-variable)
    
    --------------------------------------------------------------------
    Your code has been rated at -7.14/10 (previous run: -5.71/10, -1.43)
    
    Here is the pylint's feedback line-by-line explanation:
    - It says that there is no such thing as introducing docstring that would describe the modeule,
    - It indicates that class name "animal" should be capitalized.
    - Once again there is no docstring describing the class.
    - It suggests the class hass too few or none of public methods.
    - Once again there is no docstring describing the function.
    - It screams that variable "animal1" in the local scope of the function is as the same as the variable in the global scope.
    - It indicates parameter "animal2" in the function "rename_animal" is not used.
    - It shows that variable "my_animal" has never been defined so it cannot be passed in the function.
    - On the bottom we can see the rate of the code and comparison how it looked like during last assessment.

Decorators

Decorators
  • Decorators extend a function's functionality without modifying its code. Decorator is simply the function that brings additional functionality to a function that already exists.
  • Decorators can be custiomized by a programmer or can be provided by some library that we import to our code.
  • Code example:
    def extend_user_rights(func):
      def inner():
        func()
        print("You have got admin rights!")
      return inner
    
    @extend_user_rights
    def ordinary_user():
      print("Hello User!")
    
    if __name__ == "__main__":
      ordinary_user()
    
    - Function extend_user_rights extends ordinary_user function and give additional functionalities.
    - What kind of functionalities decorator adds can be defined in decorator's body.
    - What is the best about decorators they can be reused many times for different regular functions that, besides their own tasks, give also admin rights. And as we know from DRY anything that ensures reusability contributes to having the clean code.
    - Function func in decorator's body is replaced by any function we want to decorate.

Encapsulation

Encapsulation:
  • It refers to object-oriented programming where we want to bind data with specific section of code.
  • We can think of it as if a shield that prevents data (variable) from being accessed and thus manipulated by the code outside the shield.
  • In practice, we hide a class variable from any other class. The variable can be accessed only through a member function of the class that owns that variable. By hiding a variable we make it private to a class that delcares it.
  • Some benefits:
    - User of the app doesn't have any idea of inner implementation of a particualr class. He sees only what we allow him to see which is basically passing the value to a method that initializes variable with passed value. Method that initializes variable can add some logic and validation to restric values to a specific let's say range.
    - We can make a class variable as read-only or write-only as per our requirements.
    - It ensures reusability and makes easier to change it with some new requirements.
  • Code example:
    class Shop():
    
      def __init__(self):
        self.category = "Adventure"
        self.__item = ""
    
      def product(self):
        return self.category + ": " + self.__item
    
      def set__item(self, name):
        self.__item = name
    
      def get__item(self):
        return self.__item
    
    sh = Shop()
    sh.set__item("Video")
    
    sh.get__item()
    # Video
    
    sh.product()
    # Adventure: Video
    
    - to make a variable private we need to put double scores before its name like: __variable_name.
    - to access the private variable we need to apply getter and setter which are member methods with access to the private variable.

Static typing

Static typing in dynamic Python:
  • At first, let's see overview on programming lanugages:

    source: android.jlelse.eu
  • As we can see, Python itself is the a strong-dynamically typed pragramming language. It means we don't have to declare type of variable before initializing it with a vlaue. Dynamic Python follows duck typing:
    If it walks like a duck and it quacks like a duck, then it must be a duck.
  • In other words, we don't have to call it a duck (defining its type) when it has duck's properties and methods.
  • When Python faces na object, assumes object's type based on properties or methods this type has declared within. In case when a specific property doesn't exist and Python tryies to call it on the object then exception (error) appears. However, it apperas during running the code as opposed to statically typed programming languages where compiler checks entire code before actual running.
  • Below code are going to be executed as long as list elements contains object of a type with details attriubute. When loop encouters an object of a type where there is no attribute details declared then it throws an error. Thus, it can make 100 iterations but 101 iteration can cause an error.
    def get_detailed_info(elements):
      for element in elements:
        print(element.details)
    
  • Due to dynamic Python's nature, types declaration is optional however it increses code readibility and code's correctness control.
    from typing import List
    from element import Element
    
    def get_detailed_info(elements: List(Element)) -> None:
      for element in elements:
        print(element.details)
    
    - What we type statically in Python most often is types of passed arguments to functions and methods as well as returned types.
    - We type also variable that is unclear, for example it's a good practice to declare types of elements in a list: self.grades: List(int) = []
    - In fact, Python ignores types adnotations, however, in cooporation with IDE as PyCharm it highlights where declared type differs from passed type.
    - Besides PyCharm's help, there is a library called mypy that reads the code and lists all type's conflicts. We just need to install it: pip install mypy and use it: mypy main.py.
  • Let's languages set

    source: itnext.io

    - As projet gets bigger, the productivity of maintaining it in dynamically tiping decreases and costs increase. We will eventually hit a point where there is just too much to try (type) and catch (error) as it is in dynamic language.
    - In addition, with no types addnotations code refactoring may become a big challange when you or someone else comes back to an old code because you have no idea what is the data structure of each variable.
  • Before overall comparizon let's understand following terms: - Source code: original code (usually typed by a human into a computer).
    - Translating: converting source code into machine code.
    - Run-time: period when program is executing commands (after compilation, if compiled.)
    - Complied: code translated before run-time.
    - Interpreted: code translated on the fly, during execution.
  • Let's see the actual comparison:

    Static typing Dynamic typing
    Types checked before run-time. Types checked on the fly, during execution.
    Runs faster because of not needing to check types while executing. Runs slower because of dynamic type checking.
    At first there is a delay before running for type-checking. No delays as there is no preceding type-checking.
    Prevents variables from changing types Allows variables to change types further in the code.
    Catches errors early. Catches errors during execution.


    - However, we need to distinguish Compiled and Interpreted:

    Compiled Interpreted
    Fast to develop (edit and run) Slow to develop (edit, compile, link and run)
    Code translated into binary code before run-time. Code being translated on the fly, during execution.
    Slow to execute because each statement had to be interpreted into machine code every time it was executed Fast to execute. The whole program is already in native machine code
    More convenient for static languages. More convenient for dynamic languages.

Method overloading

  • It is an ability of a function to behave in different way based on the parameters it receives.
  • Defining a function we can accept zero, one, or multiple parameters so that we can call that function with zero, one or many parameters.
  • It improves code reusablity as we can overload the function instead of writing a brend-new one that differs only slightly. Thus, we can call one function in different ways, so this is what is called a method overloading.
  • Simpliest example:
    class User:
    
      def welcome(self, name=None):
        if name is not None:
          print(f'Hello {name}!')
        else:
          print('Hello there!')
    
    u = User()
    
    u.welcome()
    # Hello there!
    
    u.welcome("Artur")
    # Hello Artur!
    
    - As you can see above, function u.welcome can be called in two ways.
    - name parameter is set as optional.
  • Using multipledispatch library:
    from multipledispatch import dispatch
    
    @dispatch(int, int)
    def add(x, y):
      return x + y
    
    @dispatch(object, object)
    def add(x, y):
      return f'{x} {y}'
    
    add(2,2)
    # 4
    
    add('High',5)
    # 'High 5'
    
    - At first, we need to go with pip install multipledispatch.
    - Decorator dispatch states what kind of object decorated function can accept.
  • Operator overloading:
    class Point:
    
       def __init__(self, x, y):
           self.x = x
           self.y = y
    
       def __add__(self, other):
    
           if isinstance(other, Point):
               x = self.x + other.x
               y = self.y + other.y
           else:
               x = self.x + other
               y = self.y + other
    
           return Point(x,y)
    
    a = Point(1,1)
    b = Point(3,4)
    
    c = a + b
    print(c.x, c.y)
    # 4, 5
    
    d = a + 5
    print(d.x, d.y)
    # 6, 6
     
    - We overload + operator by speical method __add__.
    - Inside __add__ we check if second parameter is type of Point. If so then we sum x and y coordinates up together respectively. If other is object of integer then we add it to x and y coordinates.
    - By operator overloading we redefined the + operator. Otherwise the line c = a + b would raise an error.

Setup

No specific installation required.

Source Code

You can view the source code: HERE