{"record":{"id":"424157f31ff62f6c","repo":"p-e-w/heretic","slug":"cls-name-settings-must-be-annotated-with-a-p","errorCode":null,"errorMessage":"{cls.__name__}.settings must be annotated with a pydantic.BaseModel subclass","messagePattern":"(.+?)\\.settings must be annotated with a pydantic\\.BaseModel subclass","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"src/heretic/plugin.py","lineNumber":278,"sourceCode":"                if origin is Annotated:\n                    tp = get_args(tp)[0]\n                    continue\n                return tp\n\n        hints = get_type_hints(cls, include_extras=True)\n        annotated = hints.get(\"settings\")\n        if annotated is None:\n            return None\n\n        model = unwrap_settings_type(annotated)\n        origin = get_origin(model)\n        if origin in (Union, types.UnionType) and type(None) in get_args(model):\n            raise TypeError(\n                f\"{cls.__name__}.settings must not be Optional; \"\n                \"use a non-optional pydantic.BaseModel subclass (e.g. `settings: Settings`).\"\n            )\n        if not isinstance(model, type) or not issubclass(model, BaseModel):\n            raise TypeError(\n                f\"{cls.__name__}.settings must be annotated with a pydantic.BaseModel subclass\"\n            )\n        return model\n\n    @classmethod\n    def validate_settings(\n        cls, raw_namespace: dict[str, Any] | None\n    ) -> BaseModel | None:\n        \"\"\"\n        Validates plugin settings for this plugin class.\n\n        - If a settings model is present: returns an instance of that model.\n        - Otherwise returns None.\n        \"\"\"\n        settings_model = cls.get_settings_model()\n        if settings_model is None:\n            return None\n        return settings_model.model_validate(raw_namespace or {})","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/p-e-w/heretic/blob/bedb94ef117a271532ac2058447fbc165d5051bd/src/heretic/plugin.py#L260-L296","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Define a `class MySettings(BaseModel)` and annotate `settings: MySettings`.","Ensure pydantic is imported and the annotation is a real class, not a generic or dataclass.","Move defaults/validators into the pydantic model definition."],"exampleFix":"// before\nclass MyScorer(Plugin):\n    settings: dict\n// after\nclass MySettings(pydantic.BaseModel):\n    model_name: str\nclass MyScorer(Plugin):\n    settings: MySettings","handlingStrategy":"validation","validationCode":"from pydantic import BaseModel\nassert issubclass(MyPluginSettings, BaseModel), \"settings model must subclass pydantic.BaseModel\"","typeGuard":"import typing\nfrom pydantic import BaseModel\ndef has_pydantic_settings(cls) -> bool:\n    ann = typing.get_type_hints(cls).get(\"settings\")\n    return isinstance(ann, type) and issubclass(ann, BaseModel)","tryCatchPattern":"try:\n    Plugin.get_settings_model(MyPlugin)\nexcept TypeError as e:\n    if \"must be annotated with a pydantic.BaseModel subclass\" in str(e):\n        print(f\"Fix {MyPlugin.__name__}.settings annotation\")\n    else:\n        raise","preventionTips":["Always define settings as a pydantic BaseModel subclass.","Don't annotate settings with dict/dataclass/TypedDict.","Test plugin loading in CI to catch annotation mistakes early."],"tags":["python","pydantic","plugin","typing"],"backgroundTag":"plugin-contract-violation","analyzedSha":"bedb94ef117a271532ac2058447fbc165d5051bd","analyzedAt":"2026-08-29T08:38:06.692Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}