p-e-w/heretic · error · TypeError

{cls.__name__}.settings must be annotated with a pydantic.Ba

Error message

{cls.__name__}.settings must be annotated with a pydantic.BaseModel subclass

What it means

A plugin's `settings` attribute must be annotated with a subclass of pydantic BaseModel. Any other annotation (a plain class, a non-model type, a primitive, or a non-type like a string forward reference resolving to something else) is rejected. This is raised in get_settings_model when unwrap_settings_type does not yield a BaseModel subclass.

Source

Thrown at src/heretic/plugin.py:278

                if origin is Annotated:
                    tp = get_args(tp)[0]
                    continue
                return tp

        hints = get_type_hints(cls, include_extras=True)
        annotated = hints.get("settings")
        if annotated is None:
            return None

        model = unwrap_settings_type(annotated)
        origin = get_origin(model)
        if origin in (Union, types.UnionType) and type(None) in get_args(model):
            raise TypeError(
                f"{cls.__name__}.settings must not be Optional; "
                "use a non-optional pydantic.BaseModel subclass (e.g. `settings: Settings`)."
            )
        if not isinstance(model, type) or not issubclass(model, BaseModel):
            raise TypeError(
                f"{cls.__name__}.settings must be annotated with a pydantic.BaseModel subclass"
            )
        return model

    @classmethod
    def validate_settings(
        cls, raw_namespace: dict[str, Any] | None
    ) -> BaseModel | None:
        """
        Validates plugin settings for this plugin class.

        - If a settings model is present: returns an instance of that model.
        - Otherwise returns None.
        """
        settings_model = cls.get_settings_model()
        if settings_model is None:
            return None
        return settings_model.model_validate(raw_namespace or {})

View on GitHub (pinned to bedb94ef11)

Solutions

  1. Define a `class MySettings(BaseModel)` and annotate `settings: MySettings`.
  2. Ensure pydantic is imported and the annotation is a real class, not a generic or dataclass.
  3. Move defaults/validators into the pydantic model definition.

Example fix

// before
class MyScorer(Plugin):
    settings: dict
// after
class MySettings(pydantic.BaseModel):
    model_name: str
class MyScorer(Plugin):
    settings: MySettings
Defensive patterns

Strategy: validation

Validate before calling

from pydantic import BaseModel
assert issubclass(MyPluginSettings, BaseModel), "settings model must subclass pydantic.BaseModel"

Type guard

import typing
from pydantic import BaseModel
def has_pydantic_settings(cls) -> bool:
    ann = typing.get_type_hints(cls).get("settings")
    return isinstance(ann, type) and issubclass(ann, BaseModel)

Try / catch

try:
    Plugin.get_settings_model(MyPlugin)
except TypeError as e:
    if "must be annotated with a pydantic.BaseModel subclass" in str(e):
        print(f"Fix {MyPlugin.__name__}.settings annotation")
    else:
        raise

Prevention

When it happens

Trigger: Declaring `settings: dict`, `settings: SomeDataclass`, `settings: int`, or any non-pydantic type on a Plugin subclass; triggered via get_settings_model from plugin __init__, _get_scorer_settings_raw, or validate_settings.

Common situations: Using a dataclass or TypedDict for settings out of habit; forgetting to import BaseModel and subclass it; annotating with a generic container type instead of a settings model.

Related errors


AI-assisted analysis of p-e-w/heretic@bedb94ef11 (2026-08-29). Data as JSON: /api/errors/424157f31ff62f6c. Report an issue: GitHub.