Stream subprocess output with timeouts, tree-kill, and sane defaults.
Project description
procstream
Stream subprocess output in Python — with timeouts, tree-kill, and sane defaults.
from procstream import run
for line in run(["pytest", "-q"]).stream():
prefix = "!!" if line.is_stderr else " "
print(prefix, line.text)
Why
subprocess.run() buffers everything until the child exits. subprocess.Popen
with pipes works, but reading stdout and stderr concurrently without deadlock
is one of those things you have to get right every single time, across every
project. Timeouts that kill the whole process tree (not just the direct
child) are another recurring footgun.
procstream wraps that pattern into a small, typed, stdlib-only API.
Install
pip install procstream
Python 3.9+. No runtime dependencies.
What it gives you
- Streaming output — iterate lines as they arrive, tagged by stream (stdout / stderr), in arrival order.
- Timeouts that actually clean up — the whole process group is sent SIGTERM, then SIGKILL if it ignores the hint. No orphaned grandchildren.
- Tree-kill —
terminate_tree()/kill_tree()for manual cancel, POSIX and Windows. - Per-line callbacks —
on_stdout=printif you don't want to write the iterator loop yourself. - Context manager —
with run(...) as p:guarantees the tree dies if your code raises. - Result buffering —
wait()always returns captured stdout / stderr, even if you streamed them. - Stdlib only — no
psutil, no surprise deps.
Usage
Simple: capture output
from procstream import run
r = run(["node", "--version"]).wait()
print(r.returncode, r.stdout.strip())
Streaming: react to lines as they arrive
for line in run(["npm", "install"]).stream():
if line.is_stderr:
log.warning(line.text)
else:
log.info(line.text)
Lines come out in the same interleaved order the child emitted them. line.stream
is "stdout" or "stderr".
Callbacks: no loop required
run(
["make", "build"],
on_stdout=print,
on_stderr=lambda t: print("!!", t),
).wait()
Timeout: kills the whole tree
from procstream import run, TimeoutExpired
try:
run(["flaky-script"], timeout=30).wait()
except TimeoutExpired as e:
print("killed after", e.timeout, "seconds")
# Partial output is still available:
print(e.result.stdout)
When the deadline fires, procstream sends SIGTERM to the process group and
waits up to 2 seconds. If the child still hasn't exited, it sends SIGKILL.
Grandchildren die with it.
Manual cancel
p = run(["long-running-server", "--port", "8080"])
# ... elsewhere
p.terminate_tree(grace=5.0) # SIGTERM, wait, SIGKILL if needed
# or
p.kill_tree() # immediate SIGKILL
Context manager: cancel on exception
with run(["watcher"]) as p:
for line in p.stream():
if "ERROR" in line.text:
raise RuntimeError("saw error, bailing")
# Tree is terminated on the way out.
API
run(cmd, *, cwd=None, env=None, shell=False, timeout=None, encoding="utf-8", errors="replace", on_stdout=None, on_stderr=None) -> Process
Spawn a command and return a Process. The child starts immediately.
Process
| member | description |
|---|---|
pid |
child's pid |
returncode |
None while running, int once finished |
running |
True while the child is alive |
stream() |
iterator of Line in arrival order (consume only once) |
wait() |
block until completion, return Result; raises TimeoutExpired if killed |
terminate_tree(grace=5.0) |
SIGTERM group, escalate to SIGKILL if still alive |
kill_tree() |
SIGKILL group immediately |
| context manager | with run(...) as p: — terminates on exit |
Line
| member | description |
|---|---|
text: str |
line content, newline stripped |
stream: "stdout" | "stderr" |
origin pipe |
is_stderr: bool |
shortcut |
Result
| member | description |
|---|---|
returncode: int |
exit status |
stdout: str |
all stdout lines joined with \n |
stderr: str |
all stderr lines joined with \n |
combined: str |
stdout followed by stderr (use stream() for real order) |
ok: bool |
returncode == 0 |
TimeoutExpired
Raised from wait() (or at the end of stream()) when timeout fires.
Exposes pid, timeout, and a partial result.
Platform notes
- POSIX (macOS, Linux): child starts in a new session via
start_new_session=True. Signals go to the whole process group withos.killpg. - Windows: child starts with
CREATE_NEW_PROCESS_GROUP.terminate_tree()sendsCTRL_BREAK_EVENT; force-kill falls back totaskkill /F /T /PID <pid>.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file procstream-0.1.0.tar.gz.
File metadata
- Download URL: procstream-0.1.0.tar.gz
- Upload date:
- Size: 9.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2356bb462da169cf3360819676a7a2ada29a4e1de49f585d49e9f45685371bc0
|
|
| MD5 |
548b3163bbba69833f5f72b2ece1fd76
|
|
| BLAKE2b-256 |
7ceb740cd0d1184d8a6cac486d60d8eb0da3324c869317817f14e396cf849fe2
|
File details
Details for the file procstream-0.1.0-py3-none-any.whl.
File metadata
- Download URL: procstream-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a10be7b776906a16eaba2dccba8380596f6e6ae6b6e1de7db968fc5134f57ef7
|
|
| MD5 |
ff274deffbced3aeca06a253ef175c71
|
|
| BLAKE2b-256 |
4ab7728ba9fadb85600c51dd325430875b43e5bdd16a7b89781ec0d47b16458e
|