This post was originally published on my personal blog. Give it a look! 😄
Python is known for being a Wild West language where anything goes. Aside from indentation, code style and documentation is left mostly up to the opinion of the developer writing the application, which can lead to some messy, unreadable code.
This comes partially from Python being a dynamic typed language, meaning that types are associated with the variable's value, not the variable itself. This means that variables can take on any value at any point, and are only type checked before performing an action with them.
Consider the following code. In Python, this is totally acceptable.
age = 21 print(age) # 21 age = 'Twenty One' print(age) # Twenty One
In the code above, the value of
age is first an int, but then we change it to a str later on. Every variable can represent any value at any point in the program. That is the power of dynamic typing!
Let's try to do the same thing in a statically typed language, like Java.
int age = 21; System.out.print(age); age = "Twenty One"; System.out.print(age);
We actually end up with the following error, because we are trying to assign
"Twenty One" (a
String) to the variable
age that was declared as an
Error: incompatible types: String cannot be converted to int
For this to work in a statically typed language, we would have to use two separate variables.
int ageNum = 21; System.out.print(ageNum); String ageStr = ageNum.toString(); System.out.print(ageStr);
This works, but I really like the flexibility of Python that allows the dynamic typing, so that I don't need to clutter my code with more variables than necessary. But I also enjoy the readability of statically typed languages for other programmers to know what type a specific variable should be! To get the best of both worlds, Python 3.5 introduced Type Annotations.
Type Annotations are a new feature added in PEP 484 that allow for adding type hints to variables. They are used to inform someone reading the code what the type of a variable should be. This brings a sense of statically typed control to the dynamically typed Python. This is accomplished by adding
: <type> after initializing/declaring a variable.
An example is shown below, which adds the
: int when we declare the variable to show that age should be of type int.
age: int = 5 print(age) # 5
It is important to note that type annotations do not affect the runtime of the program in any way. These hints are ignored by the interpreter and are used solely to increase the readability for other programmers and yourself. They are not enforced are runtime.
A helpful feature of statically typed languages is that the value of a variable can always be known within a specific domain. For instance, we know string variables can only be strings, ints can only be ints, and so on. With dynamically typed languages, its basically anyone's guess as to what the value of a variable is or should be.
We can use the expected variable's type when writing and calling functions to ensure we are passing and using parameters correctly. If we pass a
str when the function expects an
int, then it most likely will not work in the way we expected.
Consider the following code below:
def mystery_combine(a, b, times): return (a + b) * times
We can see what that function is doing, but do we know what
times are supposed to be? Look at the following code, especially at the two lines where we call the
mystery_combine with different types of arguments. Observe each version's output, which is shown in the comments below each block.
# Our original function def mystery_combine(a, b, times): return (a + b) * times print(mystery_combine(2, 3, 4)) # 20 print(mystery_combine('Hello ', 'World! ', 4)) # Hello World! Hello World! Hello World! Hello World!
Hmm, based on what we pass the function, we get two totally different results. With integers we get some nice PEMDAS math, but when we pass strings to the function, we can see that the first two arguments are concatenated, and that resulting string is multiplied
It turns out that the developer who wrote the function actually anticipated the second version to be the use case of
mystery_combine! Using type annotations, we can clear up this confusion.
def mystery_combine(a: str, b: str, times: int) -> str: return (a + b) * times
We have added
: str, and
: int to the function's parameters to show what types they should be. This will hopefully make the code clearer to read, and reveal it's purpose a little more.
We also added the
-> str to show that this function will return a
-> <type>, we can more easily show the return value types of any function or method, to avoid confusion by future developers!
Again, we can still call our code in the first, incorrect way, but hopefully with a good review, a programmer will see that they are using the function in a way it was not intended. Type annotations and hints are incredibly useful for teams and multi-developer Python applications. It removes most of the guesswork from reading code!
The previous section handles many of the basic use cases of type annotations, but nothing is ever just basic, so let's break down some more complex cases.
For anything more than a primitive in Python, use the
typing class. It describes types to type annotate any variable of any type. It comes preloaded with type annotations such as
Set, and more! Then you can expand your type hints into use cases like the example below.
from typing import List def print_names(names: List[str]) -> None: for student in names: print(student)
This will tell the reader that
names should be a list of strings. Dictionaries work in a similar fashion.
from typing import Dict def print_name_and_grade(grades: Dict[str, float]) -> None: for student, grade in grades.items(): print(student, grade)
Dict[str, float] type hint tells us that
grades should be a dictionary where the keys are strings and the values are floats.
The other complex examples work using this
If you want to work with custom type names, you can use type aliases. Let's say you are working with a group of [x, y] points as Tuples, then we could use an alias to map the
Tuple type to a
from typing import List, Tuple # Declare a point type annotation using a tuple of ints of [x, y] Point = Tuple[int, int] # Create a function designed to take in a list of Points def print_points(points: List[Point]): for point in points: print("X:", point, " Y:", point)
If your function returns multiple values as a tuple, then just wrap the expected output as a
typing.Tuple[<type 1>, <type 2>, ...]
from typing import Tuple def get_api_response() -> Tuple[int, int]: successes, errors = ... # Some API call return successes, errors
The code above returns a tuple of the number of successes and errors from the API call, where both values are integers. By using
Tuple[int, int], we are indicating to a developer reading this that the function does in fact return multiple
If your function has a value that can take on a different number of forms, you can use the
Optional when the value will be be either of a certain type or
from typing import Optional def try_to_print(some_num: Optional[int]): if some_num: print(some_num) else: print('Value was None!')
The above code indicates that
some_num can eith be of type
Union when the value can take on more specific types.
from typing import Union def print_grade(grade: Union[int, str]): if isinstance(grade, str): print(grade + ' percent') else: print(str(grade) + '%')
The above code indicates that
grade can eith be of type
str. This is helpful in our example of printing grades, so that we can print either 98% or Ninety Eight Percent, with no unexpected consequences.
For more examples, check out the official Python documentation for the
typing module. They have a ton of different variations of examples that you can check out. I just hit the tip of the ice berg here, but hopefully I have peaked your interest in making cleaner, easier to read code using type annotations in Python.
As always, please reach out, like, comment, or share if you have any comments or questions!