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

  1. Append ':ClassName' to the path, where ClassName is the plugin class defined in that file.
  2. Quote the argument in your shell so the ':' is not mangled.
  3. 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

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


AI-assisted analysis of p-e-w/heretic@bedb94ef11 (2026-08-29). Data as JSON: /api/errors/0e1f19f27ab572f7. Report an issue: GitHub.