What does sys.remote_exec(pid, script) do in Python 3.14, and in which process does the script run?
answer
- Debug a process you did not start
- The caller leaves a note
- A path and a process id
- Target's main thread runs it
- sys.remote_exec, PEP 768, Python 3.14
basics
~20 ssys.remote_exec, added by PEP 768 in Python 3.14, asks an already-running interpreter, named by its process id, to execute a Python file. That file runs inside the target process's main thread, never in the caller.
solid answer
~50 s`sys.remote_exec(pid, script)` is the attach primitive PEP 768 added in Python 3.14. The caller never executes the script: it writes the path into a small control area that every 3.14 interpreter keeps in its own memory, using the platform's write-to-another-process facility, and raises a flag. The target's **main thread** notices that flag at its next safe point and runs the file with its own interpreter, its own imported modules and its own live objects, much the way a signal is delivered. That is why attaching works on a process nobody started under a debugger. The call returns immediately, tells you nothing about the outcome, and typically raises `PermissionError` when the operating system will not let you write the target's memory. The two interpreters must share a CPython major.minor version, and the file must still exist when the target gets round to reading it.
code
python · 10 linesimport pathlib
import sys
import tempfile
script = pathlib.Path(tempfile.mkdtemp()) / "probe.py"
script.write_text("import sys; print('running inside', sys.executable, flush=True)\n")
# sys.remote_exec(pid, script) would hand this path to another 3.14 process,
# which reads and runs it in its own main thread.
print(script.exists(), callable(sys.remote_exec))go deeper
Recall that Python 3.14 can attach to an already-running process by its process id, and that the code you send is executed by that process rather than by yours. Naming sys.remote_exec and knowing it needs elevated permission is enough here.
Be ready to walk through the mechanics: the caller writes a script path and a flag into the target's memory, the target's main thread picks it up, and nothing is reported back. Mention that the two interpreters must be the same CPython version.
Show you would use it deliberately: a script path that outlives the call, output routed somewhere you can actually read, and awareness that the injected code runs with the target's full state. Know that permission failures and version mismatches are the usual reasons an attach never happens.
Own whether this capability exists in your images at all and who may exercise it. The tradeoff worth stating is that the operating-system permission needed to attach already implies deep access, so the interpreter-level switch is defence in depth rather than the boundary itself.
### The gap this closes Before Python 3.14, inspecting a live interpreter you had not prepared in advance meant one of three unpleasant options: restart the process under a debugger and lose the state you cared about; leave a debugging server importable and listening in every deployment, paying for it whether or not you ever used it; or read the process's memory from outside with a native debugger and reconstruct Python-level meaning by hand. PEP 768 added a supported fourth option: hand a running interpreter a path to a Python file and let it execute that file itself. ### What the call actually does `sys.remote_exec(pid, script)` takes a process id and a path to a file of ordinary Python source. It does **not** run the file. Every CPython 3.14 process publishes a small, fixed control structure in its own memory; the debugger locates that structure in the target and writes two things into it — the path to run, and a pending flag — using the platform's facility for writing another process's address space. Then it returns. The whole transaction is one memory write plus a flag, which is why the caller is finished long before anything has happened at the other end. The target picks the request up on its **main thread**, at the next safe point in the evaluation loop — a moment between complete bytecode instructions where the interpreter's own invariants hold. CPython's documentation describes the delivery as being like signal handling, and the comparison is exact: the request is recorded, and acted on when it is safe, not when it arrives. ### What the injected script sees The script is executed by the target's interpreter, so it sees the target's world: its imported modules, its module-level state, its open connections, its live objects, its threads. That is the entire point. A stack dumper injected into a stuck worker prints *that worker's* frames; a script that reaches into a module global reads the value the running program is actually using. There is no channel back. `sys.remote_exec` returns nothing about the script, offers no completion notification, and gives no way to ask whether the file ever ran. Anything you want to learn, the script must send somewhere itself: print to the target's own stdout or stderr, write a file, or open a socket back to your tool. That last shape is how `python -m pdb -p PID`, added in 3.14 and also available as `pdb.attach(pid)`, bootstraps an interactive session — the injected code sets up a connection and then drives an ordinary debugger over it. ### The preconditions Four conditions have to hold, and each one produces a distinct failure. 1. **Operating-system permission.** Writing another process's memory is privileged. On Linux you generally need to be the same user and to satisfy the platform's tracing policy; on macOS you need root or the task-port entitlement, so even attaching to your own child fails with `PermissionError` from an ordinary shell. This is the real gate on the feature. 2. **The feature is on in the target.** A target started with `PYTHON_DISABLE_REMOTE_DEBUG` set, with `-X disable-remote-debug`, or built with the `--without-remote-debug` configure option does not act on the request. 3. **Matching interpreters.** The target must run the same CPython major and minor version as the caller; pre-release versions must match exactly. Mixed images are a real source of surprise here. 4. **The file survives.** The target reads the path when it gets to it, which may be later. Keeping the file in place until then is explicitly the caller's responsibility, so tooling writes to a stable temporary path and cleans up after the session, not after the call. ### What it is not It is not a sampler: the target runs your code, so it participates and can be perturbed. It is not a privilege escalation: anyone who can write your process's memory could already do far worse by other means. And it is not a hard interrupt — a process that never reaches a safe point never runs your script, which is a property worth understanding before you rely on attaching during an incident. On the caller's side the call raises the auditing event `sys.remote_exec` with the pid and the path, so a tool that installs an audit hook with `sys.addaudithook` can record every attach it performs.
- How does an injected script get anything back to the person who attached?It has to send it itself. `sys.remote_exec` has no result or completion channel, so the script prints to the target's own stdout or stderr, writes a file, or opens a socket back to the tool. The 3.14 `python -m pdb -p PID` attach uses the socket shape: the injected bootstrap connects back and then drives a normal debugger session over that connection.
- What must be true of the target interpreter before the call can succeed?You need operating-system permission to write its memory — the same user plus the platform's tracing policy on Linux, root or the task-port entitlement on macOS. The target must be running the same CPython major.minor version, must not have remote debugging disabled at startup, and must be able to read the path you passed.
- What happens if you delete the script file right after the call returns?The caller has already succeeded, and the target simply finds nothing to run — you get no error anywhere. Keeping the file alive until the target reads it is documented as the caller's responsibility, which is why tools write to a stable temporary path and remove it only when the session ends.
It is closer to slipping a note under a colleague's door than to phoning them: you leave an instruction and walk away, and they read and act on it when they next look up.
saying these in an interview costs you the question
- Thinks sys.remote_exec runs the script in the calling process
- Believes the target must be launched under a debugger first
- Expects a return value or a completion notification
- Assumes any user can attach to any process
- Thinks it starts a new interpreter for that pid
- Ignores that both interpreters must share a version