p-e-w/heretic · error · ValueError
You must append the plugin class name to the filepath like t
Error message
You must append the plugin class name to the filepath like this: path/to/plugin.py:ClassName
What it means
load_plugin() rejects bare '.py' file paths because a file alone cannot identify which class inside it to load. The library throws this to force the explicit 'path/to/plugin.py:ClassName' form required by its loader.
Source
Thrown at src/heretic/plugin.py:85
- `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'"
)
plugin_path = Path(file_path)
if not plugin_path.is_absolute():
plugin_path = Path.cwd() / plugin_path
plugin_path = plugin_path.resolve()
if not plugin_path.is_file():
raise ImportError(f"Plugin file '{plugin_path}' does not exist")View on GitHub (pinned to bedb94ef11)
Solutions
- Append ':ClassName' to the path, where ClassName is the plugin class defined in that file.
- Quote the argument in your shell so the ':' is not mangled.
- If you meant an importable package plugin, use the dotted form 'pkg.module.ClassName' instead.
Example fix
# before heretic --scorer plugins/my_scorer.py # after heretic --scorer plugins/my_scorer.py:MyScorer
Defensive patterns
Strategy: validation
Validate before calling
def is_valid_file_plugin(name: str) -> bool:
return ":" in name and name.split(":", 1)[0].endswith(".py") and name.endswith(":") is False and bool(name.rsplit(":", 1)[1]) Type guard
def looks_like_file_plugin(name: str) -> bool:
return name.endswith(".py") is False and ":" in name and bool(name.rsplit(":", 1)[1]) Try / catch
try:
cls = load_plugin(name, Scorer)
except ValueError as e:
sys.exit(f"Plugin spec must be path/to/file.py:ClassName — got '{name}'") Prevention
- Always write file plugin refs as 'path.py:ClassName'.
- Quote args in shell so ':' is preserved.
- Note bare '.py' paths are explicitly rejected by design.
When it happens
Trigger: Passing 'plugins/my_scorer.py' (no ':' and no class suffix) to load_plugin or a config/CLI field that resolves to a scorer plugin; using a path copied from an old version of the tool where the class name was inferred.
Common situations: Config written for an older heretic version where file plugins took just a path; forgetting the ':ClassName' suffix after migrating from directory-based plugin discovery; shell-quoting split the name at ':'.
Related errors
- Plugin '{name}' does not export a class named '{class_name}'
- 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/0e1f19f27ab572f7.
Report an issue: GitHub.