Experiment, Fail, Learn, Repeat

Life is too exciting to just keep still!

Writing static python with mypy

Python is a dynamically typed language - which provides a huge developer experience as compared to a statically typed language such as Golang. Python does serve as a nice introductory programming language for new developers but as time goes by, it’s pretty easy to see why static programming language is why nicer to work with as compared to dynamically typed language. Due to the nature of such languages, it is easy to be “loosey” about the types of the variables which inadvertably makes the code harder to follow as codebases grow larger and larger. With such large codebases - even type hints on IDE becomes harder to establish (either takes too long or the tooling just deems it impossible to do so)

NOTE: This article is just a single developer’s opinion. Feel free to disagree with it since each person’s experience with programming languages are wildly different.

Nicely enough, we can control the chaos by slowly introducing some sort of typing into such Python programs. That’ll make it easier to understand what type of variable to pass into functions. This is done by using a tool called mypy. Refer to the following website: https://mypy.readthedocs.io/. We can install it globally or in each virtual environment set up by each Python project.

Typing introduced in Python is done via type annotations. Here is one example of how we can set a function accept a string variable. The function accepts 1 singular parameter with string type. It will return no output, which is defined by the None type annotation (defined with arrows to none. -> None)

def function1(name: str) -> None:
    print("printing {}".format(name))

The following function is called as follows:

function1("aac") 

Do take note that even if we have all these type annotations in place, we can still run such python code even if it’s not conforming to the types - e.g.

function1(123)

We can benefit from all these static typing by installing the mypy plugin if we use the Visual Studio Code. Reference: https://marketplace.visualstudio.com/items?itemName=matangover.mypy

We can setup the configuration file (mypy.ini) as follows:

[mypy]
disallow_any_unimported = True
disallow_untyped_calls = True
disallow_untyped_defs = True
warn_return_any = True
warn_unreachable = True

Once we have it all setup properly, there will be the red squiggly line when the types are no inline based on the types defined by the function.

Let’s go with further examples. Let’s say we have a function that we want to have a function that accepts an integer but it returns an integer instead of returning nothing.

def function2(lol: int) -> int:
    return lol + 5

function2(12)

Let’s change things up and instead, we have functions that accept an object instead of basic types such as integer or string etc. function4 would simply accept Hoho object and it does not return anything from the function. function4a would accept a simple integer variable but returns a instantiated Hoho object.

class Hoho():
    hoho: str
    santa: float

    def __init__(self, santaInit: int) -> None:
        self.hoho = "acaca"
        self.santa = santaInit

    def print_santa(self) -> None:
        print("value of santa {}".format(self.santa))

def function4(za: Hoho) -> None:
    za.print_santa()

def function4a(pp: int) -> Hoho:
    return Hoho(pp)

h = Hoho(123)
function4(h)
a4 = function4a(79)
function4(a4)

Alternatively, we can change it up such that we have a function that accepts a list of integers for our function. Here is how we define the type annotation for a list of integer that would be passed as parameter for a function.

def function5(zolo: list[str]) -> int:
    ya = 0
    for x in zolo:
        ya += 1
        print("item: {}".format(x))
    return ya

function5(["acac", "qwec", "kqlmc"])

So, we have covered basic types such as strings, integers, etc, user defined objects and list of objects. In the python language, it is possible for a function to accept another that it would be processed further.

from typing import Callable

def function6(qa: str, zzz: Callable[[str, str],bool]) -> None:
    p = zzz("acac", qa)
    if p:
        print("function6 and true") 
    else:
        print("function6 and false")


def function7(ya: str, zzz: str) -> bool:
    print(ya)
    print(zzz)
    if ya == "ya":
        return True
    else:
        return False

Let’s say we would want to use an external library such as pandas. Apparently, when we use such external libraries, apparently the types doesn’t come in built together with the actual package. However, if that’s the case, then we would not be able to use external libraries easily and ensure that types from such external libraries would flow easily into the python scripts that we write.

The solution as of now is to install stub libraries - in the case, for pandas, apparently, there is a pandas-stubs library. With pandas-stubs library, we can use types such as the DataFrame type - which is technically a type that would be provided by pandas and would be an object that some of the function that returns the DataFrame object.

import pandas as pd 

df = pd.read_csv("lol.csv")

def function1(data: pd.DataFrame) -> int:
    return len(data)

val = function1(df) 
print(val)

Unfortunately, the above set of code snippets are simply small easy code snippets that can demonstrate the possibility of introducing typing for Python scripts. In order to properly test this out, we would need to introducing typing to actual large python codebase - that would give us a bit of “battle testing” to show how it can be useful for developers - since a large point of introducing typing into python programs is to make the developer experiences way smoother.