Type Hinting - Level Up Your Python Game
Python series - Bas Dunn - April 2024
This is a blogpost for intermediate-level Python engineers.
Are you already a pro? Keep an eye out for future blog posts on more advanced topics.
You've been working with Python for a while now. But, you want to level up your Python skills. Because, as you write more code you see more and more bugs popping up in production that you need to solve. This slows down the development of new features. In addition to that, you will get a new colleague who will join you in your codebase. What's the first thing you do?
There are many facets to code reliability, collaboration and development speed, some of which we will touch upon in future blog posts. But, one technique to improve all three of those greatly is type hinting in Python.
Type hinting explained
You are familiar with types. An object in Python can be an integer, string, list, dictionary, functions etc. Python is a dynamically typed programming language, this basically means you do not have to explicitly specify the 'type' of a variable or argument.
You can however, hint what the type of an object should be. In this case, what the type of arguments of the function should be:
def sum_up(value_one: int, value_two: int) -> int:
return value_one + value_two
In this example we hint that both arguments should be integers (
value_one: int
) and that the return value is an int (
-> int:
).
So, what happens if we completely ignore the hints and run:
sum_up(value_one='hi there', value_two='something else')
# output: value_onesomething else
It still works! Adding two strings is valid Python. This function will just return another string.
This is important to remember: Type Hints in Python are just… Hints… They do not change how your application works.
If they are just hints that can be ignored? What is the use?
Reasons to use type hinting
It explains better what a function does, not only to you, but also to others. Let's assume your colleague wrote the following code:
def determine_winner(player_one, player_two):
if player_one > player_two:
return 'player_one_wins'
elif player_one < player_two:
return 'player_two_wins'
else:
return 'tie'
As a developer you see this function and you wonder: how did he or she intend this function to be used? Because, it can be any of the following:
It determines the winner of a game where you have to;
- draw the highest number, like:
determine_winner(10, 15)
- write the longest speech. like:
determine_winner('I have a dream', 'I dont')
- come up with the most yellow fruits. like:
determine_winner(['lemon','banana'], ['orange', 'orange'])
- all of the above.
You already figured out that they are all valid Python code (e.g. all function calls run properly). You can ask your colleague to add a bunch of comments that explain the purpose of the function. But, it would be better to add type hints like so:
def determine_winner(player_one: int, player_two: int) -> str:
if player_one > player_two:
return 'player_one_wins'
elif player_one < player_two:
return 'player_two_wins'
else:
return 'tie'
Boom, that tells you it's a game where you have to pick the highest value. This will prevent you misusing the function which would result in faulty code.
Note that type hinting alone does not give the most clearity. Combine it with proper function names and argument types and you get easy to read and interpret code. This is also a good basis for making proper use of abstraction. For example, we could rename the arguments like this to make it more clear:
def determine_winner(player_one_guess: int, player_two_guess: int) -> str:
Type checking in your IDE
You are allowed to ignore the hint of your colleague, Python does not care. You can however, configure your IDE and other tools to indicate when you are using a function incorrectly (e.g. ignoring the hints). We call this concept type checking.
Modern IDEs do type checking out of the box. If it does not work in your IDE, try googling "How to enable Python type checking in [my_IDE]".
For example, if we use the determine_winner function incorrectly, it tells me I should not insert a string in the function. Remember: although it looks like an error, your code still runs, it's still just a hint.
What's great about this is that before running the code you already know you are misusing a function. This will likely result in bugs in production. Luckily, you can now prevent that due to type hinting and checking.
Type checking in CICD with mypy
Getting feedback from your IDE is super valuable already for you and your colleagues. However, people are still able to ignore the feedback from the IDE. You want to make sure that all code reaching your main-branch on gitlab/github has its types checked.
For this you cannot rely on your IDE. Instead you can use the mypy Python package in combination with gitlab CICD or github actions. It will automatically check the types when new code is pushed or a merge request is created.
For example, if we run the mypy command for the previous example, we get the following output:
bas@macbook demo % mypy python-blog.py
python-blog.py:9: error: Argument 1 to "determine_winner" has incompatible type "str"; expected "int" [arg-type]
python-blog.py:9: error: Argument 2 to "determine_winner" has incompatible type "str"; expected "int" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
As you can see, it is the same error as the IDE showed us.
Add the folowing Gitlab CI file to your project to enable automatic checking of types in Gitlab.
stages:
- test
mypy:
stage: test
image: python:latest
script:
- pip install mypy
- mypy .
Where to start?
Start small. For example, start using 'str' and 'int' types. First apply it to new code and then gradually add it existing code.
We do suggest to start using mypy in your gitlab CI or github actions as soon as possible.
Reach out to us if your organization is experiencing issues with Python applications.