p-e-w/heretic · error · ImportError

Plugin file '{plugin_path}' does not exist

Error message

Plugin file '{plugin_path}' does not exist

What it means

The .py path portion of the plugin name (resolved to an absolute path via Path.cwd() if relative) does not exist on disk. load_plugin() raises ImportError before attempting any import so the user gets a clear missing-file message instead of a low-level import traceback.

Source

Thrown at src/heretic/plugin.py:103

        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)
        if module is None:
            spec = importlib.util.spec_from_file_location(module_name, plugin_path)
            if spec is None or spec.loader is None:
                raise ImportError(
                    f"Could not load plugin '{name}' (invalid module spec)"
                )

            module = importlib.util.module_from_spec(spec)

            # Cache before executing to match normal import semantics and allow

View on GitHub (pinned to bedb94ef11)

Solutions

  1. Check the resolved path printed in the message and fix the file location or the path in the config.
  2. Use an absolute path to make the plugin independent of the working directory.
  3. If relative, cd into the expected directory or fix your launcher's cwd.
  4. Copy/mount the plugin file into the environment (e.g. Docker volume) where heretic runs.

Example fix

# before
scorer = "scorers/keyword_rate.py:KeywordRate"  # run from repo root only
# after
scorer = "/opt/myproj/scorers/keyword_rate.py:KeywordRate"
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
def plugin_file_exists(name: str) -> bool:
    p = Path(name.rsplit(":", 1)[0])
    if not p.is_absolute():
        p = Path.cwd() / p
    return p.resolve().is_file()

Try / catch

try:
    cls = load_plugin(name, Scorer)
except ImportError as e:
    sys.exit(str(e))  # includes resolved missing path

Prevention

When it happens

Trigger: load_plugin('plugins/missing.py:MyScorer', ...) where plugins/missing.py does not exist; relative path given while running from a different working directory; typo in the path; file deleted or never committed.

Common situations: Running heretic from a different cwd than the config assumes (relative paths resolve against Path.cwd()); Docker containers without the plugin file mounted; renames/moves of the plugin file; case-sensitive filesystems (MyScorer.py vs myscorer.py on Linux).

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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