Mypy 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 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:
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:
python3 -m venv venv
source venv/bin/activate
Once your virtual environment is active, install Mypy using pip
, the standard Python package manager:
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:
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:
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
:
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:
mypy src
You should see an error that indicates a type conflict, for example:
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:
[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:
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:
mypy src
You’ll see an error that highlights untyped_function
is missing an annotation:
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
:
...
def untyped_function(x: float) -> float:
return x + 1
def get_first_number(numbers: List[float]) -> Optional[float]:
return numbers[0] if numbers else None
Run Mypy again:
mypy src
This time, you should see a success message with no errors:
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 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:
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:
mypy src
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:
...
def process_data(data: Union[int, str]) -> str:
if isinstance(data, int):
return f"Number: {data}"
return data.upper()
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:
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:
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:
....
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:
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
:
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:
mypy src
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
:
from typing import Union
def process_data(data: Union[int, str]) -> str:
if isinstance(data, str):
return data.upper()
return f"Number: {data}"
print(process_data("hello")) # "HELLO"
print(process_data(42)) # "Number: 42"
Run Mypy again:
mypy src
You should now see this:
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:
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:
mypy src
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:
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:
mypy src
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:
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)
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:
mypy src
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:
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:
mypy src
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:
pip install lxml
Then, run Mypy with the --html-report
flag:
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:
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:
To resolve the issue, update partially_typed_function
by adding the missing parameter type annotation:
from typing import Optional
def typed_greet(name: str) -> str:
return f"Hello, {name}"
def partially_typed_function(flag: bool) -> Optional[str]:
if flag:
return "Now I am fully typed!"
return None
...
Generate a new coverage report:
mypy --html-report mypy_coverage src
With all functions fully annotated, Mypy confirms there are no issues:
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:
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 explores advanced topics like custom type stubs, strict mode settings, and handling dynamic code.
Thanks for reading!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github