A lightweight way to run an interactive terminal session in the background from Python
bgterm gives you a lightweight way to run an interactive terminal session in the background from Python.
It is for the common control loop:
This is useful for REPLs, shells, and interactive CLI tools such as python, ipython, sqlite3, or custom command-line programs that you want to keep alive between calls.
bgterm is a good fit when you want:
It is not the right tool when you need:
For those cases, see bgtmux.
from bgterm import poll, start_bgterm, write_stdin
sid = start_bgterm(["ipython", "--simple-prompt", "--no-confirm-exit", "--no-banner"])
startup = poll(sid, 5000)
print(startup.text)
out = write_stdin(sid, "2+2\n", 500)
print(out.text)
The object wrapper is just a convenience around the same flow:
from bgterm import Session
with Session.start(["ipython", "--simple-prompt", "--no-confirm-exit", "--no-banner"]) as sess:
print(sess.poll(5000).text)
print(sess.write_stdin("2+2\n", 500).text)
Every session has an unread-output cursor.
That means:
write_stdin() / poll() / read() call returns the next unread chunk by defaultSo the normal pattern is:
write_stdin(sid, "command\n", yield_time_ms=...) to send input and wait brieflypoll(sid, yield_time_ms=...) to wait again without sending anythingread(sid) if you just want the next unread chunk immediatelyyield_time_ms Meansyield_time_ms is a bounded wait, not a semantic completion signal.
When you call write_stdin() or poll() with yield_time_ms:
bgterm does not know whether your REPL command is “done”. It only knows whether terminal output changed.
In practice, callers usually decide completion by one of:
The primary interface is functional and sid-based.
start_bgterm(...) -> sidStart a background session and return an integer session id.
Important arguments:
cmd: command string or argv listcwd, env: forwarded to subprocess.Popenshell: defaults to True for string commands and False for argv listsencoding, errors: control decoding of terminal bytes into PollResult.textmax_buffer_bytes: total buffered output kept in memorywrite_stdin(sid, chars="", yield_time_ms=0, max_output_bytes=65536)Write input to the session, wait briefly, and return unread output.
Passing chars="" is valid and often useful.
poll(sid, yield_time_ms=0, max_output_bytes=65536)Wait for unread output without sending input.
read(sid, max_output_bytes=65536)Return unread output immediately with no waiting.
wait(sid, timeout_ms=None)Wait for the session to exit and return its exit code, or None on timeout.
close_bgterm(sid) / terminate(sid) / kill(sid)Shut down the session or forward process termination signals.
list_sessions()Return the currently active session ids in this Python process.
SessionThin convenience wrapper over the sid-based API.
PollResultEach write_stdin() / poll() / read() call returns a PollResult.
The most useful fields are:
text: decoded unread outputrunning: whether the child is still aliveexit_code: final return code, or None while still runningremaining_bytes: unread buffered output still waiting after this calldropped_bytes: bytes lost because the buffer overflowedtruncated: whether this read was partial or older unread output was lostThe offset fields are there when you need deterministic paging behavior, but most callers only need text plus the status fields above.
bgterm is optimized for interactive polling of recent output.
If a session produces output faster than you read it:
That is usually the right tradeoff for interactive terminals. If you need the full transcript of a huge job, write to a file instead of treating the terminal as the transport.
from bgterm import poll, start_bgterm, write_stdin
sid = start_bgterm(["ipython", "--simple-prompt", "--no-confirm-exit", "--no-banner"])
startup = poll(sid, 5000)
print(startup.text)
out = write_stdin(sid, "import time; time.sleep(2); print('done')\n", 200)
print(out.text) # often just echoed input so far
out = poll(sid, 2500)
print(out.text) # should now include 'done' and the next prompt
pip install -e .[dev]
pytest
Version lives in bgterm/__init__.py as __version__.
ship-bump --part 2 # patch
ship-bump --part 1 # minor
ship-bump --part 0 # major
bug, enhancement, or breaking.ship-gh
ship-pypi
If you need sessions that are named, visible outside the current Python process, and easy to reattach to manually, see bgtmux. bgtmux uses tmux itself as the session registry, so it is often the better choice when you want inspectable long-lived terminals, cross-process visibility, or tmux-native pane and window browsing. bgterm is the better fit when you want the lightest-weight PTY-backed background session model inside one Python process.