{"record":{"id":"2c47727de3843f55","repo":"p-e-w/heretic","slug":"cls-name-settings-must-not-be-optional-use","errorCode":null,"errorMessage":"{cls.__name__}.settings must not be Optional; use a non-optional pydantic.BaseModel subclass (e.g. `settings: Settings`).","messagePattern":"(.+?)\\.settings must not be Optional; use a non-optional pydantic\\.BaseModel subclass \\(e\\.g\\. `settings: Settings`\\)\\.","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"src/heretic/plugin.py","lineNumber":273,"sourceCode":"\n        def unwrap_settings_type(tp: Any) -> Any:\n            \"\"\"Unwrap `Annotated[T, ...]`.\"\"\"\n            while True:\n                origin = get_origin(tp)\n                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.","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/p-e-w/heretic/blob/bedb94ef117a271532ac2058447fbc165d5051bd/src/heretic/plugin.py#L255-L291","documentation":"A plugin's `settings` class attribute annotation must be a non-optional pydantic BaseModel subclass. Annotating it as `Optional[Settings]` is rejected because optional settings would break schema generation and validation. The error is raised by get_settings_model when it detects a Union containing NoneType in the annotation.","triggerScenarios":"Declaring `settings: Optional[MySettings]` or `settings: MySettings | None` on a Plugin subclass; get_settings_model is called by _get_scorer_settings_raw, plugin __init__, or validate_settings.","commonSituations":"Copy-pasting a settings annotation from a dataclass where Optional was common; making settings optional so defaults could be omitted instead of using a full settings model with defaults.","solutions":["Change the annotation to the plain model class, e.g. `settings: MySettings`.","Give fields inside the pydantic model default values if some settings are optional."],"exampleFix":"// before\nclass MyScorer(Plugin):\n    settings: MySettings | None\n// after\nclass MyScorer(Plugin):\n    settings: MySettings","handlingStrategy":"validation","validationCode":"import typing\nfrom heretic.plugin import Plugin\nann = typing.get_type_hints(MyPlugin).get(\"settings\")\nassert ann is not None and not typing.get_origin(ann) is typing.Union, \"settings must not be Optional\"","typeGuard":"import types, typing\nfrom pydantic import BaseModel\ndef is_valid_settings_annotation(cls) -> bool:\n    ann = typing.get_type_hints(cls).get(\"settings\")\n    if ann is None: return False\n    if typing.get_origin(ann) in (typing.Union, types.UnionType): return False\n    return isinstance(ann, type) and issubclass(ann, BaseModel)","tryCatchPattern":"try:\n    Plugin.get_settings_model(MyPlugin)\nexcept TypeError as e:\n    if \"must not be Optional\" in str(e):\n        print(\"Remove Optional from settings annotation\")\n    else:\n        raise","preventionTips":["Annotate settings as a plain BaseModel subclass.","Put optionality inside model fields (defaults), not the annotation.","Add a CI check calling validate_settings/get_settings_model on all plugins."],"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"}