Python’s runpy module runs a named module or a path as a top-level __main__ script — the same mechanism used by python -m mod. import runpy. Run by name: runpy.run_module(mod_name, run_name="__main__", alter_sys=False, init_globals=None) → dict — locates mod_name on sys.path, executes it, returns the resulting globals dict. Run by path: runpy.run_path(path_name, run_name="__main__", init_globals=None) → dict — accepts a .py file, a directory with __main__.py, or a zip archive with __main__.py; does NOT permanently alter sys.path. Parameters: run_name — overrides __name__ inside the module (default "__main__"); alter_sys — if True, temporarily sets sys.argv[0] to the module’s file and adjusts sys.modules; init_globals — dict of extra names injected into the namespace before execution. Return value: the final __dict__ of the module (all names defined or imported during execution). Both functions isolate execution: the module runs in its own namespace, not the caller’s. Claude Code generates in-process script runners, isolated namespace executors, plugin sandboxes, and test harnesses.
CLAUDE.md for runpy
## runpy Stack
- Stdlib: import runpy
- By name: ns = runpy.run_module("mypackage.script", run_name="__main__")
- By path: ns = runpy.run_path("scripts/deploy.py")
- Inject: ns = runpy.run_path("tool.py", init_globals={"CONFIG": cfg})
- alter_sys: runpy.run_module("mod", alter_sys=True) # updates sys.argv[0]
- Note: returns the module __dict__; raises SystemExit if script calls sys.exit()
runpy Script Execution Pipeline
# app/runpyutil.py — in-process runner, namespace capture, plugin sandbox, harness
from __future__ import annotations
import runpy
import sys
import traceback
from contextlib import contextmanager
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Simple run helpers
# ─────────────────────────────────────────────────────────────────────────────
def run_module(
mod_name: str,
init_globals: "dict[str, Any] | None" = None,
run_name: str = "__main__",
alter_sys: bool = False,
) -> "dict[str, Any]":
"""
Run a module by name, returning its namespace dict.
Propagates SystemExit; wraps other exceptions in RuntimeError.
Example:
ns = run_module("http.server") # starts server; intercepted by SystemExit
"""
return runpy.run_module(
mod_name,
init_globals=init_globals,
run_name=run_name,
alter_sys=alter_sys,
)
def run_path(
path: "str | Path",
init_globals: "dict[str, Any] | None" = None,
run_name: str = "__main__",
) -> "dict[str, Any]":
"""
Run a .py file (or directory/__main__.py or zip/__main__.py) by path.
Returns the resulting namespace dict.
Example:
ns = run_path("scripts/compute.py", init_globals={"N": 100})
print(ns.get("result"))
"""
return runpy.run_path(str(path), init_globals=init_globals, run_name=run_name)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Captured run — catch exceptions and SystemExit
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class RunResult:
"""
Result of a captured script/module execution.
Example:
result = safe_run_path("scripts/build.py")
if result.ok:
print(result.namespace.get("OUTPUT"))
else:
print(f"Error: {result.error}")
"""
namespace: "dict[str, Any]"
ok: bool
exit_code: "int | None" # set when SystemExit was raised
error: str # formatted traceback / exception message
exc: "BaseException | None"
def __str__(self) -> str:
if self.ok:
return f"RunResult(ok, vars={list(self.namespace.keys())[:5]})"
if self.exit_code is not None:
return f"RunResult(exit={self.exit_code})"
return f"RunResult(error={self.error[:80]!r})"
def safe_run_path(
path: "str | Path",
init_globals: "dict[str, Any] | None" = None,
run_name: str = "__main__",
) -> RunResult:
"""
Run a script by path, capturing exceptions and SystemExit.
Example:
result = safe_run_path("scripts/report.py", init_globals={"DATE": "2026-01"})
if not result.ok:
print(result.error)
"""
try:
ns = runpy.run_path(str(path), init_globals=init_globals, run_name=run_name)
return RunResult(namespace=ns, ok=True, exit_code=None, error="", exc=None)
except SystemExit as e:
code = e.code if isinstance(e.code, int) else (0 if e.code is None else 1)
return RunResult(namespace={}, ok=(code == 0), exit_code=code,
error=str(e), exc=e)
except Exception as e:
tb = traceback.format_exc()
return RunResult(namespace={}, ok=False, exit_code=None, error=tb, exc=e)
def safe_run_module(
mod_name: str,
init_globals: "dict[str, Any] | None" = None,
run_name: str = "__main__",
) -> RunResult:
"""
Run a module by name, capturing exceptions and SystemExit.
Example:
result = safe_run_module("myapp.cli", init_globals={"DRYRUN": True})
"""
try:
ns = runpy.run_module(mod_name, init_globals=init_globals, run_name=run_name)
return RunResult(namespace=ns, ok=True, exit_code=None, error="", exc=None)
except SystemExit as e:
code = e.code if isinstance(e.code, int) else (0 if e.code is None else 1)
return RunResult(namespace={}, ok=(code == 0), exit_code=code,
error=str(e), exc=e)
except Exception as e:
tb = traceback.format_exc()
return RunResult(namespace={}, ok=False, exit_code=None, error=tb, exc=e)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Namespace extractor
# ─────────────────────────────────────────────────────────────────────────────
def extract_globals(
path: "str | Path",
names: "list[str]",
init_globals: "dict[str, Any] | None" = None,
) -> "dict[str, Any]":
"""
Run a script and extract only the named variables from its namespace.
Returns {name: value} for names that exist; absent names are omitted.
Example:
vals = extract_globals("config.py", ["DB_URL", "DEBUG", "SECRET_KEY"])
print(vals)
"""
result = safe_run_path(path, init_globals=init_globals)
if not result.ok and result.exit_code != 0:
raise RuntimeError(f"Script failed: {result.error}")
return {k: result.namespace[k] for k in names if k in result.namespace}
# ─────────────────────────────────────────────────────────────────────────────
# 4. Plugin sandbox
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class PluginResult:
"""
Result of loading and running a plugin script.
Plugins can expose a dict named EXPORTS or any top-level callable.
"""
path: Path
ok: bool
exports: "dict[str, Any]"
error: str
def get(self, name: str, default: Any = None) -> Any:
return self.exports.get(name, default)
def load_plugin(
plugin_path: "str | Path",
api: "dict[str, Any] | None" = None,
) -> PluginResult:
"""
Load a plugin script via runpy.run_path, injecting api as init_globals.
Plugins signal their exports via an EXPORTS dict in their namespace.
Example:
result = load_plugin("plugins/report.py", api={"db": db_conn})
if result.ok:
fn = result.exports.get("generate")
if callable(fn):
fn()
"""
init = dict(api or {})
result = safe_run_path(plugin_path, init_globals=init)
if not result.ok:
return PluginResult(
path=Path(plugin_path), ok=False, exports={}, error=result.error
)
exports = result.namespace.get("EXPORTS", {})
if not isinstance(exports, dict):
exports = {}
# Also expose any top-level callables defined by the plugin
for k, v in result.namespace.items():
if callable(v) and not k.startswith("_") and k not in exports:
exports[k] = v
return PluginResult(path=Path(plugin_path), ok=True, exports=exports, error="")
def load_plugins_from_dir(
directory: "str | Path",
api: "dict[str, Any] | None" = None,
pattern: str = "plugin_*.py",
) -> list[PluginResult]:
"""
Load all plugin scripts matching pattern from directory.
Example:
plugins = load_plugins_from_dir("extensions/", api={"registry": reg})
for p in plugins:
if p.ok:
print(f" {p.path.name}: exports={list(p.exports.keys())}")
"""
results = []
for py_file in sorted(Path(directory).glob(pattern)):
results.append(load_plugin(py_file, api=api))
return results
# ─────────────────────────────────────────────────────────────────────────────
# 5. Test harness helper
# ─────────────────────────────────────────────────────────────────────────────
@contextmanager
def patched_argv(argv: list[str]):
"""
Context manager that temporarily replaces sys.argv.
Useful when running scripts that parse argv.
Example:
with patched_argv(["script.py", "--verbose", "--output", "out.txt"]):
ns = runpy.run_path("script.py")
"""
original = sys.argv[:]
sys.argv[:] = argv
try:
yield
finally:
sys.argv[:] = original
def run_as_main(
path: "str | Path",
argv: "list[str] | None" = None,
init_globals: "dict[str, Any] | None" = None,
) -> RunResult:
"""
Run a script as __main__ with optional argv substitution.
Example:
result = run_as_main("tools/gen.py", argv=["gen.py", "--count", "10"])
print(result.namespace.get("OUTPUT"))
"""
effective_argv = argv or [str(path)]
with patched_argv(effective_argv):
return safe_run_path(path, init_globals=init_globals, run_name="__main__")
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile
print("=== runpy demo ===")
with tempfile.TemporaryDirectory() as td:
td_path = Path(td)
# ── write test scripts ─────────────────────────────────────────────────
compute = td_path / "compute.py"
compute.write_text(
"import math\n"
"N = N if 'N' in dir() else 10\n" # uses injected N if present
"result = sum(math.sqrt(i) for i in range(1, N + 1))\n"
)
config = td_path / "config.py"
config.write_text(
"DB_URL = 'sqlite:///app.db'\n"
"DEBUG = True\n"
"SECRET_KEY = 'dev-secret'\n"
)
failing = td_path / "failing.py"
failing.write_text(
"import sys\n"
"raise RuntimeError('intentional failure')\n"
)
exiting = td_path / "exiting.py"
exiting.write_text(
"import sys\n"
"sys.exit(2)\n"
)
plugin_a = td_path / "plugin_alpha.py"
plugin_a.write_text(
"def transform(data):\n return [x * 2 for x in data]\n"
"EXPORTS = {'transform': transform, 'version': '1.0'}\n"
)
plugin_b = td_path / "plugin_beta.py"
plugin_b.write_text(
"def transform(data):\n return sorted(data, reverse=True)\n"
"EXPORTS = {'transform': transform, 'version': '2.0'}\n"
)
# ── run_path with inject ───────────────────────────────────────────────
print("\n--- run_path with init_globals ---")
ns = run_path(compute, init_globals={"N": 5})
print(f" result (N=5): {ns['result']:.4f}")
ns2 = run_path(compute)
print(f" result (default N=10): {ns2['result']:.4f}")
# ── extract_globals ───────────────────────────────────────────────────
print("\n--- extract_globals ---")
cfg = extract_globals(config, ["DB_URL", "DEBUG", "SECRET_KEY", "MISSING"])
for k, v in cfg.items():
print(f" {k} = {v!r}")
# ── safe_run_path (exception) ─────────────────────────────────────────
print("\n--- safe_run_path (exception) ---")
r = safe_run_path(failing)
print(f" ok={r.ok} exit_code={r.exit_code}")
print(f" error snippet: {r.error.splitlines()[-1]!r}")
# ── safe_run_path (SystemExit) ────────────────────────────────────────
print("\n--- safe_run_path (SystemExit) ---")
r2 = safe_run_path(exiting)
print(f" ok={r2.ok} exit_code={r2.exit_code}")
# ── load_plugins_from_dir ─────────────────────────────────────────────
print("\n--- load_plugins_from_dir ---")
plugins = load_plugins_from_dir(td_path)
for p in plugins:
if p.ok:
fn = p.exports.get("transform")
out = fn([3, 1, 4, 1, 5]) if callable(fn) else None
print(f" {p.path.name}: version={p.exports.get('version')} "
f"transform([3,1,4,1,5])={out}")
# ── run_as_main with argv ─────────────────────────────────────────────
print("\n--- run_as_main with custom argv ---")
argv_script = td_path / "printargs.py"
argv_script.write_text(
"import sys\n"
"args = sys.argv[1:]\n"
)
r3 = run_as_main(argv_script, argv=["printargs.py", "--foo", "bar"])
print(f" args captured: {r3.namespace.get('args')}")
# ── run_module (stdlib) ───────────────────────────────────────────────
print("\n--- run_module (json.tool help check) ---")
r4 = safe_run_module("json.tool", init_globals={})
# json.tool exits with error if no input; just check it ran
print(f" json.tool: ok={r4.ok} exit_code={r4.exit_code}")
print("\n=== done ===")
For the exec() built-in alternative — exec(compile(source, filename, "exec"), namespace) runs compiled source in a provided dict — use exec when you already have source text or a code object and want direct namespace control without involving the import machinery; use runpy.run_path when you want Python’s standard import-path rules (including __file__, __spec__, __loader__ setup) and zip/package support. For the subprocess.run(["python", "script.py"]) alternative — subprocess.run executes a script in a completely isolated child process with its own interpreter state — use subprocess when you need strict isolation, separate stdout/stderr capture, or cross-interpreter compatibility; use runpy when you want in-process execution and access to the script’s namespace dict without spawning a process. The Claude Skills 360 bundle includes runpy skill sets covering run_module()/run_path() thin wrappers, RunResult with ok/exit_code/error/namespace fields and safe_run_path()/safe_run_module() exception-catching runners, extract_globals() selective namespace reader, PluginResult + load_plugin()/load_plugins_from_dir() plugin sandbox, patched_argv() context manager, and run_as_main() with argv injection. Start with the free tier to try in-process script execution patterns and runpy pipeline code generation.