Python runpy locates and executes code through the module system. It implements part of the behavior behind python -m package.module and can also run scripts, directories, and ZIP archives containing a top-level __main__.py.
The module is useful for launchers, testing tools, automation, and systems that need to execute an entry point and receive the resulting globals dictionary. It is not a sandbox. Code runs in the current process and may modify files, networks, environment variables, import caches, logging, signals, threads, and other global state.
Execute a module by name
run_module() accepts an absolute module name and locates it through the normal import machinery.
import runpy
result = runpy.run_module('my_app.diagnostics')
print(result.keys())The code executes in a fresh namespace and the function returns that dictionary. This differs from a normal import, which creates or reuses a persistent entry in sys.modules.
Execute a package
When the name refers to a package, run_module() finds and executes package.__main__.
result = runpy.run_module('my_app')
# executes my_app.__main__This reproduces the core behavior of python -m my_app. The parent package may be imported during discovery, so its initializer can still produce side effects.
Special globals
Before execution, runpy defines values such as __name__, __spec__, __file__, __cached__, __loader__, and __package__.
result = runpy.run_module(
'my_app.diagnostics',
run_name='__main__',
)
print(result['__name__'])__spec__.name still identifies the real module even when run_name changes. Since Python 3.12, direct setting of some legacy globals is deprecated; code should prefer the module spec.
Pre-populate the namespace
init_globals supplies initial values without modifying the original mapping.
context = {
'SETTINGS': {'mode': 'test'},
'SERVICE': fake_service,
}
result = runpy.run_module(
'my_app.job',
init_globals=context,
)Special values controlled by runpy override matching entries. Supplying powerful objects also gives executed code access to their capabilities.
Trigger script-style behavior
Many modules use the familiar guard:
if __name__ == '__main__':
main()Set run_name='__main__' to trigger it.
runpy.run_module(
'my_app.cli',
run_name='__main__',
)This executes the module like a script but remains inside the current process. Functions and classes in the returned dictionary are not guaranteed to behave correctly after runpy returns; use a normal import when you need reusable APIs.
alter_sys and -m compatibility
With alter_sys=True, run_module() temporarily updates sys.argv[0] and installs a temporary module object in sys.modules.
result = runpy.run_module(
'my_app.cli',
run_name='__main__',
alter_sys=True,
)This more closely resembles command-line execution, but it is not thread-safe. Other threads may observe changed arguments or a partially initialized module. Leave alter_sys=False in threaded services or delegate execution to another process.
Execute a filesystem path
run_path() executes code at a named location.
import runpy
result = runpy.run_path('scripts/report.py')
print(result.get('RESULT'))The path may refer to source code, compiled bytecode, or a valid sys.path entry such as a directory or ZIP containing __main__.py.
Run directories and ZIP applications
For a path entry, runpy temporarily places it at the start of sys.path and searches for __main__.
runpy.run_path('dist/tool.pyz', run_name='__main__')This works with artifacts created by Python zipapp. Validate the archive first: if the specified location lacks __main__, another module of that name elsewhere on sys.path may be found.
run_path always alters sys
Running a directory or ZIP requires temporary changes to sys.path, sys.argv[0], and sys.modules. The values are restored later, but other threads can observe the intermediate state.
Serialize such calls or, preferably, run the target in a subprocess. A separate process also provides timeouts and failure isolation.
runpy is not a sandbox
Executed code has ordinary Python and operating-system access.
from pathlib import Path
Path('/tmp/marker').write_text('executed')Removing a few names from init_globals does not isolate the code. For third-party input, use a disposable process or container, an unprivileged user, a restricted filesystem, disabled networking, and resource quotas.
Side effects remain
The primary globals dictionary is fresh, but imports performed during execution remain cached in sys.modules. Environment changes, handlers, background threads, signal configuration, and library state may also persist.
When you need repeatable clean execution, a process boundary is more reliable than attempting to undo every effect.
runpy or importlib?
Use runpy when the goal is to execute an entry point with script semantics. Use importlib.import_module() when you want a module object and supported access to its functions and classes.
The documentation warns that definitions created by runpy may not work correctly after the function returns. For static import discovery, see Python modulefinder.
runpy or subprocess?
Runpy is fast and shares memory, but it also shares failures and global state. A subprocess offers a real argument vector, exit status, timeout, output capture, and isolation.
import subprocess
import sys
subprocess.run(
[sys.executable, '-m', 'my_app.cli'],
check=True,
timeout=30,
)Prefer subprocess for untrusted plugins, long-running tasks, concurrent execution, or code that might terminate the interpreter.
Read results from globals
The returned dictionary may expose values produced by a script.
result = runpy.run_path(
'calculation.py',
init_globals={'INPUT': 21},
)
print(result['OUTPUT'])Define a small explicit contract, including required names and expected types. Do not serialize the entire namespace because it may contain modules, functions, files, and sensitive objects.
Handle SystemExit
Executed code may call sys.exit(), which raises SystemExit.
try:
runpy.run_module('my_app.cli', run_name='__main__')
except SystemExit as exc:
exit_code = exc.codeDecide whether the code should propagate, become a result, or be logged as a failure. Handle KeyboardInterrupt at the outer boundary as well.
Exceptions and diagnostics
Import, syntax, and runtime errors propagate. Record the target module or path, Python version, and sanitized arguments.
Use the techniques from Python traceback for useful internal diagnostics while keeping sensitive details away from external users.
Controlled task launchers
An internal tool may map friendly actions to approved module names.
TASKS = {
'migrate': 'my_app.migrations.apply',
'verify': 'my_app.diagnostics',
}
module = TASKS[action]
runpy.run_module(module, run_name='__main__')Use an allowlist. Never accept arbitrary importable names or filesystem paths from a request.
Concurrency
Even with alter_sys=False, executed code may mutate process-global state. With alter_sys=True or run_path(), thread-safety concerns increase.
In asynchronous applications, avoid heavy execution on the event loop. Use process workers and limit concurrent jobs.
Test a runpy wrapper
Create small temporary scripts with explicit contracts.
def test_run_path(tmp_path):
script = tmp_path / 'job.py'
script.write_text('OUTPUT = INPUT * 2\n', encoding='utf-8')
result = runpy.run_path(
str(script),
init_globals={'INPUT': 5},
)
assert result['OUTPUT'] == 10Test SystemExit, missing modules, exceptions, ZIPs without __main__, global-state changes, and concurrent calls.
Relationship to code and codeop
Python code creates persistent REPLs. Codeop detects incomplete interactive input. Runpy executes complete units identified by a module name or path.
Choose the abstraction that matches the experience. Do not use runpy as an improvised interactive sandbox.
Common mistakes
- Treating runpy as a sandbox.
- Using
alter_sys=Truein multithreaded code. - Executing an unvalidated path.
- Assuming every side effect is reverted.
- Reusing returned definitions without guarantees.
- Accepting arbitrary module names from users.
- Ignoring SystemExit and timeouts.
Best practices
- Use allowlists for modules and paths.
- Prefer importlib for reusable APIs.
- Prefer subprocess for isolation.
- Avoid temporary
syschanges in threads. - Validate ZIPs and
__main__.py. - Define a small result contract.
- Log failures without secrets.
Conclusion
Python runpy programmatically executes modules and filesystem paths with semantics similar to python -m and script execution. It supports packages, directories, ZIPs, and returned globals.
Use it only for trusted code in the current process. For concurrency, isolation, and security, prefer subprocesses. Consult the official runpy documentation and the documentation for the -m option.







