Introduced since Python 3.5, Python’s typing module attempts to provide a way of hinting types to help static type checkers and linters accurately predict errors.
Due to Python having to determine the type of objects during run-time, it sometimes gets very hard for developers to find out what exactly is going on in the code.
Even external type checkers like PyCharm IDE do not produce the best results; on average only predicting errors correctly about 50% of the time, according to this answer on StackOverflow.
Python attempts to mitigate this problem by introducing what is known as type hinting (type annotation) to help external type checkers identify any errors. This is a good way for the programmer to hint the type of the object(s) being used, during compilation time itself and ensure that the type checkers work correctly.
This makes Python code much more readable and robust as well, to other readers!
NOTE: This does not do actual type checking at compile time. If the actual object returned was not of the same type as hinted, there will be no compilation error. This is why we use external type checkers, such as mypy to identify any type errors.
For using the typing
module effectively, it is recommended that you use an external type checker/linter to check for static type matching. One of the most widely used type checkers in use for Python is mypy, so I recommend that you install it before reading the rest of the article.
We have already covered the basics of type checking in Python. You can go through this article first.
We will be using mypy
as the static type checker in this article, which can be installed by:
pip3 install mypy
You can run mypy
to any Python file to check if the types match. This is as if you are ‘compiling’ Python code.
mypy program.py
After debugging errors, you can run the program normally using:
python program.py
Now that we have our prerequisites covered, let’s try to use some of the module’s features.
We can annotate a function to specify its return type and the types of its parameters.
def print_list(a: list) -> None:
print(a)
This informs the type checker (mypy
in my case) that we have a function print_list()
, that will take a list
as an argument and return None
.
def print_list(a: list) -> None:
print(a)
print_list([1, 2, 3])
print_list(1)
Let’s run this on our type checker mypy
first:
vijay@JournalDev:~ $ mypy printlist.py
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)
As expected, we get an error; since the line #5 has the argument as an int
, rather than a list
.
Since Python 3.6, we can also annotate the types of variables, mentioning the type. But this is not compulsory if you want the type of a variable to change before the function returns.
# Annotates 'radius' to be a float
radius: float = 1.5
# We can annotate a variable without assigning a value!
sample: int
# Annotates 'area' to return a float
def area(r: float) -> float:
return 3.1415 * r * r
print(area(radius))
# Print all annotations of the function using
# the '__annotations__' dictionary
print('Dictionary of Annotations for area():', area.__annotations__)
Output of mypy:
vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}
This is the recommended way to use mypy
, first providing type annotations, before using the type checker.
The typing
module provides us with Type Aliases, which is defined by assigning a type to the alias.
from typing import List
# Vector is a list of float values
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)
Output
vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]
In the above snippet, Vector
is an alias, which stands for a list of floating point values. We can type hint at an alias, which is what the above program is doing.
The complete list of acceptable aliases is given here.
Let’s look at one more example, which checks every key:value pair in a dictionary and check if they match the name:email format.
from typing import Dict
import re
# Create an alias called 'ContactDict'
ContactDict = Dict[str, str]
def check_if_valid(contacts: ContactDict) -> bool:
for name, email in contacts.items():
# Check if name and email are strings
if (not isinstance(name, str)) or (not isinstance(email, str)):
return False
# Check for email xxx@yyy.zzz
if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
return False
return True
print(check_if_valid({'vijay': 'vijay@sample.com'}))
print(check_if_valid({'vijay': 'vijay@sample.com', 123: 'wrong@name.com'}))
Output from mypy
vijay@JournalDev:~ $ mypy validcontacts.py
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)
Here, we get a static compile time error in mypy
, since the name
parameter on our second dictionary is an integer (123). Thus, aliases are another way to enforce accurate type checking from mypy
.
We can use the NewType()
function to create new user defined types.
from typing import NewType
# Create a new user type called 'StudentID' that consists of
# an integer
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)
The static type checker will treat the new type as if it were a subclass of the original type. This is useful in helping catch logical errors.
from typing import NewType
# Create a new user type called 'StudentID'
StudentID = NewType('StudentID', int)
def get_student_name(stud_id: StudentID) -> str:
return str(input(f'Enter username for ID #{stud_id}:\n'))
stud_a = get_student_name(StudentID(100))
print(stud_a)
# This is incorrect!!
stud_b = get_student_name(-1)
print(stud_b)
Output from mypy
vijay@JournalDev:~ $ mypy studentnames.py
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)
This is a special type, informing the static type checker (mypy
in my case) that every type is compatible with this keyword.
Consider our old print_list()
function, now accepting arguments of any type.
from typing import Any
def print_list(a: Any) -> None:
print(a)
print_list([1, 2, 3])
print_list(1)
Now, there will be no errors when we run mypy
.
vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1
All functions without a return type or parameter types will implicitly default to using Any
.
def foo(bar):
return bar
# A static type checker will treat the above
# as having the same signature as:
def foo(bar: Any) -> Any:
return bar
You can thus use Any to mix up statically and dynamically typed code.
In this article, we have learned about the Python typing module, which is very useful in the context of type checking, allowing external type checkers like mypy
to accurately report any errors.
This provides us with a way to write statically typed code in Python, which is a dynamically typed language by design!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.