Python type hints, while appearing to be just annotations, fundamentally shift how Python code is analyzed, enabling a form of static verification that was previously impossible.

Let’s see this in action. Imagine a simple function that adds two numbers:

def add(a, b):
    return a + b

result = add(5, "hello")
print(result)

Without type hints, this code runs fine in Python, producing "5hello". However, this is likely not the intended behavior for an add function. Now, let’s add type hints:

def add(a: int, b: int) -> int:
    return a + b

result = add(5, "hello")
print(result)

This is where mypy, a static type checker for Python, comes in. When you run mypy on this file, it will flag an error:

main.py:4: error: Argument 2 to "add" has incompatible type "str"; expected "int"

mypy caught the mismatch between the expected int for b and the provided str. This is the core power: catching logic errors before runtime.

The Mental Model: Beyond Syntax Sugar

Type hints aren’t just for readability; they are a formal contract. When you write a: int, you’re not just telling another developer that a should be an integer, you’re telling mypy that a must be an integer for the code to be considered type-correct. mypy then uses this information to trace the flow of data and identify potential type violations across your entire codebase.

The -> int part specifies the return type. This allows mypy to check if the value being returned by the function actually conforms to the declared return type. If add were to return "result" instead of a + b, mypy would also catch that.

This system allows for:

  • Early Error Detection: Catching type-related bugs (like passing a string where an integer is expected) before they manifest at runtime, saving debugging time.
  • Improved Code Maintainability: Type hints make complex codebases easier to understand and refactor. When you change a function signature, mypy will immediately show you all the places that need updating.
  • Enhanced Tooling: IDEs leverage type hints for better autocompletion, inline error checking, and intelligent code navigation.

The basic types are straightforward: int, float, str, bool, list, dict, tuple, set. But the real power comes with more complex types from the typing module:

  • List[int]: A list where all elements are integers.
  • Dict[str, int]: A dictionary where keys are strings and values are integers.
  • Tuple[int, str, float]: A tuple with a specific sequence of types.
  • Optional[str]: A string or None.
  • Union[int, str]: An integer or a string.
  • Any: Bypasses type checking for a specific variable or parameter. Use sparingly.

Consider a function that processes a list of users, where each user is a dictionary with a name (string) and age (integer):

from typing import List, Dict, TypedDict

class User(TypedDict):
    name: str
    age: int

def process_users(users: List[User]) -> List[str]:
    names = []
    for user in users:
        # mypy checks if user['name'] is a str and user['age'] is an int
        # and that the returned value is a str
        names.append(user['name'].upper())
    return names

users_data = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
]

processed_names = process_users(users_data)
print(processed_names)

Here, TypedDict provides a way to define dictionary structures with specific keys and value types, making the User type explicit. If you were to pass {"name": "Charlie", "age": "twenty"} to process_users, mypy would flag the age as an incorrect type.

The fundamental mechanism mypy uses is type inference and type checking. It builds a representation of your code’s types and then walks through it, ensuring that every operation is valid according to those types. It doesn’t execute your code; it analyzes the static structure. This is why it’s called "static" type checking.

When you define a generic type like List[int], you are telling mypy that this list should only contain integers. If you later try to append a string to this list, mypy will raise an error. This strictness is what prevents many common runtime bugs.

The next frontier in leveraging type hints is understanding how they interact with asynchronous code and complex inheritance hierarchies.

Want structured learning?

Take the full Python course →