Skip to main content

How the plugin system works

The plugin system operates in several phases:
1
Collection
2
Plugins are collected from the mypy config file and imported using importlib.
3
Entry point
4
Each module must provide an entry point function (default name: plugin) that accepts a mypy version string and returns a Plugin subclass.
5
Constructor
6
All plugin constructors must accept an Options object and call super().__init__().
7
Hook calls
8
During semantic analysis and type checking, mypy calls get_xxx methods with fully qualified names.
9
First match
10
Plugins are called in config order. The first plugin returning non-None is used.
11
Callback execution
12
The callback receives context with current state and API access.

Semantic analyzer notes

The semantic analyzer processes code in a specific order:
Processing order:
  1. Module top levels (including class bodies)
  2. Functions and methods (after top levels complete)
  3. Import cycles are handled by processing all top levels before any functions

Key differences from old analyzer

  • Multiple passes: Each target can be processed multiple times if forward references aren’t ready
  • Deferred processing: anal_type() returns None if types aren’t available yet
  • Explicit deferral: Use defer() to request reprocessing
  • Final iteration: Check final_iteration property - never defer during final iteration
  • Placeholders: Handle PlaceholderNode objects for unprocessed definitions
Plugin hooks must be idempotent since they can be called multiple times for the same name.

Plugin hook reference

get_type_analyze_hook

Customize type analyzer behavior for specific types. Use case: Support types that aren’t expressible in standard Python typing.
from mypy.plugin import Plugin, AnalyzeTypeContext
from mypy.types import Type, Instance

class CustomPlugin(Plugin):
    def get_type_analyze_hook(self, fullname: str):
        if fullname == "mylib.Vector":
            return analyze_vector_type
        return None

def analyze_vector_type(ctx: AnalyzeTypeContext) -> Type:
    # Vector can have arbitrary number of type arguments
    # Vector[int, int] and Vector[int, int, int] are both valid
    if not ctx.type.args:
        return ctx.api.named_type('builtins.object', [])
    # Return appropriate Instance type
    return ctx.api.named_type('mylib.Vector', [])
Example usage:
from lib import Vector

a: Vector[int, int]
b: Vector[int, int, int]

get_function_hook

Adjust the return type of a function call. Use case: Infer complex return types that depend on argument values.
def get_function_hook(self, fullname: str):
    if fullname == "mylib.make_thing":
        return infer_make_thing_return_type
    return None

def infer_make_thing_return_type(ctx: FunctionContext) -> Type:
    # Access argument types and values
    if ctx.arg_types and ctx.arg_types[0]:
        arg_type = ctx.arg_types[0][0]
        # Return a more precise type based on arguments
        return ctx.api.named_generic_type('mylib.Thing', [arg_type])
    return ctx.default_return_type
This hook is also called for class instantiation (Class() calls).

get_function_signature_hook

Adjust the signature of a function before type checking. Use case: Customize parameter types based on context.
def get_function_signature_hook(self, fullname: str):
    if fullname == "mylib.flexible_func":
        return adjust_flexible_func_signature
    return None

def adjust_flexible_func_signature(ctx: FunctionSigContext) -> FunctionLike:
    # Modify the signature based on context
    return ctx.default_signature

get_method_hook

Adjust return type of a method call. Use case: Similar to get_function_hook but for methods.
def get_method_hook(self, fullname: str):
    if fullname == "mylib.Container.get":
        return container_get_callback
    return None

def container_get_callback(ctx: MethodContext) -> Type:
    # Access the type of 'self'
    self_type = ctx.type
    # Infer better return type
    return ctx.default_return_type
Method hooks use the class where the method is defined, not where it’s called.

get_method_signature_hook

Adjust method signatures, including special methods. Example from ctypes plugin:
def get_method_signature_hook(self, fullname: str):
    if fullname == "ctypes.Array.__setitem__":
        return array_setitem_callback
    return None
Usage:
from ctypes import Array, c_int

x: Array[c_int]
x[0] = 42  # Plugin customizes __setitem__ behavior

get_attribute_hook

Override attribute access type inference. Use case: Custom attribute type logic for properties and fields.
def get_attribute_hook(self, fullname: str):
    if fullname == "mylib.Dynamic.special_attr":
        return dynamic_attr_callback
    return None

def dynamic_attr_callback(ctx: AttributeContext) -> Type:
    # ctx.type is the object type
    # ctx.is_lvalue indicates if this is an assignment target
    if ctx.is_lvalue:
        return ctx.api.named_type('builtins.str', [])
    return ctx.default_attr_type
Only called for:
  • Existing fields/properties on classes without __getattr__/__getattribute__
  • All fields (including nonexistent) on classes with __getattr__/__getattribute__
  • Does not include method calls

get_class_attribute_hook

Similar to get_attribute_hook but for class attributes.
def get_class_attribute_hook(self, fullname: str):
    if fullname == "mylib.MyClass.special":
        return class_attr_callback
    return None

get_class_decorator_hook

Modify class definition for decorators. Example from dataclasses plugin:
def get_class_decorator_hook(self, fullname: str):
    if fullname == "dataclasses.dataclass":
        return dataclass_class_callback
    return None

def dataclass_class_callback(ctx: ClassDefContext) -> None:
    # Add __init__ method to the class
    # Modify ctx.cls.info in place
    pass
Usage:
from dataclasses import dataclass

@dataclass  # Plugin adds __init__ here
class User:
    name: str

user = User(name='example')  # Mypy understands this
This hook runs during semantic analysis and may encounter placeholders. Use get_class_decorator_hook_2 for a later pass without placeholders.

get_class_decorator_hook_2

Later-pass decorator hook without placeholders. Difference from hook 1:
  • Runs after placeholders are resolved
  • Can return False to request another pass
  • Must still be idempotent
def get_class_decorator_hook_2(self, fullname: str):
    if fullname == "mylib.special_class":
        return special_class_callback
    return None

def special_class_callback(ctx: ClassDefContext) -> bool:
    # Check if base classes are ready
    for base in ctx.cls.info.bases:
        if not is_processed(base):
            return False  # Request another pass
    # Process the class
    return True

get_metaclass_hook

Update class definition for metaclasses.
def get_metaclass_hook(self, fullname: str):
    if fullname == "mylib.CustomMeta":
        return metaclass_callback
    return None
Only called for explicit metaclasses, not inherited ones.

get_base_class_hook

Update class definition for base classes.
def get_base_class_hook(self, fullname: str):
    if fullname == "mylib.SpecialBase":
        return base_class_callback
    return None

get_dynamic_class_hook

Handle dynamic class definitions. Use case: Classes created by function calls.
def get_dynamic_class_hook(self, fullname: str):
    if fullname == "mylib.dynamic_class":
        return dynamic_class_callback
    return None

def dynamic_class_callback(ctx: DynamicClassDefContext) -> None:
    # Create TypeInfo and add to symbol table
    # ctx.call is the RHS CallExpr
    # ctx.name is the class name being assigned
    class_name = ctx.name
    # Create and register the class
    ctx.api.add_symbol_table_node(class_name, ...)
Usage:
from lib import dynamic_class

X = dynamic_class('X', [])  # Plugin handles this

get_customize_class_mro_hook

Modify class MRO before body analysis.
def get_customize_class_mro_hook(self, fullname: str):
    if fullname == "mylib.CustomMRO":
        return customize_mro_callback
    return None

get_additional_deps

Add module dependencies dynamically. Use case: Libraries with configuration-based imports.
def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
    if file.fullname == "mylib.dynamic":
        # Return (priority, module_name, line_number)
        return [(10, "mylib.extra", -1)]
    return []
Use priority 10 for most cases. Line number can be -1 if unknown.

report_config_data

Report configuration for cache invalidation. Use case: Per-module configuration affecting type checking.
def report_config_data(self, ctx: ReportConfigContext) -> Any:
    # Return JSON-encodable data
    if ctx.id == "mylib":
        return {
            'strict_mode': True,
            'version': '1.0'
        }
    return None
Mypy invalidates cache when returned data changes between runs.

Plugin API interfaces

CommonPluginApi

Available to all hooks:
class CommonPluginApi:
    options: Options  # Global mypy options
    
    def lookup_fully_qualified(self, fullname: str) -> SymbolTableNode | None:
        """Lookup symbol by full name."""

SemanticAnalyzerPluginInterface

Available during semantic analysis:
class SemanticAnalyzerPluginInterface:
    options: Options  # Per-file options
    modules: dict[str, MypyFile]
    cur_mod_id: str
    
    def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance: ...
    def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None: ...
    def defer(self) -> None: ...
    def add_symbol_table_node(self, name: str, symbol: SymbolTableNode) -> Any: ...
    
    @property
    def final_iteration(self) -> bool: ...

CheckerPluginInterface

Available during type checking:
class CheckerPluginInterface:
    msg: MessageBuilder
    options: Options
    
    def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None: ...
    def named_generic_type(self, name: str, args: list[Type]) -> Instance: ...
    def get_expression_type(self, node: Expression) -> Type: ...

Testing plugins

The proper_plugin helps find missing get_proper_type() calls:
[mypy]
plugins = mypy.plugins.proper_plugin
Enable proper_plugin in your CI to catch common plugin mistakes.

Real-world plugin example

Here’s a simplified version from the default plugin:
Default plugin structure
from mypy.plugin import Plugin, MethodContext
from mypy.types import Type

class DefaultPlugin(Plugin):
    def get_method_hook(self, fullname: str):
        if fullname == "builtins.int.__pow__":
            return int_pow_callback
        return None

def int_pow_callback(ctx: MethodContext) -> Type:
    # Infer int for non-negative exponents, float for negative
    if len(ctx.arg_types) == 2 and len(ctx.arg_types[0]) == 1:
        arg = ctx.args[0][0]
        if isinstance(arg, IntExpr):
            exponent = arg.value
            if exponent >= 0:
                return ctx.api.named_generic_type("builtins.int", [])
            else:
                return ctx.api.named_generic_type("builtins.float", [])
    return ctx.default_return_type

def plugin(version: str):
    return DefaultPlugin