# Introduction to Mypy

[Mypy](https://mypy-lang.org/) is a powerful, fast, and feature-rich static type checker designed to work with Python. It helps enforce code quality, catch type-related errors early, and boost productivity by analyzing your code’s type hints.

This guide will show you how to configure and use Mypy in your Python projects.


## Prerequisites

Before you start with Mypy, make sure you have:

- A recent version of [Python](https://www.python.org/downloads/) installed (Python 3.13 or higher is recommended).
- Basic knowledge of Python type annotations.


## Step 1 — Setting up the project directory

In this section, you will create a new project directory, set up a virtual environment, and install Mypy within that environment. 

First, create a new directory for your project and navigate into it:

```command
mkdir mypy-demo && cd mypy-demo
```

Next, create and activate a virtual environment so that any packages you install don’t affect other Python projects on your system:

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

Once your virtual environment is active, install Mypy using `pip`, the standard Python package manager:

```command
pip install mypy
```

This will install Mypy as a Python package within the virtual environment, making it available for static type checking in your project. You can confirm that Mypy is installed correctly by running:

```command
mypy --version
```

This command will print the installed version number if everything is set up correctly, indicating that Mypy is ready to analyze your Python code. Here is an example of what you might see:

```text
[output]
mypy 1.15.0 (compiled: yes)
```

Now that Mypy is installed, you can continue configuring it for your project and start taking advantage of its static type-checking capabilities.



## Step 2 — Getting started with Mypy

Now that Mypy is installed, you can see how it checks Python code. In this step, you will create a simple Python script, introduce a type mismatch, and use Mypy to detect the error.

Begin by creating a directory named `src` in your project and then add a file called `main.py`:

```python
[label src/main.py]
def greet(name: str) -> str:
    return "Hello, " + name

print(greet("Mypy"))
print(greet(42))  # This should trigger a type error
```

Here, `greet` is a function that expects a `str` parameter. Passing an integer (`42`) violates the type hint, which Mypy will flag as a mistake.

To run Mypy on the `src` folder, execute the following command from your project’s root directory:

```command
mypy src
```

You should see an error that indicates a type conflict, for example:

```text
[output]
src/main.py:6: error: Argument 1 to "greet" has incompatible type "int"; expected "str"  [arg-type]
Found 1 error in 1 file (checked 1 source file)
```
Here, Mypy catches the mismatch between `str` and `int`. 

In the next step, you’ll learn how to configure Mypy more precisely to suit your project’s needs.


## Step 3 — Configuring Mypy
In this step, you'll learn how to customize Mypy's behavior using a configuration file. you'll start with the most essential settings that provide a good balance between type safety and practicality.

Begin by creating a file named `mypy.ini` in your project’s root directory. This file lets you customize how Mypy checks your code. 

Add the following code to the file:

```python
[label mypy.ini]
[mypy]
python_version = 3.13
disallow_untyped_defs = True
check_untyped_defs = True
strict_optional = True
```

The `python_version` indicates which Python version to use, `disallow_untyped_defs` requires function annotations, `check_untyped_defs` checks unannotated function bodies, and `strict_optional` handles `Optional` and `None` carefully.


Next, update the `main.py` file with the following code to see these checks in action:

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

def add_numbers(numbers: List[float]) -> float:
    return sum(numbers)

# This function currently triggers a type-check error because it's unannotated.
def untyped_function(x):
    return x + 1

def get_first_number(numbers: List[float]) -> Optional[float]:
    return numbers[0] if numbers else None
```

When you run Mypy with:

```command
mypy src
```

You’ll see an error that highlights `untyped_function` is missing an annotation:

```text
[output]
src/main.py:9: error: Function is missing a type annotation  [no-untyped-def]
Found 1 error in 1 file (checked 1 source file)
```

To fix this error, add type annotations to `untyped_function`:

```python
[label src/main.py]
...
[highlight]
def untyped_function(x: float) -> float:
    return x + 1
[/highlight]

def get_first_number(numbers: List[float]) -> Optional[float]:
    return numbers[0] if numbers else None

```

Run Mypy again:

```command
mypy src
```

This time, you should see a success message with no errors:

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

With this change, your code passes Mypy's checks. As your project grows, feel free to [explore additional settings and strictness](https://mypy.readthedocs.io/en/stable/config_file.html) levels in your `mypy.ini` to enforce even tighter type safety.


## Step 4 — Using advanced type hints with Mypy

After configuring Mypy for basic checks, you can deepen your code’s safety by using `Union` and `Optional` from the `typing` module. These annotations help you manage variables that can take multiple types or might be `None`, ensuring Mypy flags potential type errors upfront.

### Handling multiple types with `Union`

Sometimes, a function parameter can accept two or more different types. By annotating it with `Union`, you inform Mypy that multiple types are valid and prompt it to check each case correctly.

Update your `main.py` to include a function that takes either an integer or a string:

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

def add_numbers(numbers: List[float]) -> float:
    return sum(numbers)

def process_data(data: Union[int, str]) -> str:
    return data.upper()  # Will trigger an error if data is int

print(process_data("Hello"))
print(process_data(42))  # Mypy will flag calling .upper() on int
```

Running Mypy will show an error:

```command
mypy src
```

```text
[output]
src/main.py:9: error: Item "int" of "int | str" has no attribute "upper"  [union-attr]
Found 1 error in 1 file (checked 1 source file)
```

To fix this, distinguish between integer and string cases:

```python
[label src/main.py]
...
[highlight]
def process_data(data: Union[int, str]) -> str:
    if isinstance(data, int):
        return f"Number: {data}"
    return data.upper()
[/highlight]

print(process_data("Hello"))
print(process_data(42))
```

### Handling `None` with `Optional`

A function or variable may sometimes yield a valid type or `None`. Using `Optional` clarifies that `None` is possible and warns you if you treat a `None` as a valid object.

Add a function returning `Optional[str]` to illustrate:

```python
[label src/main.py]
from typing import Optional

def maybe_uppercase(flag: bool) -> Optional[str]:
    if flag:
        return "Hello, Optional!"
    return None

val = maybe_uppercase(False)
print(val.upper())  # Will trigger an error if val is None
```

Running Mypy again highlights the issue:

```text
[output]
src/main.py:11: error: Item "None" of "str | None" has no attribute "upper"  [union-attr]
Found 1 error in 1 file (checked 1 source file)
```

To resolve this, check for `None` before calling `upper()` at the end of the file:

```python
[label src/main.py]
....
val = maybe_uppercase(False)
if val is not None:
    print(val.upper())
else:
    print("No string to uppercase")
```

When you run Mypy now, you should see:

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

Explicitly handling each type scenario—whether multiple types through `Union` or the possibility of `None` through `Optional`—allows Mypy to catch subtle bugs.

 This approach keeps your code clear, robust, and safer for future maintenance.

## Step 5 — Using type guards and type narrowing

In the previous steps, you focused on how Mypy checks basic type hints and handles unions. In this step, you’ll refine those checks by leveraging type guards and type narrowing. 

These features help Mypy understand exactly when your code transitions from one type to another, ensuring more accurate error detection.

Begin by updating your `main.py` to explore the difference between calling a method on a union type outright and safely narrowing its type with `isinstance`:

```python
[label src/main.py]
from typing import Union

def process_data(data: Union[int, str]) -> str:
    # Attempting to call 'upper()' on every input triggers a type error
    return data.upper()

print(process_data("hello"))
print(process_data(42))  # Mypy will complain here because '42' is int
```

When you run Mypy you will see an error message like this:


```command
mypy src
```

```text
[output]
src/main.py:6: error: Item "int" of "int | str" has no attribute "upper"  [union-attr]
Found 1 error in 1 file (checked 1 source file)
```

Mypy detects that `data` might be an integer, which lacks the `upper()` method. To resolve this, you need to **narrow the type** at runtime using `isinstance`:


```python
[label src/main.py]
from typing import Union

[highlight]
def process_data(data: Union[int, str]) -> str:
    if isinstance(data, str):
        return data.upper()
    return f"Number: {data}"
[/highlight]

print(process_data("hello"))  # "HELLO"
print(process_data(42))       # "Number: 42"
```

Run Mypy again:

```command
mypy src
```

You should now see this:

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

By using `isinstance`, you **guide Mypy’s type inference** and ensure that operations are performed on the correct types. This same principle applies when working with collections containing multiple types.


### Using a custom type guard

For more detailed checks, especially when you need to confirm something more complex than a basic `isinstance`, Python 3.10+ allows you to write type guard functions. 

A type guard function returns `True` only if its input is guaranteed to be a particular subtype, and you specify that subtype using `TypeGuard`.

In the following example, `is_alpha_string` checks if `data` is a string containing only alphabetic characters. When `is_alpha_string(data)` returns `True`, Mypy treats `data` as a fully alphabetic string:

```python
[label src/main.py]
from typing import Union, TypeGuard

def process_data(data: Union[int, str]) -> str:
    if isinstance(data, str):
        return data.upper()
    return f"Number: {data}"

def is_alpha_string(data: Union[str, int]) -> TypeGuard[str]:
    return isinstance(data, str) and data.isalpha()

def process_alpha(data: Union[int, str]) -> str:
    if is_alpha_string(data):
        # Mypy knows 'data' is an alphabetic string here
        return f"Alphabetic string: {data.upper()}"
    elif isinstance(data, int):
        return f"Number: {data}"
    # Mypy now knows 'data' is a string with non-alphabetic characters
    return f"Non-alpha string: {data}"

print(process_alpha("Hello"))      # "Alphabetic string: HELLO"
print(process_alpha("Hello123"))   # "Non-alpha string: Hello123"
print(process_alpha(123))          # "Number: 123"
```

When you run:

```command
mypy src
```
```text
[output]
Success: no issues found in 1 source file
```

Mypy concludes that within the `if is_alpha_string(data):` block, `data` must be an alphabetic string. You don’t need any extra checks, and any attempts to misuse `data` in that block will be flagged before runtime.

Type guards and type narrowing allow Mypy to track your code’s logic with greater precision. You reduce the risk of calling invalid methods on your data by specifying exactly when a union type transitions to a narrower subtype. 

As your codebase expands, these features become vital to maintaining clarity and catching subtle bugs early.

## Step 6 — Using generic types with Mypy

After learning about type guards and narrowing, the next step is understanding generic types. 

Generics allow you to write flexible code that works with different types while maintaining type safety. They're especially useful when writing classes and functions that should work with various data types.

Begin by updating your `main.py` with a basic example of a container class without proper generic typing:

```python
[label src/main.py]
class Container:
    def __init__(self, item):
        self.item = item
    
    def get_item(self):
        return self.item

# These work, but Mypy can't verify type safety
str_container = Container("hello")
int_container = Container(42)

print(str_container.get_item().upper())  
print(int_container.get_item() + 1)      
```

Running Mypy shows errors because our functions lack type annotations:

```command
mypy src
```

```text
[output]

src/main.py:2: error: Function is missing a type annotation  [no-untyped-def]
src/main.py:5: error: Function is missing a return type annotation  [no-untyped-def]
Found 2 errors in 1 file (checked 1 source file)
```

Python itself won’t break if you pass any type to this `Container`, you won’t see a runtime error here. However, Mypy cannot verify type safety because it cannot know which type each container should hold. 

To fix this, you need to declare the class as generic:


```python
[label src/main.py]
[highlight]
from typing import TypeVar, Generic

T = TypeVar('T')

class Container(Generic[T]):
    def __init__(self, item: T) -> None:
        self.item = item
    
    def get_item(self) -> T:
        return self.item

# Now Mypy tracks types properly
str_container: Container[str] = Container("hello")
int_container: Container[int] = Container(42)
[/highlight]

print(str_container.get_item().upper())
print(str_container.get_item() + 1)
```
Here, you introduce a `TypeVar` named `T` and make `Container` inherit from `Generic[T]`.  Mypy now knows that each container instance holds a consistent type—either `str`, `int`, or another type you specify. 

With this change, Mypy can enforce that `string_container` is strictly a `Container[str]` and `integer_container` is strictly a `Container[int]`. 

Running Mypy now shows no errors:

```command
mypy src
```

```text
[output]
Success: no issues found in 1 source file
```
The generic version fixes our typing issues by using `TypeVar` as a placeholder type and `Generic[T]` to make our class work with any type. By annotating our methods with `T`, Mypy can track the specific type used in each container instance, ensuring type-safe operations throughout your code.


## Step 7 — Checking type coverage with Mypy

After exploring advanced type hints and generics, you might wonder how thoroughly your project is annotated. Mypy includes a coverage reporting feature that helps identify which parts of your code are fully typed and which need attention. 

This is particularly valuable for larger projects where you want to gradually improve type coverage.

Begin by updating your `main.py` with a mix of typed and untyped functions:

```python
[label src/main.py]
from typing import Optional

def typed_greet(name: str) -> str:
    return f"Hello, {name}"

def partially_typed_function(flag) -> Optional[str]:
    if flag:
        return "I have a return type but no parameter type"
    return None

print(typed_greet("Mypy Coverage"))
print(partially_typed_function(False))
```

Here, `typed_greet` has complete type annotations, while `partially_typed_function` is missing its parameter type. Running Mypy shows:

```command
mypy src
```

```text
[output]
src/main.py:8: error: Function is missing a type annotation for one or more arguments  [no-untyped-def]
Found 1 error in 1 file (checked 1 source file)
```

You can generate an HTML report to get a more detailed view of type coverage. First, install the required dependency:


```command
pip install lxml
```
Then, run Mypy with the `--html-report` flag:

```command
mypy --html-report mypy_coverage src
```

This generates a `mypy_coverage` directory containing an HTML report. The `index.html` file provides an overview of type coverage across all modules, and Mypy links to it in the output:

```text
[output]
src/main.py:8: error: Function is missing a type annotation for one or more arguments  [no-untyped-def]
Generated HTML report (via XSLT):
/Users/stanley/mypy-demo/mypy_coverage/index.html
Found 1 error in 1 file (checked 1 source file)
```

Opening this file in a browser displays a breakdown of type annotations and any missing hints:

![Screenshot of the HTML coverage](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c554de62-9147-4a07-7c04-5efdaaa76e00/md1x =3248x1994)

To resolve the issue, update `partially_typed_function` by adding the missing parameter type annotation:

```python
[label src/main.py]
from typing import Optional

def typed_greet(name: str) -> str:
    return f"Hello, {name}"
[highlight]
def partially_typed_function(flag: bool) -> Optional[str]:
[/highlight]
    if flag:
        return "Now I am fully typed!"
    return None
...
```

Generate a new coverage report:

```command
mypy --html-report mypy_coverage src
```
With all functions fully annotated, Mypy confirms there are no issues:

```text
[output]
Generated HTML report (via XSLT): /Users/stanley/mypy-demo/mypy_coverage/index.html
Success: no issues found in 1 source file
```

Opening the updated report in the browser reflects the improved type coverage:

![Screenshot 2025-02-21 at 6.14.52 PM.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/40c0a66d-f616-4bf5-229a-7c0f0ba5e800/md2x =3248x1994)

Regularly checking type coverage ensures consistency across your codebase, making it easier to catch type-related issues early and maintain a high level of type safety.

## Final Thoughts

This guide has shown how Mypy enhances code clarity, prevents runtime errors, and enforces consistency in your projects. 

There’s still much more to explore. The [official Mypy documentation](https://mypy.readthedocs.io/en/stable/) explores advanced topics like custom type stubs, strict mode settings, and handling dynamic code. 


Thanks for reading!