Type Checking
Protean ships with a mypy plugin that makes static type checkers understand the framework's runtime transformations. Without the plugin, tools like mypy report false errors because they cannot see the base classes and field types that Protean injects dynamically.
Why a Plugin is Needed
Protean uses two patterns that are invisible to static analysis:
-
Field factories —
String(),Integer(), etc. returnFieldSpecobjects at the class level, but at runtime the descriptor protocol resolves them tostr,int, and so on. The plugin teaches mypy the correct return types. -
Decorator-based registration —
@domain.aggregate,@domain.entity, and other decorators callderive_element_class()at runtime, which dynamically injects a base class (e.g.BaseAggregate) viatype(name, (base_cls,), ...). The plugin injects these base classes during type analysis so that methods likeraise_(),to_dict(), and auto-injected attributes likeidare visible.
Quick Setup
Add the plugin to your mypy configuration in pyproject.toml:
[tool.mypy]
plugins = ["protean.ext.mypy_plugin"]
That's it. Both field type resolution and decorator base class injection are enabled automatically.
What the Plugin Does
Field Type Resolution
Field factories resolve to their Python types:
| Field | Resolved Type |
|---|---|
String() |
str \| None |
String(required=True) |
str |
String(default="hello") |
str |
Integer() |
int \| None |
Float() |
float \| None |
Boolean() |
bool \| None |
Date() |
datetime.date \| None |
DateTime() |
datetime.datetime \| None |
List() |
list |
Dict() |
dict |
Identifier(identifier=True) |
str |
Auto(identifier=True) |
str |
Fields are Optional (union with None) unless they are required=True,
have a default, or are identifier=True. Container fields (List, Dict)
always have an implicit default and are never Optional.
Decorator Base Class Injection
When you write:
@domain.aggregate
class Customer:
name = String(required=True)
The plugin makes mypy see Customer as if it inherits from BaseAggregate,
giving access to:
customer.id— auto-injectedstrfor aggregates and entitiescustomer.raise_(event)— raise domain eventscustomer.to_dict()— serialize to dictionarycustomer._events— event list- All other
BaseAggregatemethods and attributes
This works for all 15 decorator types: aggregate, entity, value_object,
command, event, domain_service, command_handler, event_handler,
application_service, subscriber, projection, projector, repository,
database_model, and email.
Both @domain.aggregate (without parens) and @domain.aggregate() (with
parens) are supported.
VS Code Setup
For the best experience in VS Code:
- Install the mypy extension (
ms-python.mypy-type-checker) - Disable Pylance's type checking (keep Pylance for autocomplete and go-to-definition):
{
"python.analysis.typeCheckingMode": "off",
"mypy-type-checker.args": ["--config-file=pyproject.toml"],
"mypy-type-checker.importStrategy": "fromEnvironment"
}
The project's .vscode/settings.json and .vscode/extensions.json already
include these settings and extension recommendations.
Known Limitations
-
auto_add_id_field=False— The plugin does not inspect decorator arguments. If you passauto_add_id_field=Falseto@domain.aggregate, the plugin still injectsid. Use# type: ignore[attr-defined]if needed. -
Autocomplete for injected methods — While mypy correctly type-checks injected base class methods, some IDE autocomplete engines may not show them in suggestions because the base class is not in the explicit MRO.
-
Explicit inheritance — If you already inherit from a base class (e.g.
class Order(BaseAggregate):), the plugin detects this and skips injection to avoid duplicates.