Using Python's Type Annotations
Tags: python, tutorial, beginners, learning
Published: 2018-08-18 16:02:55

---
title: Using Python's Type Annotations
published: true
description: Python is known for being a Wild West language where anything goes. Indentation aside, code style and documentation are mostly left to the developer's opinion writing the application.
tags: Python, tutorial, beginner, learning
cover_image: https://images.unsplash.com/photo-1515879218367-8466d910aaa4?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=8bafd14605a87addc5db927cf3dc0587
---

Python is known for being a Wild West language where anything goes. Indentation aside, code style and documentation are mostly left to the developer's opinion writing the application, which can lead to some messy, unreadable code.

This vague styling structure comes partially from Python being a *dynamic typed* language, meaning that types are associated with the variable's value at a point in time, not the variable itself. This language attribute means that variables can take on any value at any point and are only [type checked](https://en.wikipedia.org/wiki/Type_system#Dynamic_type_checking_and_runtime_type_information) when an attribute or method is accessed. 

Consider the following code. In Python, this is acceptable.

```python
age = 21
print(age)  # 21
age = 'Twenty One'
print(age)  # Twenty One
```

In the code above, the value of `age` is first an `int` (integer), but then we change it to a `str` (string) later on. Every variable can represent any value at any point in the program. That is the power of dynamic typing!

Let's do the same thing in a *statically typed* language, like Java.

```java
int age = 21;
System.out.print(age);
age = "Twenty One";
System.out.print(age);
```

We 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 `int`.

```
Error: incompatible types: String cannot be converted to int
```

To work in a statically typed language, we would have to use two separate variables and use some assistive type-conversion method, such as the standard `toString()` method.

```java
int ageNum = 21;
System.out.print(ageNum);
String ageStr = ageNum.toString();
System.out.print(ageStr);
```

This conversion works, but I really like the flexibility of Python, and I don't want to sacrifice its positive attributes as a dynamic, readable, and beginner-friendly language just because types are difficult to reason about in most cases. With this said, I also enjoy the readability of statically typed languages for other programmers to know what type a specific variable should be! So, to get the best of both worlds, Python 3.5 introduced **type annotations**.

## What Are Type Annotations?

Type Annotating is a new feature added in [PEP 484](https://www.python.org/dev/peps/pep-0484/) that allows adding *type hints* to variables. They are used to inform someone reading the code what the type of a variable *should be* expected. This hinting brings a sense of statically typed control to the dynamically typed Python. This is accomplished by adding a given type declaration after initializing/declaring a variable or method.

## Why & How to Use Type Annotations
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.

### Annotating Variables

When annotating variables, it can be defined in the form

```python
my_var: <type> = <value>
```

to create a variable named `my_var` of the given type `<type>` with the given value.

An example is shown below, which adds the `: int` when we declare the variable to show that age should be of type *int*.

```python
age: int = 5
print(age)
# 5
```

It is important to note that **type annotations do not affect the program's runtime in any way**. These hints are ignored by the interpreter and are solely used to increase the readability for other programmers and yourself. But again, these type hints are not enforced are runtime, so it is still up to the caller method/function/block to ensure proper types are used.

## Annotating Functions & Methods

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:

```python
def mystery_combine(a, b, times):
    return (a + b) * times
```

We can see *what* that function is doing, but do we know what `a`, `b`, or `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.

```python
# 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](https://xkcd.com/992/) 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 `times` times. 

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.

```python
def mystery_combine(a: str, b: str, times: int) -> str:
    return (a + b) * times
```

We have added `: str`, `: 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 `str`. Using `-> <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!

We can extend this one step further to handle default argument values. We have adapted `mystery_combine` below to use `2` as the default argument value of the `times` parameter. This default value gets placed after the type hint.

```python
def mystery_combine(a: str, b: str, times: int = 2) -> str:
    return (a + b) * times
```

### Type Hints with Methods

Type hints work very similarly with methods, although it's pretty common - in my experience anyways - to leave off the type hint for `self`, since that is implied to be an instance of the containing class itself.

```python
class WordBuilder:

    suffix = 'World'

    def mystery_combine(self, a: str, times: int) -> str:
        return (a, self.suffix) * times
```

You can see above that the code is very similar to the previous function-based example, except we have dropped the `b` parameter for a `suffix` attribute that is on the `WordBuilder` class. Note that we don't need to explicitly add `: str` to the `suffix` definition because most code editors will look at the default value for the expected type.

---

## Available Types

The previous section handles many basic use cases of type annotations, but nothing is ever *just* basic, so let's break down some more complex cases and show the common types.

### Basic Types
The most basic way to annotate objects is with the class types themselves. You can provide anything that satisfies a `type` in Python.

```python
# Built-in class examples
an_int: int = 3
a_float: float = 1.23
a_str: str = 'Hello'
a_bool: bool = False
a_list: list = [1, 2, 3]
a_set: set = set([1, 2, 3])  # or {1, 2, 3}
a_dict: dict = {'a': 1, 'b': 2}

# Works with defined classes as well
class SomeClass:
    pass

instance: SomeClass = SomeClass()
```

### Complex Types

Use the [`typing`](https://docs.python.org/3/library/typing.html) module for anything more than a primitive in Python. It describes types to hint *any* variable of *any* type more detailed. It comes preloaded with type annotations such as `Dict`, `Tuple`, `List`, `Set`, and more! In the example above, we have a `list`-hinted variable, but nothing defines what should be _in_ that list. The typing containers provided by the `typing` module allow us to specify the desired types more correctly.

Then you can expand your type hints into use cases like the example below.

```python
from typing import Sequence

def print_names(names: Sequence[str]) -> None:
    for student in names:
        print(student)
```

This will tell the reader that `names` should be a [`Sequence`](https://techvidvan.com/tutorials/python-sequences/) of `str`s, such as a `list`, `set`, or `tuple` of strings.

Dictionaries work in a similar fashion.

```python
from typing import Dict

def print_name_and_grade(grades: Dict[str, float]) -> None:
    for student, grade in grades.items():
        print(student, grade)
```

The `Dict[str, float]` type hint tells us that `grades` should be a dictionary where the keys are `str`ings and the values are `float`s.

### Type Aliases

If you want to work with custom type names, you can use *type aliases*. For example, let's say you are working with a group of `\[x, y\]` points as `Tuple`s, then we could use an alias to map the `Tuple` type to a `Point` type.

```python
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[0], "  Y:", point[1])
```

### Multiple Return Values

If your function returns multiple values as a tuple, wrap the expected output as a `typing.Tuple[<type 1>, <type 2>, ...]`

```python
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 return multiple `int` values.

### Multiple Possible Return Types

If your function has a value that can take on a different number of forms, you can use the `typing.Optional` or `typing.Union` types. 

Use `Optional` when the value will be be either of the given type *or* `None`, exclusively. 

```python
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 either be of type `int` or `None`.

Use `Union` when the value can take on more specific types.

```python
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 either be of type `int` or `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.

## Working with Dataclasses
[Dataclasses](https://docs.python.org/3/library/dataclasses.html) are a convenience class that provide automatically generated `__init__` and `__repr__` methods to an appropriate class. It reduces the amount of [boilerplate code](https://en.wikipedia.org/wiki/Boilerplate_code) needed to create new classes that take in multiple keyword arguments to their constructor. These dataclasses use type hints and class-level attribute definitions to determine what keyword arguments and associated values can be passed to `__init__` and printed by `__repr__`.

The following code is directly from the [`dataclasses` documentation](https://docs.python.org/3/library/dataclasses.html). It defines an `InventoryItem` that has three attributes defined on it, all using type hints; a `name`, `unit_price`, and `quantity_on_hand` .

```python
from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand
```

Using the type hints and `@dataclass` decorator, new `InventoryItem`s can be created with the following code, and the dataclass will take care of mapping the keyword arguments to attributes.

```python
common_item = InventoryItem(name='My Item', unit_price=2.99, quantity_on_hand=60)
other_item = InventoryItem(name='My Item', unit_price=2.99)  # uses default value of 10 quantity
```

An important note to `@dataclass`es is that _any_ class attribute defined with a default value **must** be declared after any attributes without a default value. This means `quantity_on_hand` has to be declared after `name` and `unit_price`. This can get interesting when working with dataclasses that extend from a parent dataclass, so be careful, but the Python interpreter should catch these issues for you.

### More Examples

For more examples, check out the [official Python documentation](https://docs.python.org/3/library/typing.html) 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 iceberg here, but hopefully, I have piqued 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!