Skip to content

Read the report

Read from the top and stop when you have an explanation. The text report is organized in three tiers, in this order:

  1. Conclusions — the header (outcome, verdict, and the findings count it points at), findings or explanations, finder comparisons, imports that produced no module, and monitoring errors. Everything a reader almost always needs.
  2. State / reference — run details (the monitoring: line, capture status, and the state snapshots), report-time checks, finder calls, finder API observations, the event-by-event change sections (sys.meta_path/sys.path_hooks/sys.path/importer-cache changes and replacements), the displaced-finder checks, imports attributed to standard finders, and the loader inventory. Background material you consult once you know what you are looking for.
  3. Appendix — the raw event timeline, always last.

Paths below the working directory are shortened in text reports. JSON keeps absolute paths and full object identities.

The header leads with the verdict; the run configuration — the monitoring: line, capture status, and the state snapshots — is grouped separately, further down, under a -- run details -- heading, so you can skip straight to the findings and skip run details until you need it.

Outcome and verdict

target outcome: reports whether the target completed, raised, or called SystemExit. A target exception is not automatically an import-hook problem.

The verdict counts:

  • Problems, where capture connects unusual import behavior to an observed effect.
  • Risks, where the behavior is suspicious but the report cannot prove it caused a failure.
  • Notes, which provide context without alleging a fault.

When a block says observed, metapathology recorded that event while the target ran. Correlated joins several recorded events. Inferred combines recorded activity with state inspected later. The last case deserves more verification.

How a finding reads

Each finding is a numbered block. The first line is the finding tag — for example [missing-namespace-locations] — and the affected module, colored by severity. When metapathology also reconstructed a cause, an indented diagnosis (confidence): … block follows and lists the supporting event numbers, which you can find in the timeline by their ► markers. Each block ends with why it matters, a one-line evidence: (the evidence level, plus any machine-noted caveats), an optional dimmed next step, and a guide link. The severity is not repeated on the evidence: line — it already leads the block as the colored [n] [tag] marker. Plain note findings are listed compactly at the end.

The machine-readable limitations for each finding also stay in the JSON report; the text evidence: line surfaces them as caveats.

Findings

module-executed-again

What it means. The same loader executed the same module name again, but with a different module object. This is not an ordinary importlib.reload(), which normally reuses the object already stored in sys.modules.

Why it matters. Code can retain references to both objects. State, patches, and type identities attached to one object are then invisible through the other. Native extensions may also reject being initialized twice.

What to check. Follow both loader events. Look for code that removed or replaced the first sys.modules entry, or called exec_module() manually with another object. This finding requires loader-call capture.

module-failed-after-loading

What it means. One import loaded the module, then a later import resolved the same normalized origin with the same loader type and failed.

Why it matters. A normal second import is served from sys.modules; it does not resolve and execute the module again. Something made the first cache entry unavailable. Source modules can fail on a second execution, and some native extensions explicitly reject a second load.

What to check. Follow the linked searches and inspect code that removes or replaces the module-cache entry. The report establishes the earlier success, later failure, loader type, and origin. It does not invent an exception message that was not captured.

import-failed-after-state-change

What it means. An import failed after sys.meta_path, sys.path_hooks, sys.path, or the importer cache changed during that same import search.

Why it matters. Changing import state while resolution is in progress can remove the finder or path needed by the import. Timing alone does not prove that this happened.

What to check. Open the linked change and its stack, then ask whether the added, removed, or reordered entry could affect the failed module. This finding requires exact import-result capture.

finder-changed-module-cache

What it means. A finder's find_spec() call changed the target module's sys.modules entry even though the finder returned None or raised.

Why it matters. A finder is expected to decide whether it can locate a module. Loading or replacing that module while making the decision changes what later finders and imports see.

What to check. The report shows the cache state before and after the call. Inspect that finder's find_spec() implementation for an import of the target or a related module. The MetaPathFinder.find_spec() contract is useful when reporting the behavior upstream.

module-replacement

What it means. During a loader's create_module() or exec_module() call, the object stored for the module name changed, or the loader was given a different object from the one in sys.modules.

Why it matters. Recursive imports and existing references may see a different module object from later imports. Lazy-loading systems sometimes do this deliberately, so the finding is about identity, not necessarily a bug.

What to check. Compare the object identities shown in the finding. If the replacement is intentional, check whether code keeps early references. If it is not intentional, start with the named loader. This finding requires loader-call capture.

missing-namespace-locations

What it means. A custom finder returned a namespace package with fewer search locations than PathFinder found, and an import below one of the omitted locations failed.

Why it matters. A namespace package can span several directories. Its __path__ must preserve every contributing location; omitting one makes modules in that directory unreachable.

What to check. The explanation names the missing location and failed descendant. Editable-install finders are a common source, so try reinstalling the distribution, comparing editable and regular installs, and reporting the omission to the build backend or finder maintainer.

Further investigation. If files or import state may have changed before reporting, unsafe branch exploration can ask the skipped PathFinder during a disposable reproduction.

module-hides-namespace

What it means. PathFinder saw a namespace-package portion in an earlier sys.path entry, continued searching, and selected a regular module or package from a later entry. An import below the selected name then failed.

Why it matters. Continuing after a namespace portion is normal Python behavior. The problem is the collision: a later regular module wins and may not support the descendants that existed under the namespace directory.

What to check. The explanation names the namespace location, selected file, and failed descendant. Fix path order, rename the colliding module, or make the selected object a package if descendants are intended.

competing-path-hooks

What it means. Two different path hooks accepted the same path in observed calls.

Why it matters. PathFinder tries sys.path_hooks in order and caches the first path-entry finder that accepts a path. The other hook does not process imports from that path.

What to check. Confirm that the acceptances refer to comparable path state; the report may have observed them at different times. Reordering hooks and clearing sys.path_importer_cache can demonstrate the conflict, but it merely chooses the other behavior. The durable fix is for the tools to cooperate, often by delegating to or wrapping FileFinder. This finding requires path-hook-call capture.

Further investigation. To see whether the skipped hook's finder can locate the affected module, use unsafe branch exploration in a disposable reproduction.

legacy-finder-api

What it means. A meta-path finder has callable find_module() but no callable find_spec().

Why it matters. find_module() is the pre-3.4 API. Python 3.12 removed the fallback that called it, so newer interpreters skip that finder. Code that assumes every meta-path entry has find_spec() can also fail; pytest#12179 is a real example.

What to check. Use the linked sys.meta_path change to identify what installed the finder, then upgrade or replace that package. Metapathology inspects the methods without calling them.

module-without-spec

What it means. A module is in sys.modules without __spec__, and no recorded finder call explains it.

Why it matters. Normal imports create a ModuleSpec. A missing spec often means code created the module manually or executed it through lower-level loader APIs, bypassing tools that rely on import hooks.

What to check. Many such modules are harmless—some standard-library modules are created this way. Investigate only when the named module should have passed through another import hook. Then look for types.ModuleType, manual sys.modules insertion, or direct loader execution.

Finder results and comparisons

For modules found by a custom finder, the report shows two aligned rows so the finder, loader, and origin line up column-for-column:

  • at runtime: the result that finder actually returned during the run.
  • PathFinder now: what PathFinder.find_spec() returns for the same name and recorded search path when the report is built.

A different loader is often legitimate. Editable installs, assertion rewriters, and instrumentation tools exist specifically to use custom loaders. The comparison becomes useful when another tool expected the standard path search to run. In beartype#556, an editable-install finder handled the module first, so beartype's path hook never saw it.

The standard search uses current files, hooks, and caches. It also calls PathFinder directly, skipping other meta-path finders. Read it as “what the standard path search returns now,” not “what would have won earlier.”

If that timing difference matters, use unsafe branch exploration in a disposable reproduction to ask later finders during the import.

When several modules are listed, caveats that apply to all of them — that they ran before PathFinder, the branch-exploration next step, and a warning that import state changed since monitoring started — appear once at the top of the section instead of on every entry. That last warning is shown only when path hooks or the importer cache actually changed after monitoring began, since the report-time search then reflects different state than the run.

The optional displaced-finder check investigates a different pattern: an importer-cache entry changed, a later lookup through that path failed, and the old finder is still available to ask about the failed module. It checks at most 16 candidates per report and states when more were omitted.

An explored result says that a skipped candidate returned a spec, returned nothing, or raised when called. Check its timeline events for call order and module-state changes.

predicts_alternative_winner is always false. Before changing finder order, inspect the spec difference and look for an independently observed effect.

Imports that did not produce a module

This section lists import searches that left no module in sys.modules. Failed optional imports are common and harmless. Pay attention when the target failed for the same module; the report marks it.

The import started at event #n reference points at CPython's import audit event, which the timeline shows as search started. It says resolution started, not that the import succeeded or which finder won. Imports served from the module cache do not emit this event. On CPython 3.15 and newer, these events also cover searches started through importlib.import_module(); earlier CPython versions omit those searches.

Finder calls

This section counts calls to instrumented finder instances and lists the modules they found. Standard CPython class finders are deliberately not modified; their work appears in the standard-resolution section instead.

Event timeline and change sections

The change sections (sys.meta_path changes, sys.path_hooks changes, and the rest listed below) sit in tier 2, alongside run details and the other reference material. The event timeline itself is the appendix: it is always the last section in the report.

The timeline orders all recorded events under one #n sequence so findings can point back to them. On multi-threaded runs, this is monitor-recording order, not a precise wall-clock order.

Each line has three aligned columns under a #seq | category | detail header: the #n sequence gutter, a short category naming the machinery involved (import, __import__, find_spec, PathFinder, path entry, path hook, loader, meta_path, path_hooks, sys.path, cache, explore), and the detail. A collapsed run of routine events is tagged ⋯. Rows a finding or explanation cites are marked with ► (and bold in color) so you can find them while scanning. Colors flag only anomalies: failures are red, routine non-results and cache hits are dimmed, and ordinary success stays plain.

When import-outcome capture is on, an import's audit start and its started lifecycle event merge into one search started line carrying both #n anchors.

Detailed calls are recorded when they complete. A nested detailed outcome therefore points to its enclosing call's #n, which can appear later in the timeline. JSON exposes the stable call references directly.

Long runs of routine events — repeated finder declines, path-entry misses, and cache-hit __import__ calls — collapse into one summary line in text. JSON is always complete.

An import that ran to completion without triggering another import folds the same way: its whole lifecycle becomes one import row naming the loader and origin it resolved to, with the #n range it stands for in the gutter. Only such leaf imports fold, so the tree of nested imports stays readable; an import whose execution started another one keeps every row. Folding needs import-outcome capture, since the loaded event is what marks a lifecycle complete, and it never applies to a failed import or to any event a finding cites.

The text report has three verbosity levels, controlled by --report-verbosity (CLI) or the report_verbosity keyword (library API): summary shows only the verdict, findings, monitoring errors, and run details, ending with a line stating how many sections were omitted; standard is the ordinary report described above; full shows every timeline event with no collapsing or line merging. Without an explicit setting, the METAPATHOLOGY_REPORT_VERBOSITY environment variable is checked next; if that is unset too, the level depends on the destination — a terminal defaults to summary, a file to standard. The JSON report is unaffected by verbosity; it is always complete.

The tier-2 change sections each answer a specific question:

  • sys.meta_path changes: who added, removed, or reordered finders?
  • sys.meta_path replacements: when did metapathology discover that code assigned a new list? The shown stack belongs to the next import, because plain assignment cannot be intercepted.
  • sys.path_hooks changes and replacements: who changed path-hook order?
  • sys.path changes and replacements: what changed when --sys-path was enabled?
  • sys.path_importer_cache changes: which paths gained, lost, or switched path-entry finders between snapshots?
  • Loader inventory: which loader does each module report at report time?

Monitoring errors — where metapathology lost evidence while allowing the target import to continue — get their own tier-1 section, ahead of run details, so they never hide below the timeline.

Report-time checks

active means the check ran. disabled means configuration turned it off. unavailable means it was requested but a required capture mechanism was explicitly disabled; the report lists what is missing.

What “no problems found” means

It means the enabled capture mechanisms did not produce a problem finding. It does not certify the entire import history. Check the monitoring: line and limitations, especially when the relevant finder was installed before monitoring or the import was served from sys.modules.