In this post you will learn some best practices for creating readable Python code. Specifically, you will learn how to use type hints to allow others know what your functions, classes, and variables expect as inputs and outputs. By the end you will:

  • Learn how write readable/usable functions with type hints
  • Learn how to make custom type hints to increase readability
  • Learn some advanced type hint options

Prerequisites:

  • Basic/working knowledge of how to write python code (i.e., know how to write a function).

What are Type Hints?

Type hints are a python tool that can be used improve readability by listing the expectations for code input and output for functions and classes. With type hints you specify the type each argument requires/expects as well as the expected output type.

def add(a: float, b: float=3.1) -> float:
    return(a + b)

In this simiple example, you see that both arguments expect float values and the second argument even has a default value of 3.1. Additionally, the expected output is also a float. As you can see the syntax is pretty simple. After a parameter, just add a : and then the type hint to use. If you want to have a default value, just use an = as per usually. And finally, you use an -> between the closing ) and the final : to indicate the return type hint.

The image below shows how linters use type hints to help you when writing code. Since I included the int type hint for my function’s value parameter, when I use the function with a str, it gives me the linting warning. As you can imagine, this can help a ton for improving the usability of your code.

Linter Example with Type Hints

Type Hint Options

Importantly, python does not actually check to confirm the types are as hinted. So, you will still need to raise the appropriate exception, if necessary. This is only to make it easier to understand how to properly use the script and/or function. And it will dramatically improve your ability to develop and use the code when you have a linter (when you IDE tells you when you have coding mistakes). You can use all the primative types for hints:

  • int
  • str
  • float
  • bool
  • list[int or str or float] ([] indicates what type are expected inside the list)
  • dict[str, str] (for a dictionary with both the key and value as strings)
  • tuple[str, int] (for a tuple of size 2 with a string then int)
  • set

As of python 3.9+ you can use the list, dict, tuple, and set natively. However, previous versions of python require you to import them from the typing module (e.g., from typing impor Dict). Additionally for all these sequences, you can specify what type are expected inside the sequence with the [] notation.

Note: For all type hints that are imported, you need to change the type hint to capitalize the first letter when using them in functions.

Basic Usage

As shown in previous examples, type hints have a consistent structure: VAR_NAME: TYPE=DEFAULT. So, you can include them inside of functions and classes, to really improve readability. For classes, they should go before the __init__ function for the class.

class Player():
    attack: int  # int based attack variable without default
    health: int=100  # Int health variable with default of 100

    def __init__(self) -> None:
        pass

Here we made a Player class with some attributes (attack and health), where health also has a default value. Although they are not very common (and far less helpful, except for constants at the top of a module), you can actually, use type hints for standard variables, and as you can see, they have the exact same structure as with functions and classes.

NUMBER: int = 69
print(NUMBER)

To get the most out of type hints they should be as precise as you can make them. So don’t use int, when there are really only a few options. Instead make your own custom type hints, to be more precise.

Custom Type Hints Example

When creating type hints, it is important that you are as specific as possible, otherwise they are not that useful. For example, it would be a bad idea to have a str type hint if it really could only be BEGINNER, INTERMEDIATE, and ADVANCED. Instead you should create you own custom type hint. Luckily, the Enum function makes this incredibly easy. You just need to create a class, which inherits from Enum and contains all the options for your specific use case. For example,

from enum import Enum

class Difficulty(Enum):
    # You can also assign a specific value if needed.
    #     E.g., BEGINNER = 1 # or 'b'
    BEGINNER = auto()
    INTERMEDIATE = auto()
    ADVANCED = auto()

def get_difficulty(diff: Difficulty) -> Difficulty:
    return(diff.name)

result = get_difficulty(Difficulty.ADVANCED)
print(result)  # 👉️ ADVANCED

This example, is much better than a type hint of str because now you know exactly what options are expected. Get into the habit of making your type hints as specific as possible.

Complex Type Hints

There are also instances where you might need some more complex type hints, the most common example would be and optional type hint. This can be done several different ways. The example below shows three different syntaxes that all produce the same result, an x variable that is optionally, either a str or None (here we also assign the variable, but of course that is not required).

from typing import Union, Optional

condition = True
x: str | None = "test" if condition else None
x: Union[str, None] = "test" if condition else None
x: Optional[str] = "test" if condition else None

There are lots of other obscure one off situations for type hints and they pretty much all have a sitaution, but it is likely not useful for you to have examples for each. Instead, I will just highlight a few imports you they you may come across.

  • If you need to type hint a function use Callable from the typing module
  • If you need either a list, tuple, etc. You can import Sequence

In some situations, you may want to specifically get quick access to the value of the Enum for those situations you can use this:

# Setting default comparison behavior to be the value
class Key(Enum):
    RIGHT_ARROW = 83
    LEFT_ARROW = 81

    def __eq__(self, other):
        if isinstance(other, Key):
            return super().__eq__(other)
        return self.value == other

This dunder method (__eq__) will allow you to use the shorthand Key.RIGHT_ARROW instead of Key.RIGHT_ARROW.value. This is specific to comparisons and the printing behavior will be the same.

Recap

As you can see, type hints make your code much more clear as to what each function/class expects as inputs. Using them can help others (including your future self after you forgot this project) understand what your functions need to work. By including them in your code, you will significantly increase your codes readability.

Of course you can do some more more complicated things with type hints, but we will save that for another time. Here I just wanted to give you some general exposure to the concept of type hints and how they can be used to increase code readability and usability.

Additional Resources

For more information about type hints such as other use cases, using special type hints, or using custom type hints see this post.