I have been trying to code daily the last several weeks. Shortly after starting a project in Python, I decided to refactor it to use Python type hinting. Type hinting is a newer feature of the language that I’ve known about, but hadn’t yet used much. So, I gave it a shot.
Why?
Before getting started, you might be asking yourself “Why bother with types in Python? Isn’t dynamic typing a feature that makes python a great language in the first place?”. While dynamic typing can be easier to code, there are some benefits to a more static typing approach. To name a few:
-
Static types help clarify what a function expects for parameters, and what it returns. You can look at a single line of code and immediately understand what will be returned by a function, without having to skim through and follow the code.
-
Static typing increases the power compiling/linting has to find errors before run-time.
-
It forces the developer to fully think through code design a bit deeper. Having to match up data types when chaining functions involves a bit more planning compared to just throwing generic data around.
Basic Type Hinting
By default, python is a strongly typed, dynamic language. This means that while Python does have types for everything, the system generally does all of this under the hood, without requiring the programmer to define and keep track of the types. However, ’type hinting’ provides the ability to define types in python code.
For an example, lets start by looking at this stupid simple function. It
takes an age for input, and returns a string that states how old you will be in
the future (it increment’s the age by 1 and adds it to a string 😑. I know,
riveting code here…):
def next_year_age(age):
new_age = age + 1
return f"In the future, you will be {new_age}!"
It can be assumed that age
is likely an int
, and that the return value is a
str
… but thats not a given. So, lets declare the types using type hinting.
To use type hinting in python, just specify the type when defining variables
using the format var_name: type
. Additionally, to define a return type for a
function, add a ->
symbol at the end of the function def pointing to the
return type. So, our example function would look something like this:
def next_year_age(age: int) -> str:
new_age: int = age + 1
return f"In the future, you will be {new_age}!"
Here we defined that the age
parameter, as well as the new_age
variable
inside the function, are both int
s. Additionally, we declared that the
function expects to return a string, by using the -> str:
in the function
definition.
MyPy
When using type hinting, it is strongly suggested to use
Mypy, an optional static type checker for python, which
can be installed using pip
:
pip install mypy
To try mypy
out with our example function above, lets assume that it
is saved in a file named example.py
. To run a basic check:
(post_examples) ➜ post_examples mypy example.py
Success: no issues found in 1 source file
Looks good! Now, lets pretend we accidentally wrote that the function
would return a float
(even though it actually returns a str
):
def next_year_age(age: int) -> float:
new_age: int = age + 1
return f"In the future, you will be {new_age}!"
Mypy will catch the error for us:
(post_examples) ➜ post_examples mypy example.py
example.py:3: error: Incompatible return value type (got "str", expected "float")
Found 1 error in 1 file (checked 1 source file)
Strict Checking
Up until now, mypy
has just told us if there are any issues with the type
hinting we added. It hasn’t been too pushy about ensuring we have types
declared for everything.
To have mypy
enforce that we write code with un-ambiguous types (and get the
most out of static typing), we can use the --strict
flag. Looking at our
original example again, lets say we didn’t declare a return type at all:
def next_year_age(age: int):
new_age: int = age + 1
return f"In the future, you will be {new_age}!"
Running mypy
normally won’t care about this omission, because none of the
types we did declare are wrong:
(post_examples) ➜ post_examples mypy example.py
Success: no issues found in 1 source file
However, using the --strict
flag on the same file will let us know that we
forgot to define a return type:
(post_examples) ➜ post_examples mypy --strict example.py
example.py:1: error: Function is missing a return type annotation
Found 1 error in 1 file (checked 1 source file)
More Complex Types
If you need to use compound or more complicated types, you might have to import
from the typing module to do
so. For example, lets say we want to instead have the function return a list of
the two ages. We can do this by importing List
from typing
, and then define
the return type as a list of int
s:
from typing import List
def next_year_age(age: int) -> List[int]:
new_age: int = age + 1
return [age, new_age]
If we want to instead use a 2-element tuple:
from typing import Tuple
def next_year_age(age: int) -> Tuple[int, int]:
new_age: int = age + 1
return (age, new_age)
and so on.
Lastly, to define your own class and type hint it:
from typing import Tuple
class Person:
def __init__(self, name: str, age: int):
self.name: str = name
self.age: int = age
def next_year_age(age: int) -> Tuple[int, int]:
new_age: int = age + 1
me: Person = Person("Ryan", age)
return (me.age, new_age)
mypy
can check that as well:
(post_examples) ➜ post_examples mypy example.py --strict
Success: no issues found in 1 source file
The typing
module also supports types like the Any
, Callable
, Union
s and
others.
Issues
As I progressed further in my project, I started hitting more and more
limitations/frustrations specifically related to the type checking. Most
often, it was related to the crazy work-around I had to do to support Optional
parameters and variables. Optionals are supported, but a bit complicated.
The biggest issue I had was being able to tell the system “I checked, and
definitely have this object, it’s not None
”. I would check for a value and
only run a case on it if it existed, but the type system would still refuse to
run logic like <
on a Optional(int)
type. It didn’t catch on to the fact
that I had wrapped the variable in a case that can only occur if the value was
an int
.
I eventually tried using the returns
library, which was great and can apparently help get around these issues by
introducing Maybe
and Some
types, but at that point I realized I had added
SO much overhead complexity to a very tiny and simple side project, that it
really didn’t make sense.
Conclusion
Type hinting in python is probably best if you want to sprinkle it throughout
the code without --strict
checking, or if you have something where everything
is rather straight forward and there aren’t many ‘optional’ variables defined
and passed around. For projects where you want really strict type checking…
you should probably use a different language (ex: Rust or Go) instead of forcing
Python to be something it wasn’t designed to be from the start.
Still, I appreciate that type hinting exists in Python and I think I’ll use it throughout my projects more often. If you haven’t used it before, I recommend giving it a try!
Gnome Font Scaling Script Traded in my M1 MacBook Air for the 14" MacBook Pro