Using metapathology¶
The CLI is the primary interface. Use the library API only when the
target cannot be wrapped, such as a notebook, embedded interpreter, or a
project where only conftest.py can be changed.
Run a script¶
The script sees its path in sys.argv[0], the remaining arguments in
sys.argv[1:], and its directory at the front of sys.path, as it would under
a direct Python invocation.
Run a module¶
The module runs as __main__. As with tools such as coverage and cProfile,
execution through Python's runpy module has some differences from a
direct invocation, including __main__ import metadata and
Windows multiprocessing re-import behavior.
Use the shorter command¶
Installation also provides a metapathology executable with the same script
and module modes:
Prefer python -m metapathology when interpreter selection matters: it
guarantees the monitor is installed in the same interpreter and virtual
environment as the target, while the shorter command uses whichever
installation appears first on PATH.
Exit behavior: the target's integer SystemExit status is preserved, and an
unhandled exception prints its traceback and exits with status 1; the report
is written to standard error in both cases. A nonexistent script path is a
CLI error (exit status 2, no report).
Start a monitored interactive interpreter¶
With no target, the CLI installs the monitor and drops into a standard
interactive interpreter, like python itself:
Type imports at the prompt and inspect what happened as you go — the
metapathology module is preloaded, so
print(metapathology.render_report()) shows the report mid-session. The
usual report is also written when the session ends (Ctrl-D, Ctrl-Z, or
exit()). All monitoring options work in this mode:
This is a code.interact() console, not the full 3.13+ REPL: tab completion
is available where readline exists, but multiline editing and syntax
highlighting are not.
Options and report files¶
Put metapathology options before the script or -m target; everything after
the target is passed to the target.
python -m metapathology --report diagnostics.json path/to/script.py
python -m metapathology --report diagnostics.txt --report-format text -m package.module
python -m metapathology --color always path/to/script.py
python -m metapathology --no-path-hook-monitoring path/to/script.py
python -m metapathology --no-importer-cache-monitoring path/to/script.py
python -m metapathology --sys-path-monitoring path/to/script.py
python -m metapathology --deep path/to/script.py
--report PATHwrites the report to a file instead of standard error. Files default to JSON; pass--report-format textfor text. The file is written atomically and the parent directory must exist.- Report filenames automatically include the process ID so concurrent
workers do not overwrite each other:
diagnostics.jsonbecomesdiagnostics.1234.json. Put{pid}in the path to control its position. --color auto(the default) uses ANSI colors only on a TTY, and never whenNO_COLORis set orTERM=dumb.--color alwaysand--color neveroverride. Color never changes report meaning.--no-path-hook-monitoringleaves thesys.path_hookslist object untouched;--no-importer-cache-monitoringskipssys.path_importer_cachesnapshots. Both are on by default and neither ever replaces the cache dictionary.--sys-path-monitoringrecords every ordinarysys.pathlist mutation with its caller stack and detects direct reassignment at the next import. It is off by default and restores a plain list during cleanup.
Environment variables¶
Every option has an environment variable, for situations where you cannot pass CLI flags (frozen apps, the startup bootstrap below). Explicit CLI or API values win over the environment; the environment wins over defaults.
METAPATHOLOGY_REPORT=diagnostics-{pid}.json
METAPATHOLOGY_REPORT_FORMAT=json # or: text
METAPATHOLOGY_COLOR=auto # or: always, never
METAPATHOLOGY_MONITOR_PATH_HOOKS=true
METAPATHOLOGY_MONITOR_IMPORTER_CACHE=true
METAPATHOLOGY_MONITOR_SYS_PATH=false
METAPATHOLOGY_DEEP=false
Boolean variables accept 1/0, true/false, yes/no, and on/off. These
variables configure a monitor that something installs; they never cause
metapathology to install itself (except METAPATHOLOGY_EARLY_BOOTSTRAP,
below).
Reports include command lines, filesystem paths, and stack file names — treat them as potentially sensitive.
Deep diagnostics¶
Deep diagnostics record what the default mode cannot: path hook calls, path
entry finder calls, loader create_module/exec_module calls, and exact
import outcomes. They are off by default because they place monitor code
inline with foreign imports, and wrapping a path hook changes its callable
identity. Use them in a controlled reproduction after the default evidence
proves insufficient; the report warns whenever any are active.
--deep enables all four delegated boundaries plus sys.path mutation
monitoring. Individual switches (--deep-path-hooks,
--deep-path-entry-finders, --deep-loaders, --deep-import-outcomes, each
with a --no- form, and matching METAPATHOLOGY_DEEP_* variables) override
it per mechanism.
Notes on individual mechanisms:
--deep-loadersobserves existingcreate_moduleandexec_modulemethods only; it never adds missing methods or wraps legacyload_module. It can catch a loader that replaces a module object.--deep-import-outcomesrecords whether each import actually loaded or failed, and captures realPathFinderresults, on CPython 3.10–3.14. It uses a profiler slot, so it is refused (without breaking anything) when another profiler is already installed. It covers the installing thread and threads created later throughthreading; the report states the achieved coverage.
Observe later .pth files¶
Normal monitoring starts after Python has already processed the .pth files
in site-packages — which is exactly how some import hooks (for example
scikit-build-core's editable-install finder) are installed. To observe those,
an optional bootstrap moves monitoring into interpreter startup on CPython
3.10–3.14:
Run that with the same interpreter/venv as the target. It creates
00_metapathology_early.pth in that interpreter's site-packages. The file
does nothing unless the activation variable is set:
(PowerShell: $env:METAPATHOLOGY_EARLY_BOOTSTRAP = "1".) The usual report
variables apply, including PID-safe filenames. Both variables are inherited
by child processes, which activate themselves and write their own reports.
Inspect or remove the file with:
All three commands accept --site-packages DIR and are idempotent. The
generated file carries an ownership header, and the manager refuses to touch
a file it does not own. Installing the metapathology package normally never
creates this file.
What it can and cannot see: Python processes .pth files in one directory
in filename order, so the bootstrap (named 00_...) observes files sorted
after it in its own directory — not earlier names, not site directories
Python processed first, and not startup under -S. The report lists the
.pth names that ran before it so the boundary stays visible. Python 3.15
deprecates executable .pth lines, so the command rejects 3.15 and newer.
Install from code¶
Install the process-wide monitor before the imports under investigation:
Installation is idempotent. By default an exit callback writes the report to standard error. For explicit control over timing and destination:
import sys
import metapathology
monitor = metapathology.install(report_at_exit=False)
try:
import package_under_investigation
finally:
metapathology.write_report(sys.stdout, color="auto")
metapathology.uninstall()
uninstall() is also idempotent. It restores plain sys.meta_path and
sys.path_hooks lists, removes the finder wrappers, and unregisters the exit
callback. Recorded events remain available from monitor.events().
The library API reference documents the complete lifecycle and event types.
Integrate with another diagnostic¶
Use metapathology.render_report(format="json") when a harness needs a
machine-readable report string, or inspect the structured event
records returned by monitor.events() (a snapshot;
changing it does not alter the monitor). render_report(color=True) returns
ANSI-styled text; the default is plain because a returned string has no
destination to auto-detect.
Calling write_report() or render_report() before the first install()
raises RuntimeError. See Reading the report for
interpretation.