p-e-w/heretic · error · ValueError
Plugin '{name}' does not export a class named '{class_name}'
Error message
Plugin '{name}' does not export a class named '{class_name}' What it means
load_plugin() resolved the plugin module but getattr(module, class_name) returned nothing class-like. The library throws this because the plugin name promises a class (after ':' for file plugins or after the last '.' for import plugins) that the module does not actually define. It guards against silently returning a function, variable, or None as a 'plugin class'.
Source
Thrown at src/heretic/plugin.py:78
) -> type[T]:
"""
Load a plugin class from either a filesystem `.py` file or a fully-qualified Python import path.
Also checks that the class exists in the module and that it
subclasses the correct Plugin subclass (e.g Scorer).
Accepted forms:
- `path/to/plugin.py:MyPluginClass` (relative or absolute): load `MyPluginClass`
from that file.
- `fully.qualified.module.MyPluginClass`: import the module and load the class.
"""
def validate_class(module: ModuleType, class_name: str) -> type[Any]:
"""
Checks that the module actually exports the class as claimed and returns the class.
"""
obj = getattr(module, class_name, None)
if not inspect.isclass(obj):
raise ValueError(
f"Plugin '{name}' does not export a class named '{class_name}'"
)
return obj
# Common user trap with filepath imports.
if name.endswith(".py"):
raise ValueError(
"You must append the plugin class name to the filepath like this: path/to/plugin.py:ClassName"
)
# File path with explicit class name, e.g. "C:\\path\\plugin.py:MyPlugin".
if ":" in name:
file_path, class_name = name.rsplit(":", 1)
if not file_path.endswith(".py") or not class_name:
raise ValueError(
"File-based plugin must use the form 'path/to/plugin.py:ClassName'"
)
View on GitHub (pinned to bedb94ef11)
Solutions
- Open the plugin file/module and confirm a class with the exact name (after ':' or the last '.') exists.
- Fix the class name in your config/CLI argument to match the definition, including case.
- If the class is defined in another module, import it into the plugin module or point load_plugin at the module that truly defines it.
- If you exported a function/constant instead, wrap it in a proper subclass of the expected base class.
Example fix
# before heretic --scorer my_scorers.py:KeywrodRate # after heretic --scorer my_scorers.py:KeywordRate
Defensive patterns
Strategy: validation
Validate before calling
import importlib, inspect
def plugin_class_exists(dotted: str) -> bool:
mod_name, cls = dotted.rsplit(".", 1)
obj = getattr(importlib.import_module(mod_name), cls, None)
return inspect.isclass(obj) Type guard
def is_plugin_class(obj: object) -> bool:
return inspect.isclass(obj) Try / catch
try:
cls = load_plugin(name, Scorer)
except ValueError as e:
print(f"bad plugin name '{name}': {e}") Prevention
- Keep plugin names in one config file and verify class names with grep before running.
- Match case exactly; class names are case-sensitive.
- Add a startup smoke test that calls load_plugin for every configured plugin.
When it happens
Trigger: Calling load_plugin('path/to/plugin.py:MyScorer', Scorer) where myscorer.py defines no MyScorer; a typo in the class name segment; the class is imported under a different name (e.g. `from x import Scorer as Foo`); or an import-path plugin like 'pkg.module.Scorer' where module only exports a function or dict named Scorer.
Common situations: Renaming a plugin class after it was referenced in a TOML config; copy-pasting an example plugin name; the plugin file exporting the class conditionally (e.g. behind `if TYPE_CHECKING` or re-exported without __all__); case-sensitivity typos (myscorer vs MyScorer).
Related errors
- You must append the plugin class name to the filepath like t
- File-based plugin must use the form 'path/to/plugin.py:Class
- Import-based plugin must use the form 'fully.qualified.modul
- Plugin file '{plugin_path}' does not exist
- 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/a23718980fb8d7f2.
Report an issue: GitHub.