Skip to content

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

python -m metapathology path/to/script.py argument-one argument-two

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

python -m metapathology -m package.module argument-one

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:

metapathology path/to/script.py argument-one
metapathology -m package.module argument-one

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:

python -m metapathology

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:

python -m metapathology --deep
python -m metapathology --report session.json

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 PATH writes the report to a file instead of standard error. Files default to JSON; pass --report-format text for 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.json becomes diagnostics.1234.json. Put {pid} in the path to control its position.
  • --color auto (the default) uses ANSI colors only on a TTY, and never when NO_COLOR is set or TERM=dumb. --color always and --color never override. Color never changes report meaning.
  • --no-path-hook-monitoring leaves the sys.path_hooks list object untouched; --no-importer-cache-monitoring skips sys.path_importer_cache snapshots. Both are on by default and neither ever replaces the cache dictionary.
  • --sys-path-monitoring records every ordinary sys.path list 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-loaders observes existing create_module and exec_module methods only; it never adds missing methods or wraps legacy load_module. It can catch a loader that replaces a module object.
  • --deep-import-outcomes records whether each import actually loaded or failed, and captures real PathFinder results, 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 through threading; 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:

python -m metapathology.site_bootstrap install

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:

METAPATHOLOGY_EARLY_BOOTSTRAP=1 \
METAPATHOLOGY_REPORT=diagnostics.json \
python path/to/script.py

(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:

python -m metapathology.site_bootstrap status
python -m metapathology.site_bootstrap remove

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:

import metapathology

monitor = metapathology.install()

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.