# A Complete Guide to Python Type Hints

[`mypy`](https://github.com/python/mypy) is a tool that checks your Python code for type errors without running it. It's great for catching bugs early and keeping your code clean and easy to maintain. It's widely used in Python and is a common part of many professional projects.

Python's type hints give you everything you'd expect from a modern type system — support for basic types, lists and dictionaries, custom classes, generics, and more. One of the best things about it is how flexible it is. You can gradually add type hints and customize them to fit your project's small or large needs.

In this article, you'll learn how to use type hints in your Python code with the help of the `typing` module.

[ad-logs]

## Prerequisites

Before proceeding, ensure you have the latest version of Python installed on your machine. This article assumes you're familiar with basic Python programming concepts.

## Getting started with type hints

Type hints were introduced in Python 3.5 through [PEP 484](https://www.python.org/dev/peps/pep-0484/) and have evolved significantly since then. They let you indicate the expected types of variables, function parameters, and return values in your code.

To get started, create a new project directory and move into it:

```command
mkdir python-typing && cd python-typing
```

Next, set up and activate a virtual environment:

```command
python3 -m venv venv
```
```command
source venv/bin/activate
```

Then install `mypy` inside your virtual environment:

```command
pip install mypy
```

Create a simple script with type hints:

```python
[label main.py]
def greeting(name: str) -> str:
    return f"Hello, {name}!"

print(greeting("World"))
```

Run the program normally:

```command
python main.py
```

Output:

```text
[output]
Hello, World!
```

The program runs like any Python script. Type hints don't affect runtime behavior at all. Now check types with `mypy`:

```command
mypy main.py
```

Output:

```
[output]
Success: no issues found in 1 source file
```

This confirms your code aligns with the type annotations. Remember, Python still runs code with type errors, but `mypy` catches them before runtime, helping you avoid bugs.

## Basic types in Python

Python supports **optional static typing** using type hints, which help you catch bugs early and make your code easier to understand.

You can annotate variables and function arguments with basic types like `int`, `float`, `bool`, and `str`:

```python
# Primitive types
x: int = 1
y: float = 2.5
z: bool = True
name: str = "John"

# Simple function with type hints
def greeting(name: str) -> str:
    return f"Hello, {name}!"
```

For collections such as lists and dictionaries, use the `typing` module to specify the types of their contents:

```python
[label main.py]
from typing import List, Dict

# Specify collection contents
numbers: List[int] = [1, 2, 3]
user_ages: Dict[str, int] = {"John": 30, "Jane": 25}
```

Run `mypy` to check your types:

```command
mypy main.py
```

Output:

```
[output]
Success: no issues found in 1 source file
```

The following diagram illustrates how mypy checks your code for type errors before runtime, saving you from bugs that would otherwise only be caught during execution:

![diagram illustrating how mypy checks your code]
(https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c820644c-51ff-46c6-9f48-ae28de9d2000/md1x =1998x1202)



The real benefit of type hints is that they show up when mistakes are made. For example, if you accidentally add a string to your list of integers, Python won’t complain at runtime — but `mypy` will catch the issue before you even run the code.

### Type aliases

Type aliases can help improve code readability and maintainability when working with complex or nested data structures. 

Instead of repeating long type hints throughout your code, you can define aliases with meaningful names.


```python
[label main.py]
from typing import Dict, List, Tuple

# Type aliases
Point = Tuple[float, float]
Points = List[Point]
UserDatabase = Dict[str, Dict[str, str]]

def plot_points(points: Points) -> None:
    for point in points:
        x, y = point
        print(f"Point: ({x}, {y})")
```

Type aliases make your code cleaner, especially when dealing with nested structures. They also centralize type definitions, making updates easier.

For example, instead of repeating `List[Tuple[float, float]]` everywhere, you just use `Points`. This makes function signatures and annotations much easier to read, especially in large codebases.

To check the types, use the following command in your terminal:

```command
mypy main.py
```

This helps validate that your types are used correctly and can catch bugs early in development.

## Function annotations

Function annotations are the most common use case for type hints. 

They let you specify what types a function expects and what it returns, making your code more self-documenting and easier to understand for both humans and tools.

```python
[label main.py]
from typing import List, Optional, Union

def repeat_string(s: str, times: int) -> str:
    """Repeat a string multiple times."""
    return s * times

def get_first_item(items: List[int]) -> Optional[int]:
    """Return the first item or None if empty."""
    return items[0] if items else None

def process_value(value: Union[int, str]) -> str:
    """Process either an int or a string."""
    if isinstance(value, int):
        return f"Number: {value * 2}"
    else:
        return f"String: {value.upper()}"
```

These annotations tell readers and tools exactly what your functions expect and return. 

- `repeat_string` takes a `str` and an `int`, and returns a `str`.
- `Optional[int]` means the function might return an integer or `None` if the list is empty.
- `Union[int, str]` allows `process_value` to accept either an integer or a string.

Annotations also improve support from editors and IDEs (like autocomplete, static analysis, and refactoring). They're invaluable in larger codebases or teams, where clear function contracts reduce misunderstandings and bugs.


Let's see how `mypy` helps catch errors. Create deliberate type mistakes with the following example:

```python
[label main.py]
from typing import List

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

# Type error: string instead of int
result = add_numbers("5", 10)

def get_first_item(items: List[int]) -> int:
    return items[0]

# Type error: empty list access
empty_list: List[int] = []
first_item = get_first_item(empty_list)
```

If you run this with Python, the first line would concatenate strings instead of adding numbers, and the second would crash with an IndexError. But `mypy` catches both issues before runtime:

```command
mypy main.py
```

Output:

```text
[output]
main.py:9: error: Argument 1 to "add_numbers" has incompatible type "str"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)
```

In this case, `mypy` flags the incorrect argument type passed to `add_numbers`. It tells you that `"5"` is a string, but an `int` was expected. 

These are exactly the kinds of bugs that can slip through testing and cause problems in production. With static type checking, `mypy` helps you catch these issues early in development — before they turn into runtime errors.

## Optional and union types

Python's type system provides elegant ways to express variables that can have multiple types or be absent. This is especially useful when dealing with optional return values or inputs that can vary in type.

```python
from typing import Optional, Union

# Can return a string or None
def find_user(user_id: int) -> Optional[str]:
    user_database = {1: "John", 2: "Jane"}
    return user_database.get(user_id)

# Can accept either string or bytes
def process_input(data: Union[str, bytes]) -> str:
    if isinstance(data, bytes):
        return data.decode('utf-8')
    return data
```

`Optional[str]` is shorthand for `Union[str, None]`. It clearly communicates that the function may return a string — or nothing at all (`None`). This is common in functions that search, retrieve, or look up values that might not exist (e.g., from a dictionary or a database).

`Union` types are perfect for writing flexible functions accepting multiple input types. In the example above, `process_input` accepts either a string or a byte string (`str` or `bytes`). The function logic adapts based on the input type, handling both cases safely.

Using `Optional` and `Union` helps make your code:

- **More expressive**: Readers understand the range of valid inputs or outputs at a glance.
- **Safer**: Tools like `mypy` can alert you when you're not handling all possible cases (e.g., forgetting to check for `None`).
- **Cleaner**: You avoid long comments or confusing docstrings — the types speak for themselves.

These hints are compelling when combined with static analysis tools or in larger codebases where assumptions about types can easily lead to bugs.


## Generic types

Generics allow you to create reusable components that work with different types. Instead of writing separate versions of the same class or function for each data type, generics let you define a single implementation that can handle them all — safely and predictably.

```python
from typing import Generic, TypeVar, List

# Define a type variable
T = TypeVar('T')

# Create a generic class
class Stack(Generic[T]):
    def __init__(self) -> None:
        self.items: List[T] = []
        
    def push(self, item: T) -> None:
        self.items.append(item)
        
    def pop(self) -> T:
        if not self.items:
            raise IndexError("Pop from empty stack")
        return self.items.pop()
```

Usage:

```python
# Integer stack
int_stack = Stack[int]()
int_stack.push(1)

# String stack
str_stack = Stack[str]()
str_stack.push("hello")
```

In this example, `T` is a type variable — a placeholder that will be replaced by a concrete type when the class is used. When you write `Stack[int]`, you're telling Python (and type checkers like `mypy`) that this stack will only hold integers.

This lets you:

- Avoid code duplication: One implementation works for all types.
- Enforce type safety: `mypy` will raise an error if you try to push a string into a `Stack[int]`.
- Write flexible data structures: You can build things like generic lists, trees, caches, or queues — all type-safe and reusable.

Generics are widely used in libraries and frameworks, especially for building containers, APIs, and utilities that need to support many data types without sacrificing type checking.

They are also fully supported by tools like `mypy`, which can validate that your generic types are used consistently across your code.


## Gradual typing

You don't need to add types to your entire codebase at once. Python's type system supports **gradual typing**, which means you can start with a few critical functions and expand from there at your own pace.


![Screenshot showing partial vs. full type annotations](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/d3d38e1e-3374-40cc-77d5-3ddfe73ab200/md2x =1792x1222)

For example, take a look at this code:

```python
# Fully typed function
def get_user_data(user_id: int) -> dict:
    # ...
    return user_data

# Partially typed function
def process_data(data):
    # ...
    return processed_data
```

In this example, `get_user_data` has complete type annotations, which helps tools like `mypy` check that it's being used correctly. On the other hand, `process_data` has no annotations, so it's treated dynamically — just like traditional Python.

This approach is constructive when working with large or legacy codebases. You can begin by typing functions that are central to your application’s logic or are most prone to bugs. Over time, as you refactor or revisit modules, you can add more annotations and tighten up type checks.

Type checkers like `mypy` will analyze the parts of your code that include type hints, and skip over the rest. This allows you to benefit from type checking immediately, without requiring a complete rewrite.

To enforce a stricter typing policy as you go, you can run:

```command
mypy filename.py --disallow-untyped-defs
```

This flags any function that’s missing annotations, helping you identify where to improve type coverage next. Gradual typing lets you balance safety and flexibility, while progressively improving code clarity and reliability.


## Final thoughts 

This article has explored Python type hints in depth—from basic syntax to more advanced patterns. Type hints improve readability and maintainability and help catch bugs early in development.

You can start using type hints gradually in your existing code, configure `mypy` to fit your workflow, and explore tools like Pyright or Pyre for even more powerful checks.

For more details, see the [official Python typing documentation](https://docs.python.org/3/library/typing.html).

Thanks for reading — happy typing!