Harnessing Type Hinting and Mypy for Enhanced Code Readability

Empowering developers with the techniques of type hinting and Mypy to improve code comprehension and maintainability

Introduction

Python is a dynamically typed language meaning unlike java or C#, we do not have to specify data types of variables or arguments in the functions as python handles it at the run time. This makes python flexible and popular among developers as they do not have to track the types. This is fine for short scripts or small projects but for bigger projects or for codes used by third parties it is always better to define types for each of the variables, methods arguments, and return type.

In this blog, we will learn the below topics

  • What is type hinting, and its importance
  • Need for type hints
  • Type hinting the variables, methods, classes
  • Pros and cons of type hinting
  • Conclusion — when to implement and when to avoid

What is type hinting

Let us take an example to understand the topic better. Here is a simple python function that takes the first name, last name, and domain to return an email address.

def generate_email(first_name, last_name, domain_name):
return f'The email address is {first_name}.{last_name}@{domain_name}.com'

print(generate_email('John', 'Miller', 'gmail'))
print(generate_email(100, 'Miller', 'gmail'))

OUTPUT:
The email address is John.Miller@gmail.com
The email address is 100.Miller@gmail.com

We have not declared the datatypes for any of the variables in the above code chunk. So the function works fine and returns the output in both cases but we know that the second output doesn’t make sense. There are ways to enforce the strict checking of type like below but this error will be caught only at run time.

def generate_email(first_name, last_name, domain_name):
assert isinstance(first_name, str), f'First Name should be string'
return f'The email address is {first_name}.{last_name}@{domain_name}.com'

print(generate_email(100, 'Miller', 'gmail'))


OUTPUT:
AssertionError Traceback (most recent call last)
c:\Users\type_hinting.py in <module>
----> 46 print(generate_email(100, 'Miller', 'gmail'))

c:\Users\type_hinting.py in generate_email(first_name, last_name, domain_name)
40 def generate_email(first_name, last_name, domain_name):
----> 41 assert isinstance(first_name, str), f'First Name should be string'
42 return f'The email address is {first_name}.{last_name}@{domain_name}.com'
43

AssertionError: First Name should be string

An assert statement helped us catch the error in the above scenario. The other way would be to have code comments in docstrings specifying the types the function expects but even that does not eliminate the possibility of the above error from occurring.

To know more about docstrings and how it is used for automated technical documentation, please read Powerful Documentation Using Python

Need for type hints

In order to overcome the above shortcoming, Python (3.5 and above) has the ability to annotate the variables with type information. With type hints, we would catch type errors ahead of time before the code executes. The type hints do not run at runtime instead it is checked in the editor or IDE at the development stage. It is not meant to enforce strict type hinting but is an indirect way to ensure that certain errors are handled during coding. In the following sections, we will learn about all the different types and ways to implement type hinting in each case. Here is the snapshot from vscode — with and without type hinting.

Without type hinting — Source: Author
With type hinting — Source: Author

Type hinting the variables, methods, and classes

We will need a few libraries installed before progressing further

pip install mypy, typing

Variables

The variables can be declared and initiated as below very similar to other languages.

# Assign a default value
age: int = 25


# Just initialize the variable, value can be assigned later
age: int

None type

There are times when the function does not return any value like the below example where the function just prints the output. We specify the return type of the function using “ -> None”

def generate_email(first_name : str, last_name : str, domain_name: str) -> None:
print(f'The email address is {first_name}.{last_name}@{domain_name}.com')

generate_email('John', 'Miller', 'gmail')


OUTPUT:
The email address is John.Miller@gmail.com

The none type can be used in the class methods as below. as the init method does not return anything it is set to type None

class Employee:
def __init__(self, first_name: str, last_name: str, emp_id: int) -> None:
self.first_name = first_name
self.last_name = last_name
self.emp_id = emp_id

String type

If the function returns a string type then we specify the type as str

def generate_email(first_name : str, last_name : str, domain_name: str) -> str:
return f'The email address is {first_name}.{last_name}@{domain_name}.com'

print(generate_email('John', 'Miller', 'gmail'))


OUTPUT:
The email address is John.Miller@gmail.com

Int type

If we execute the below function with two different inputs (int and float), the code will run without any errors as python will resolve the types in the run time but if we run it with mypy, it catches the type error as below

from typing import Union

def total_income(salary: int, bonus: int) -> int:
return salary + bonus

print(total_income(2500, 5000))
#-----------------------------------------------------------------------

(venv)(base) C:\Users>mypy type_hinting.py

OUTPUT:
Success: no issues found in 1 source file
#-----------------------------------------------------------------------

print(total_income(2500, 5000.56))

(venv)(base) C:\Users>mypy type_hinting.py

OUTPUT:
type_hinting.py:error: Argument 2 to "total_income" has incompatible type "float"; expected "int" [arg-type]
Found 1 error in 1 file (checked 1 source file)

Union type

If we need to set up more than one type then we use Union meaning either one of the declared types is acceptable.

from typing import Union

def total_income(salary: Union[int, float], bonus: Union[int, float]) -> Union[int, float]:
return salary + bonus

print(total_income(2500, 5000.56))

(venv)(base) C:\Users>mypy type_hinting.py
OUTPUT:
Success: no issues found in 1 source file

Note: Python 3.10 has replaced the Union with “|”. The above code can also be written as

def total_income(salary: int | float, bonus: int|float -> int | float:
return salary + bonus

Any type

A special kind of type is Any which lets all types be compatible.

from typing import Union

def total_income(salary: Union[int, float], bonus: Union[int, float]) -> Any:
return salary + bonus

print(total_income(2500, 5000.56))

(venv)(base) C:\Users>mypy type_hinting.py
OUTPUT:
Success: no issues found in 1 source file

Optional type

An optional type accepts only the defined type or none. In the below code, the age can be an int or none. If a string is passed as an argument, the mypy would catch a type error.

from typing import Optional

def employee(age: Optional[int], exp: int):
print(f'Tha age is {age} and experience is {exp}')

employee(25, 5)

(venv)(base) C:\Users>python type_hinting.py
OUTPUT:
The age is 25 and experience is 5

#-------------------------------------------------------------------------

employee('John', 5)

(venv)(base) C:\Users>mypy type_hinting.py
OUTPUT:
type_hinting.py:error: Argument 1 to "employee" has incompatible type "str"; expected "Optional[int]" [arg-type]
Found 1 error in 1 file (checked 1 source file)

List type

If a function expects input in the form of a list and returns a list object.

from typing import List, Union

def profile(name : List[str], income: List[Union[int, float]], address: List[Union[str, int]]) -> List:
return name + income + address


print(profile(["John", "Miller"], [5000, 500], ["IND", 562245]))
#--------------------------------------------------------------------------

(venv)(base) C:\Users>mypy type_hinting.py
OUTPUT:
Success: no issues found in 1 source file
#--------------------------------------------------------------------------

print(profile(["John", "Miller"], 5000, ["IND", 562245]))
(venv)(base) C:\Users>mypy type_hinting.py
OUTPUT:
type_hinting.py: error: Argument 2 to "profile" has incompatible type "int"; expected "List[Union[int, float]]" [arg-type]
Found 1 error in 1 file (checked 1 source file)

In the second output, we passed a value of 5000 instead of a list and hence the error. A similar implementation can be done for dictionary, set, and tuple objects as well.

Type hinting the classes

Similar to the function, we can also annotate class methods as below. In the below example, the init method returns None and the other method returns a string

class Employee:
def __init__(self, first_name: str, last_name: str, emp_id: int) -> None:
self.first_name = first_name
self.last_name = last_name
self.emp_id = emp_id


def generate_email(self) -> str:
return @gmail.com">@gmail.com">f'{self.first_name}.{self.last_name}@gmail.com'

#--------------------------------------------------------------------------

employee1 = Employee('John', 'Miller', 123456)
print(employee1.generate_email())

OUTPUT:
John.Miller@gmail.com

Advantages of type hinting

  • It helps to better document the code where input and output types are specified thus making it less ambiguous.
  • It lets us do automated checks with Mypy
  • Modern editors, IDE’s have plugins to catch errors and recommend types
  • The end result is clean architecture, better looking, and easy-to-read code

Disadvantages of type hinting

  • The start-up time is slightly high hence for small python scripts, it can be avoided as it results in overhead in the performance.
  • More development effort for developers to add type hints

Conclusion

As we have seen, it is not mandatory to use type hinting in python but it is recommended given its usefulness. The best part of python is we don’t have to implement it in the entire project, we can use it where necessary and skip where it’s not. For anyone starting new with python, the type hinting can be skipped and should be picked up once the language becomes more comfortable to use. Also, we can use mypy for automated testing of types and it can also be configured as part of Tox for automated testing as part of workflows.

To know more about Tox and automated testing, please refer Automate And Standardize Testing With Tox In Python

I hope you liked the article and found it helpful.

You can connect with me — on Linkedin and Github

References

https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
https://docs.python.org/3/library/typing.html

Leave a Reply

Your email address will not be published. Required fields are marked *