or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ast-nodes.mdbuild-system.mdcommand-line-tools.mddaemon-mode.mderror-system.mdindex.mdmypyc-compiler.mdplugin-system.mdprogrammatic-api.mdstub-tools.mdtype-system.md

plugin-system.mddocs/

0

# Plugin System

1

2

Extensible plugin architecture for customizing type checking behavior and adding support for specific libraries or frameworks. Mypy's plugin system allows deep integration with third-party libraries and custom type checking logic.

3

4

## Capabilities

5

6

### Core Plugin Classes

7

8

Base classes for creating mypy plugins that customize type checking behavior.

9

10

```python { .api }

11

class Plugin:

12

"""

13

Base class for mypy plugins.

14

15

Plugins can customize various aspects of type checking by providing

16

hooks that are called during different phases of analysis.

17

18

Methods to override:

19

- get_type_analyze_hook(self, fullname: str) -> Callable | None

20

- get_function_hook(self, fullname: str) -> Callable | None

21

- get_method_hook(self, fullname: str) -> Callable | None

22

- get_attribute_hook(self, fullname: str) -> Callable | None

23

- get_class_decorator_hook(self, fullname: str) -> Callable | None

24

- get_metaclass_hook(self, fullname: str) -> Callable | None

25

- get_base_class_hook(self, fullname: str) -> Callable | None

26

"""

27

28

def __init__(self, options: Options):

29

"""Initialize plugin with mypy options."""

30

31

class CommonPluginApi:

32

"""

33

Common API available to plugin callbacks.

34

35

Provides access to type analysis utilities, name lookup,

36

and type construction functions used by plugins.

37

38

Attributes:

39

- modules: dict[str, MypyFile] - Loaded modules

40

- msg: MessageBuilder - Error reporting

41

- options: Options - Mypy configuration

42

43

Methods:

44

- named_generic_type(name: str, args: list[Type]) -> Instance

45

- named_type(name: str) -> Instance

46

- lookup_fully_qualified(name: str) -> SymbolTableNode | None

47

- fail(msg: str, ctx: Context) -> None

48

"""

49

50

class SemanticAnalyzerPluginInterface(CommonPluginApi):

51

"""

52

API available during semantic analysis phase.

53

54

Used for plugins that need to analyze code structure,

55

modify AST nodes, or add new symbol table entries.

56

57

Additional methods:

58

- add_symbol_table_node(name: str, stnode: SymbolTableNode) -> None

59

- lookup_current_scope(name: str) -> SymbolTableNode | None

60

- defer_node(node: Node, enclosing_class: TypeInfo | None) -> None

61

"""

62

63

class CheckerPluginInterface(CommonPluginApi):

64

"""

65

API available during type checking phase.

66

67

Used for plugins that need to perform custom type checking,

68

validate specific patterns, or integrate with type inference.

69

70

Additional methods:

71

- check_subtype(left: Type, right: Type, ctx: Context) -> bool

72

- type_check_expr(expr: Expression, type_context: Type | None) -> Type

73

"""

74

```

75

76

### Plugin Context Classes

77

78

Context objects passed to plugin hooks containing information about the analysis context.

79

80

```python { .api }

81

class FunctionContext:

82

"""

83

Context for function call analysis hooks.

84

85

Attributes:

86

- default_return_type: Type - Default return type

87

- arg_types: list[list[Type]] - Argument types for each argument

88

- arg_names: list[list[str | None]] - Argument names

89

- callee_type: Type - Type of the function being called

90

- context: Context - AST context for error reporting

91

- api: CheckerPluginInterface - Type checker API

92

"""

93

94

class AttributeContext:

95

"""

96

Context for attribute access analysis hooks.

97

98

Attributes:

99

- default_attr_type: Type - Default attribute type

100

- type: Type - Type of the object being accessed

101

- context: Context - AST context

102

- api: CheckerPluginInterface - Type checker API

103

"""

104

105

class ClassDefContext:

106

"""

107

Context for class definition analysis hooks.

108

109

Attributes:

110

- cls: ClassDef - Class definition AST node

111

- reason: Type - Reason for hook invocation

112

- api: SemanticAnalyzerPluginInterface - Semantic analyzer API

113

"""

114

115

class BaseClassContext:

116

"""

117

Context for base class analysis hooks.

118

119

Attributes:

120

- cls: ClassDef - Class definition

121

- arg: Expression - Base class expression

122

- default_base: Type - Default base class type

123

- api: SemanticAnalyzerPluginInterface - API access

124

"""

125

```

126

127

## Built-in Plugins

128

129

### Default Plugin

130

131

Core plugin providing built-in type checking functionality.

132

133

```python { .api }

134

class DefaultPlugin(Plugin):

135

"""

136

Default plugin with built-in type handling.

137

138

Provides standard type checking for:

139

- Built-in functions and types

140

- Standard library modules

141

- Common Python patterns

142

- Generic type instantiation

143

"""

144

```

145

146

### Library-Specific Plugins

147

148

Pre-built plugins for popular Python libraries.

149

150

```python { .api }

151

# Available built-in plugins in mypy.plugins:

152

153

class AttrsPlugin(Plugin):

154

"""Support for attrs library decorators and classes."""

155

156

class DataclassesPlugin(Plugin):

157

"""Support for dataclasses with proper type inference."""

158

159

class EnumsPlugin(Plugin):

160

"""Enhanced support for enum.Enum classes."""

161

162

class FunctoolsPlugin(Plugin):

163

"""Support for functools decorators like @lru_cache."""

164

165

class CtypesPlugin(Plugin):

166

"""Support for ctypes library type checking."""

167

168

class SqlAlchemyPlugin(Plugin):

169

"""Support for SQLAlchemy ORM type checking."""

170

```

171

172

## Creating Custom Plugins

173

174

### Basic Plugin Structure

175

176

```python

177

from mypy.plugin import Plugin, FunctionContext

178

from mypy.types import Type, Instance

179

from mypy.nodes import ARG_POS, Argument, Var, PassStmt

180

181

class CustomPlugin(Plugin):

182

"""Example custom plugin for specialized type checking."""

183

184

def get_function_hook(self, fullname: str):

185

"""Return hook for specific function calls."""

186

if fullname == "mylib.special_function":

187

return self.handle_special_function

188

elif fullname == "mylib.create_instance":

189

return self.handle_create_instance

190

return None

191

192

def handle_special_function(self, ctx: FunctionContext) -> Type:

193

"""Custom handling for mylib.special_function."""

194

# Validate arguments

195

if len(ctx.arg_types) != 2:

196

ctx.api.fail("special_function requires exactly 2 arguments", ctx.context)

197

return ctx.default_return_type

198

199

# Check first argument is string

200

first_arg = ctx.arg_types[0][0] if ctx.arg_types[0] else None

201

if not isinstance(first_arg, Instance) or first_arg.type.fullname != 'builtins.str':

202

ctx.api.fail("First argument must be a string", ctx.context)

203

204

# Return custom type based on analysis

205

return ctx.api.named_type('mylib.SpecialResult')

206

207

def handle_create_instance(self, ctx: FunctionContext) -> Type:

208

"""Custom factory function handling."""

209

if ctx.arg_types and ctx.arg_types[0]:

210

# Return instance of type specified in first argument

211

type_arg = ctx.arg_types[0][0]

212

if isinstance(type_arg, Instance):

213

return type_arg

214

215

return ctx.default_return_type

216

217

# Plugin entry point

218

def plugin(version: str):

219

"""Entry point for mypy plugin discovery."""

220

return CustomPlugin

221

```

222

223

### Advanced Plugin Features

224

225

```python

226

from mypy.plugin import Plugin, ClassDefContext, BaseClassContext

227

from mypy.types import Type, Instance, CallableType

228

from mypy.nodes import ClassDef, FuncDef, Decorator

229

230

class AdvancedPlugin(Plugin):

231

"""Advanced plugin with class and decorator handling."""

232

233

def get_class_decorator_hook(self, fullname: str):

234

"""Handle class decorators."""

235

if fullname == "mylib.special_class":

236

return self.handle_special_class_decorator

237

return None

238

239

def get_base_class_hook(self, fullname: str):

240

"""Handle specific base classes."""

241

if fullname == "mylib.BaseModel":

242

return self.handle_base_model

243

return None

244

245

def handle_special_class_decorator(self, ctx: ClassDefContext) -> None:

246

"""Process @special_class decorator."""

247

# Add special methods to the class

248

self.add_magic_method(ctx.cls, "__special__",

249

ctx.api.named_type('builtins.str'))

250

251

def handle_base_model(self, ctx: BaseClassContext) -> Type:

252

"""Handle BaseModel inheritance."""

253

# Analyze class for special fields

254

if isinstance(ctx.cls, ClassDef):

255

self.process_model_fields(ctx.cls, ctx.api)

256

257

return ctx.default_base

258

259

def add_magic_method(self, cls: ClassDef, method_name: str,

260

return_type: Type) -> None:

261

"""Add a magic method to class definition."""

262

# Create method signature

263

method_type = CallableType(

264

arg_types=[Instance(cls.info, [])], # self parameter

265

arg_kinds=[ARG_POS],

266

arg_names=['self'],

267

return_type=return_type,

268

fallback=self.lookup_typeinfo('builtins.function')

269

)

270

271

# Add to symbol table

272

method_node = FuncDef(method_name, [], None, None)

273

method_node.type = method_type

274

cls.info.names[method_name] = method_node

275

276

def process_model_fields(self, cls: ClassDef, api) -> None:

277

"""Process model field definitions."""

278

for stmt in cls.defs.body:

279

if isinstance(stmt, AssignmentStmt):

280

# Analyze field assignments

281

self.analyze_field_assignment(stmt, api)

282

283

def plugin(version: str):

284

return AdvancedPlugin

285

```

286

287

### Plugin with Type Analysis

288

289

```python

290

from mypy.plugin import Plugin, AttributeContext

291

from mypy.types import Type, Instance, AnyType, TypeOfAny

292

293

class TypeAnalysisPlugin(Plugin):

294

"""Plugin demonstrating type analysis capabilities."""

295

296

def get_attribute_hook(self, fullname: str):

297

"""Handle attribute access."""

298

if fullname == "mylib.DynamicObject.__getattr__":

299

return self.handle_dynamic_getattr

300

return None

301

302

def handle_dynamic_getattr(self, ctx: AttributeContext) -> Type:

303

"""Handle dynamic attribute access."""

304

# Analyze the attribute name

305

attr_name = self.get_attribute_name(ctx)

306

307

if attr_name and attr_name.startswith('computed_'):

308

# Return specific type for computed attributes

309

return ctx.api.named_type('builtins.float')

310

elif attr_name and attr_name.startswith('cached_'):

311

# Return cached value type

312

return self.get_cached_type(attr_name, ctx)

313

314

# Default to Any for unknown dynamic attributes

315

return AnyType(TypeOfAny.from_error)

316

317

def get_attribute_name(self, ctx: AttributeContext) -> str | None:

318

"""Extract attribute name from context."""

319

# This would need to analyze the AST context

320

# Implementation depends on specific use case

321

return None

322

323

def get_cached_type(self, attr_name: str, ctx: AttributeContext) -> Type:

324

"""Determine type for cached attributes."""

325

# Custom logic for determining cached value types

326

cache_map = {

327

'cached_count': ctx.api.named_type('builtins.int'),

328

'cached_name': ctx.api.named_type('builtins.str'),

329

'cached_data': ctx.api.named_type('builtins.list')

330

}

331

332

return cache_map.get(attr_name, AnyType(TypeOfAny.from_error))

333

334

def plugin(version: str):

335

return TypeAnalysisPlugin

336

```

337

338

## Plugin Configuration and Loading

339

340

### Plugin Entry Points

341

342

```python

343

# setup.py or pyproject.toml configuration for plugin distribution

344

from setuptools import setup

345

346

setup(

347

name="mypy-custom-plugin",

348

entry_points={

349

"mypy.plugins": [

350

"custom_plugin = mypy_custom_plugin.plugin:plugin"

351

]

352

}

353

)

354

```

355

356

### Plugin Loading in mypy.ini

357

358

```ini

359

[mypy]

360

plugins = mypy_custom_plugin.plugin, another_plugin

361

362

[mypy-mylib.*]

363

# Plugin-specific configuration

364

ignore_errors = false

365

```

366

367

### Plugin Loading Programmatically

368

369

```python

370

from mypy.build import build, BuildSource

371

from mypy.options import Options

372

373

# Load plugin programmatically

374

options = Options()

375

options.plugins = ['mypy_custom_plugin.plugin']

376

377

# Custom plugin instance

378

plugin_instance = CustomPlugin(options)

379

380

sources = [BuildSource("myfile.py", None, None)]

381

result = build(sources, options, extra_plugins=[plugin_instance])

382

```

383

384

## Testing Plugins

385

386

### Plugin Test Framework

387

388

```python

389

import tempfile

390

import os

391

from mypy import api

392

from mypy.test.helpers import Suite

393

394

class PluginTestCase:

395

"""Test framework for mypy plugins."""

396

397

def run_with_plugin(self, source_code: str, plugin_path: str) -> tuple[str, str, int]:

398

"""Run mypy with plugin on source code."""

399

with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:

400

f.write(source_code)

401

temp_file = f.name

402

403

try:

404

# Run mypy with plugin

405

result = api.run([

406

'--plugins', plugin_path,

407

'--show-error-codes',

408

temp_file

409

])

410

return result

411

finally:

412

os.unlink(temp_file)

413

414

def assert_no_errors(self, result: tuple[str, str, int]):

415

"""Assert that mypy found no errors."""

416

stdout, stderr, exit_code = result

417

assert exit_code == 0, f"Expected no errors, got: {stderr}"

418

419

def assert_error_contains(self, result: tuple[str, str, int],

420

expected_message: str):

421

"""Assert that error output contains expected message."""

422

stdout, stderr, exit_code = result

423

assert expected_message in stderr, f"Expected '{expected_message}' in: {stderr}"

424

425

# Usage

426

def test_custom_plugin():

427

"""Test custom plugin functionality."""

428

source = '''

429

from mylib import special_function

430

431

result = special_function("hello", 42) # Should pass

432

result2 = special_function(123, "world") # Should fail

433

'''

434

435

test_case = PluginTestCase()

436

result = test_case.run_with_plugin(source, "mypy_custom_plugin.plugin")

437

438

# Should have one error for the second call

439

test_case.assert_error_contains(result, "First argument must be a string")

440

```

441

442

### Integration Testing

443

444

```python

445

import pytest

446

from mypy import api

447

448

class TestPluginIntegration:

449

"""Integration tests for plugin with real codebases."""

450

451

@pytest.fixture

452

def sample_project(self, tmp_path):

453

"""Create sample project for testing."""

454

# Create project structure

455

(tmp_path / "mylib").mkdir()

456

(tmp_path / "mylib" / "__init__.py").write_text("")

457

458

(tmp_path / "mylib" / "core.py").write_text('''

459

class SpecialResult:

460

def __init__(self, value: str):

461

self.value = value

462

463

def special_function(name: str, count: int) -> SpecialResult:

464

return SpecialResult(f"{name}_{count}")

465

''')

466

467

(tmp_path / "main.py").write_text('''

468

from mylib.core import special_function

469

470

result = special_function("test", 5)

471

print(result.value)

472

''')

473

474

return tmp_path

475

476

def test_plugin_with_project(self, sample_project):

477

"""Test plugin with complete project."""

478

os.chdir(sample_project)

479

480

result = api.run([

481

'--plugins', 'mypy_custom_plugin.plugin',

482

'main.py'

483

])

484

485

stdout, stderr, exit_code = result

486

assert exit_code == 0, f"Plugin failed on project: {stderr}"

487

```

488

489

## Plugin Best Practices

490

491

### Performance Considerations

492

493

```python

494

class EfficientPlugin(Plugin):

495

"""Example of performance-conscious plugin design."""

496

497

def __init__(self, options):

498

super().__init__(options)

499

# Cache expensive computations

500

self._type_cache = {}

501

self._analyzed_classes = set()

502

503

def get_function_hook(self, fullname: str):

504

# Use early returns to avoid unnecessary work

505

if not fullname.startswith('mylib.'):

506

return None

507

508

# Cache hook lookups

509

if fullname not in self._hook_cache:

510

self._hook_cache[fullname] = self._compute_hook(fullname)

511

512

return self._hook_cache[fullname]

513

514

def handle_expensive_operation(self, ctx: FunctionContext) -> Type:

515

"""Cache expensive type computations."""

516

cache_key = (ctx.callee_type, tuple(str(t) for t in ctx.arg_types[0]))

517

518

if cache_key in self._type_cache:

519

return self._type_cache[cache_key]

520

521

# Perform expensive computation

522

result = self._compute_type(ctx)

523

self._type_cache[cache_key] = result

524

return result

525

```

526

527

### Error Handling in Plugins

528

529

```python

530

class RobustPlugin(Plugin):

531

"""Plugin with proper error handling."""

532

533

def handle_function_call(self, ctx: FunctionContext) -> Type:

534

"""Safely handle function calls with error recovery."""

535

try:

536

# Validate context

537

if not ctx.arg_types:

538

ctx.api.fail("Missing arguments", ctx.context)

539

return ctx.default_return_type

540

541

# Perform analysis

542

return self._analyze_call(ctx)

543

544

except Exception as e:

545

# Log error and fall back to default behavior

546

ctx.api.fail(f"Plugin error: {e}", ctx.context)

547

return ctx.default_return_type

548

549

def _analyze_call(self, ctx: FunctionContext) -> Type:

550

"""Internal analysis with proper error handling."""

551

# Implementation with validation at each step

552

pass

553

```