Extending Static Typing with Mypy Plugins in Python


What is a Mypy Plugin?

A Mypy plugin is a mechanism that allows developers to extend or customize the behavior of the Mypy static type checker. While Mypy supports standard PEP 484-style Python type hints out of the box, some advanced use cases—like type-safe Django ORM models or custom data validation libraries—require deeper type inference than static annotations alone provide.

Mypy plugins enable integration with complex Python frameworks or libraries by providing hook functions that modify how Mypy interprets classes, functions, and expressions during type checking. These plugins are written in Python and imported dynamically when Mypy is executed.

In short, Mypy plugins allow framework-specific or library-specific typing intelligence, going beyond what standard static analysis can infer.


Major Use Cases of Mypy Plugins

Mypy plugins are useful when dealing with dynamic or meta-programmed Python code. Common use cases include:

1. Framework Integration

Frameworks like Django or SQLAlchemy use dynamic code generation (e.g., adding attributes at runtime), which static type checkers can’t analyze. Mypy plugins bridge this gap.

  • Example: The django-stubs plugin enables static typing for Django’s dynamically generated model fields.

2. Advanced Type Inference

Plugins can dynamically infer or alter the return type of a function or the attribute type of a class based on custom logic or decorators.

  • Example: A plugin might make a function return List[int] or List[str] depending on an argument value.

3. Library Customization

For libraries with heavy use of decorators, metaclasses, or custom descriptors, plugins allow for proper type resolution without modifying the source code.

  • Example: Libraries like attrs, pydantic, and marshmallow use plugins to provide type hints for generated fields.

4. Custom DSL or Domain Modeling

If you’re building a DSL (domain-specific language) or a configuration system using Python, a Mypy plugin can help enforce typing rules specific to your model.


How Mypy Plugin Works (Architecture Overview)

The Mypy plugin system hooks into Mypy’s semantic analysis and type checking phases. Here’s how it functions in simplified terms:

1. Mypy Core

Mypy loads Python modules and parses them into an Abstract Syntax Tree (AST).

2. Plugin Loader

If the mypy.ini or pyproject.toml config specifies a plugin, Mypy dynamically imports it and registers it with the type checker.

3. Hook Registration

The plugin implements hook functions like:

  • get_class_decorator_hook
  • get_function_hook
  • get_type_analyze_hook
  • get_dynamic_class_hook

These hooks are registered through the Plugin base class in mypy.plugin.

4. Type Manipulation

When Mypy encounters the relevant code (a function, class, or decorator), it calls the plugin’s hook function, which can:

  • Change the inferred type
  • Inject new type information
  • Modify behavior conditionally based on arguments

5. Type Checking Continuation

Once hooks return modified type information, Mypy continues checking the program using the adjusted data.


Basic Workflow of a Mypy Plugin

Here’s a high-level flow of how a Mypy plugin interacts during a type-checking run:

  1. Developer writes code using a dynamic framework or pattern.
  2. Plugin is registered in Mypy configuration.
  3. Mypy runs and encounters plugin-eligible code.
  4. Plugin hook is called, returning custom type logic.
  5. Mypy continues type checking with enhanced information.
  6. Errors or warnings are shown, if types do not match.

Step-by-Step Getting Started Guide for Mypy Plugins

Prerequisites

  • Python 3.8+
  • mypy installed (pip install mypy)
  • Basic familiarity with Python typing and static analysis

Step 1: Create a New Python Project

Set up your working directory:

mkdir mypy-plugin-demo && cd mypy-plugin-demo
python -m venv venv
source venv/bin/activate
pip install mypy

Step 2: Define Your Plugin File

Create myplugin.py:

from mypy.plugin import Plugin, FunctionContext
from mypy.types import Type

class MyCustomPlugin(Plugin):
    def get_function_hook(self, fullname: str):
        if fullname == "mymodule.dynamic_func":
            return self.analyze_dynamic_func
        return None

    def analyze_dynamic_func(self, ctx: FunctionContext) -> Type:
        # Override return type regardless of argument
        return ctx.api.named_generic_type("builtins.str", [])

def plugin(version: str):
    return MyCustomPlugin

Step 3: Write the Target Code to Be Typed

Create mymodule.py:

def dynamic_func(x):
    return x * 5

Create test.py:

from mymodule import dynamic_func

result = dynamic_func(10)
reveal_type(result)  # Mypy will report this as 'str' due to plugin

Step 4: Configure Mypy to Use Your Plugin

Create a mypy.ini file:

[mypy]
plugins = myplugin

Step 5: Run Mypy

mypy test.py

You should see:

test.py:4: note: Revealed type is "builtins.str"

Step 6: Expand with More Hooks

You can now add hooks for class decorators, attribute types, metaclasses, etc., to handle more complex typing cases.