Python runpy: Execute Modules and Paths

Published on: August 13, 2026
Reading time: 5 minutes
Executing code representing modules and paths run with Python runpy

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.code

Decide 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'] == 10

Test 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=True in 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 sys changes 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026