Wiki

Plugin-synthesized Any is not user Any

When a mypy plugin synthesizes an Any to fill a hole the plugin can't statically determine, the right TypeOfAny variant is from_omitted_generics, not explicit. The user didn't write that Any. Tagging it as if they did makes --disallow-any-explicit fire on every model that exercises the plugin's fallback path.

This is a one-line distinction inside the mypy plugin API, and it shows up the moment a project ships a plugin that turns a dynamic-but-not-statically-knowable signature into a method declaration. Pydantic shipped the wrong tag for ~two years.

When to reach for it

I'm writing or maintaining a mypy plugin. I'm calling add_method (or building Argument / AnyType directly) and one of the parameters is Any because the plugin can't compute the real type at static-check time. The **kwargs fallback that lets a class accept arbitrary keyword arguments is the canonical example, but the same issue applies anywhere the plugin synthesizes a fallback type for a slot it couldn't determine.

The two variants

TypeOfAny is an enum on mypy.types.AnyType describing how the Any came into existence. The two variants that matter for plugin-synthesized fallbacks:

Plugin-synthesized fallback Any is conceptually closer to the second case: the plugin is filling a hole, not transcribing a user choice. The right tag is from_omitted_generics.

The pydantic case

pydantic#13161, reported 2026-04-12 by julianochoi.

Pydantic's mypy plugin reads each model class and synthesizes a typed __init__ so static checkers see real parameters. When init_forbid_extra is enabled (forbids unknown keyword args at runtime), the plugin normally drops the catch-all **kwargs parameter. But if the model has a dynamic validation alias (AliasChoices, AliasPath, or alias_generator), the plugin can't statically determine the alias keys, so it has to keep **kwargs: Any to accept whichever keys the alias resolves to at runtime.

That fallback **kwargs: Any was synthesized with TypeOfAny.explicit at pydantic/mypy.py:937 and :968. Any project running with both disallow_any_explicit = true and init_forbid_extra = true saw a misc error on every dynamic-alias model:

error: Explicit "Any" is not allowed [misc]

The user never wrote Any. They wrote Field(validation_alias=AliasChoices(...)). The plugin produced the Any to make the synthesized signature accept the dynamic keys. Tagging it explicit made mypy report a violation against code the user didn't write.

The fix

Two-character change at each fault site, plus an existing in-file precedent:

# Before
args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2))

# After
args.append(Argument(var, AnyType(TypeOfAny.from_omitted_generics), None, ARG_STAR2))

The same plugin already handled this distinction correctly on a different path: pydantic/mypy.py:929-931 used ChangeExplicitTypeOfAny(TypeOfAny.from_omitted_generics) to re-tag the _cli_settings_source / _build_sources parameters, which were also plugin-synthesized fallbacks. The two **kwargs sites had the same shape but missed the conversion.

The PR is pydantic#13163. The regression test seeds AliasChoices, AliasPath, and alias_generator under mypy-plugin-strict-no-any.ini, asserts the dynamic-alias warnings fire (because the config enables them), and asserts no --disallow-any-explicit error.

The general rule

In a mypy plugin, when you reach for AnyType(...):

  1. Ask: did the user write this Any?
  2. If yes (you're transcribing a user-written annotation), TypeOfAny.explicit is correct.
  3. If no (you're filling a hole), TypeOfAny.from_omitted_generics is correct.

The **kwargs fallback is "filling a hole". So is any synthesized parameter type that compensates for dynamic configuration the plugin can't read at static-check time. So is the type of a generated property whose underlying expression isn't statically analyzable.

What this doesn't replace

When not to use it

When the user actually wrote Any and you're transcribing it. A plugin that reads __init__(self, x: Any) from source and copies the annotation onto a synthesized method should preserve TypeOfAny.explicit, because that's what the user wrote. The rule is about plugin-introduced Any, not user-written Any the plugin happens to encounter.

Related

Revisit

Add a second real application the next time the variant mismatch shows up in a different mypy plugin. If a third variant becomes the right answer in a synthesized-fallback case (TypeOfAny.special_form, for instance, for plugin output that participates in a typing special form), name it here and rewrite "The general rule" as a three-way decision instead of a two-way one.