p-e-w/heretic · error · ValueError
File-based plugin must use the form 'path/to/plugin.py:Class
Error message
File-based plugin must use the form 'path/to/plugin.py:ClassName'
What it means
A name containing ':' was given, but either the part before ':' does not end in '.py' or the class part after ':' is empty. load_plugin() throws this to enforce the strict 'path/to/plugin.py:ClassName' syntax for file-based plugins.
Source
Thrown at src/heretic/plugin.py:93
"""
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")
# We're writing directly to the sys.modules dict,
# so the typical restrictions on module names
# (no dots, slashes, etc.) don't apply.
module_name = f"heretic_plugin_{plugin_path}"
# Reuse already-loaded modules to avoid re-executing the plugin on repeated loads.
module = sys.modules.get(module_name)View on GitHub (pinned to bedb94ef11)
Solutions
- Make sure the path segment ends with '.py'.
- Ensure a non-empty class name follows the ':'.
- Verify with Path(file).is_file() that the target exists before running.
Example fix
# before scorer = "plugins/my_scorer.py:" # after scorer = "plugins/my_scorer.py:MyScorer"
Defensive patterns
Strategy: validation
Validate before calling
import re
FILE_PLUGIN_RE = re.compile(r"^.+\.py:.+$")
def valid_file_plugin(name: str) -> bool:
return ":" in name and bool(FILE_PLUGIN_RE.match(name)) Try / catch
try:
cls = load_plugin(name, Scorer)
except ValueError:
sys.exit("use path/to/plugin.py:ClassName (non-empty .py path and class name)") Prevention
- Check the file ends in .py and the class name after ':' is non-empty.
- Avoid extra colons in paths on Windows except the single separator before ClassName.
When it happens
Trigger: load_plugin('plugins/my_scorer.txt:MyScorer', ...) with a non-.py extension; load_plugin('plugins/my_scorer.py:', ...) with an empty class name; Windows-style drive letters only in the segment (rare, handled by rsplit(':',1) but a truncated path like 'C:plugin.py:Foo' can slip through).
Common situations: Pointing the config at a compiled/other file type; leaving a trailing ':' after deleting the class name; typos like '..py' or '.pyy'.
Related errors
- Plugin '{name}' does not export a class named '{class_name}'
- You must append the plugin class name to the filepath like t
- 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/efe7bb4ecbea67f2.
Report an issue: GitHub.