p-e-w/heretic · error · TypeError
{cls.__name__}.settings must not be Optional; use a non-opti
Error message
{cls.__name__}.settings must not be Optional; use a non-optional pydantic.BaseModel subclass (e.g. `settings: Settings`). What it means
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.
Source
Thrown at src/heretic/plugin.py:273
def unwrap_settings_type(tp: Any) -> Any:
"""Unwrap `Annotated[T, ...]`."""
while True:
origin = get_origin(tp)
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.View on GitHub (pinned to bedb94ef11)
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.
Example fix
// before
class MyScorer(Plugin):
settings: MySettings | None
// after
class MyScorer(Plugin):
settings: MySettings Defensive patterns
Strategy: validation
Validate before calling
import typing
from heretic.plugin import Plugin
ann = typing.get_type_hints(MyPlugin).get("settings")
assert ann is not None and not typing.get_origin(ann) is typing.Union, "settings must not be Optional" Type guard
import types, typing
from pydantic import BaseModel
def is_valid_settings_annotation(cls) -> bool:
ann = typing.get_type_hints(cls).get("settings")
if ann is None: return False
if typing.get_origin(ann) in (typing.Union, types.UnionType): return False
return isinstance(ann, type) and issubclass(ann, BaseModel) Try / catch
try:
Plugin.get_settings_model(MyPlugin)
except TypeError as e:
if "must not be Optional" in str(e):
print("Remove Optional from settings annotation")
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- {cls.__name__}.settings must be annotated with a pydantic.Ba
- Plugin '{name}' does not export a class named '{class_name}'
- You must append the plugin class name to the filepath like t
- File-based plugin must use the form 'path/to/plugin.py:Class
- Could not load plugin '{name}' (invalid module spec)
AI-assisted analysis of p-e-w/heretic@bedb94ef11 (2026-08-29).
Data as JSON: /api/errors/2c47727de3843f55.
Report an issue: GitHub.