What does runpy.run_path give you that a normal import does not?
answer
- Executing a file is not importing it
- Nothing is cached, so it runs every time
- You get the resulting namespace back
- The caller chooses what __name__ will be
- Same machinery that implements the -m switch
basics
~20 srunpy.run_path executes the code at a filesystem location in a fresh throwaway namespace and returns that namespace as a dictionary. Nothing is cached in sys.modules, the file need not be importable, and run_name lets you execute it exactly as the interpreter would.
solid answer
~40 s`runpy.run_path(path_name, init_globals=None, run_name=None)` takes a **filesystem location** — a `.py` file, or a directory or zip archive containing a top-level `__main__.py` — executes it in a brand-new module namespace, and hands that namespace back as a dict. An import needs the target to be findable on `sys.path` under a valid module name, and caches the result in `sys.modules` so a second import is a no-op; `run_path` needs neither, and running it twice executes twice. `run_name` sets `__name__` for the execution — pass `"__main__"` and the file's guard fires, exactly as `python file.py` would; leave it out and `__name__` is `'<run_path>'`. Its sibling `runpy.run_module(mod_name, ...)` does the same for a dotted name resolved through the import system, and is the machinery behind the `-m` switch.
code
python · 12 linesimport pathlib
import runpy
import sys
import tempfile
with tempfile.TemporaryDirectory() as tmp:
job = pathlib.Path(tmp, "job.py")
job.write_text("INTERVAL = 11\nprint('name is', __name__)\n")
ns = runpy.run_path(str(job), run_name="__main__")
print("returned INTERVAL:", ns["INTERVAL"])
print("cached in sys.modules:", "job" in sys.modules)go deeper
Recall that importing caches a module under its name while runpy executes a file and hands back the resulting namespace, and that run_name decides whether the executed file thinks it is main.
Explain the two functions and their signatures, the '<run_path>' default for name, and why nothing lands in sys.modules — then connect run_module to what the -m switch actually does.
Show where it belongs in real tooling: running a user-supplied config or migration file, launching a target the way a debugger or profiler must, and knowing that objects defined in that namespace are not picklable by reference.
Weigh it as a plugin strategy: executing arbitrary files gives no capability isolation and no import identity, so decide when a real importable module with a declared interface is the safer contract for code other teams will supply.
## Executing a file versus importing a module Importing is a *caching, name-based* operation: the import system resolves a dotted name against `sys.path`, creates a module object, executes the file into that module's namespace, and stores it in `sys.modules`. The target must be findable and must have an identifier-legal name, and the second import of the same name does nothing at all. `runpy` offers the other operation — *execute this code the way the interpreter would start it* — with two functions: - `runpy.run_path(path_name, init_globals=None, run_name=None)` takes a filesystem location. - `runpy.run_module(mod_name, init_globals=None, run_name=None, alter_sys=False)` takes a dotted module name. Both build a fresh namespace dictionary, execute the target's code into it, and **return the dictionary**. Neither leaves the executed code in `sys.modules` under its own name, so the result is a plain dict of whatever the file defined, and calling again runs the code again. ## What run_path specifically buys you **No importability requirement.** The path can be `config-2026.py`, a file in a directory nobody put on `sys.path`, a migration script, or a zip archive with a top-level `__main__.py`. None of those are importable by name. **Faithful `__main__` semantics.** Pass `run_name="__main__"` and the executed file sees `__name__ == "__main__"`, so its guard fires and it behaves as if launched directly. Omit it and `__name__` is `'<run_path>'` — a deliberately non-identifier default that makes it obvious the code was not imported. This is why debuggers, profilers and coverage tools run their target through `runpy` rather than reading the file and calling `exec`: `runpy` reproduces the interpreter's start-up contract, including `__file__`, the fresh namespace, and temporary adjustments to `sys.path` and `sys.argv[0]` needed to make the target behave like a real entry point. Those `sys` adjustments are not optional in `run_path` — they are required so that a directory or zip path can be executed at all — whereas `run_module` makes them opt-in with `alter_sys`. **A returned namespace.** You get the file's globals as data. A tool that loads a user-supplied settings file can execute it and read the names it defined without ever creating a module for it: ```python import runpy ns = runpy.run_path("settings.py") interval = ns.get("INTERVAL", 30) ``` **Repeatability.** Because nothing is cached, a long-running tool can re-execute a changed file without the import system's "already imported, so nothing happens" behaviour and without reaching for cache invalidation. ## run_module, and why -m behaves the way it does `run_module` resolves its argument through the import system, which means it imports the parent packages first (running their `__init__.py`), then executes the named module in a fresh namespace under `run_name` — defaulting to the module's own name. Running a module this way under the run name `"__main__"` is precisely what the `-m` switch does, and it explains a fact that surprises people: the `-m` target lives in `sys.modules` as `"__main__"`, not under its dotted name, so importing it again by dotted name executes it a second time into a second module object. The same family of machinery is what rebuilds the parent's entry module inside a child process when `multiprocessing` uses a start method that begins from a fresh interpreter. ## Caveats worth stating out loud - **It is execution, not sandboxing.** The code runs with the full privileges of the caller. `runpy` gives isolation of *namespace*, never of *capability*. Treating it as a safe way to run untrusted files is a security bug. - **The objects are not importable.** A class defined in a `run_path` namespace has no reachable module to be found in, so pickling it by reference fails and the error message is confusing. If you need to send those objects anywhere, define them in a real module. - **The namespace outlives the call if anything holds it.** Functions defined by the executed code keep a reference to the namespace they were compiled against, so returning a function keeps the whole dict alive. - **Side effects still happen.** A fresh namespace does not undo what the code did to files, sockets, environment variables or other modules' state. ## The interview answer Say: `import` is name-based and cached; `runpy` is location-or-name based, uncached, returns the namespace, and reproduces the interpreter's own entry contract — including letting you choose `__name__`. Then name where it shows up: the `-m` switch, debuggers and profilers running a target file, and tools that must execute a file the user pointed at.
- How does runpy.run_module differ from runpy.run_path?`run_module` takes a dotted module name and resolves it through the import system, so parent packages are imported and their `__init__.py` files run; its default `run_name` is the module's own name and its `sys` adjustments are opt-in via `alter_sys`. `run_path` takes a filesystem location — including a directory or zip with a top-level `__main__.py` — defaults `__name__` to `'<run_path>'`, and always adjusts `sys` temporarily.
- Does runpy.run_path add the executed file to sys.modules?No. The code executes into a fresh dictionary that is returned to the caller, and nothing is cached, so calling it twice executes the file twice. A practical consequence: classes and functions defined there are not reachable by import, so pickling them by reference fails — anything that must be sent to another process belongs in a real module.
- Why do debuggers and profilers launch their target through runpy instead of exec?Because `runpy` reproduces the interpreter's own entry contract: a fresh namespace with `__file__` set, `__name__` chosen by the caller so the target's guard fires, and the temporary `sys.path` and `sys.argv[0]` adjustments a real launch performs. Reading the source and calling `exec` gives none of that, so the target runs in a namespace it was never written for.
saying these in an interview costs you the question
- Thinks run_path registers the file in sys.modules
- Claims runpy sandboxes or restricts untrusted code
- Passes a file path to run_module or a dotted name to run_path
- Assumes __name__ defaults to "__main__" in run_path
- Expects a second call to be cached like a repeat import