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:
- 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.
- 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. - 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 changesand replacements: who changed path-hook order?sys.path changesand replacements: what changed when--sys-pathwas 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.