bgterm gives you a lightweight way to run an interactive terminal session in the background from Python.
It is for the common control loop:
- start a session once
- get back a session id
- send more input later
- wait a bounded amount of time
- inspect the output that arrived since your last read
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:
- a persistent interactive session, not a one-shot subprocess
- a simple functional API from Python
- output buffered between calls
- a light in-process tool without bringing in tmux
It is not the right tool when you need:
- named sessions you can reattach to manually
- visibility across multiple processes
- a faithful way to retrieve very large transcripts
For those cases, see bgtmux.
from bgterm import poll, start_session, write_stdin
sid = start_session(["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:
- output can arrive between calls and still be available later
- each
write_stdin()/poll()/read()call returns the next unread chunk by default - if there is already unread output waiting, a later poll returns immediately
So 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 immediately
yield_time_ms is a bounded wait, not a semantic completion signal.
When you call write_stdin() or poll() with yield_time_ms:
- if unread output is already buffered, the call returns immediately
- otherwise it waits until output arrives, the process exits, or the timeout expires
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:
- seeing the next prompt
- printing a sentinel string and waiting for it
- waiting for process exit
The primary interface is functional and sid-based.
Start a background session and return an integer session id.
Important arguments:
cmd: command string or argv listcwd,env: forwarded tosubprocess.Popenshell: defaults toTruefor string commands andFalsefor argv listsencoding,errors: control decoding of terminal bytes intoPollResult.textmax_buffer_bytes: total buffered output kept in memory
Write input to the session, wait briefly, and return unread output.
Passing chars="" is valid and often useful.
Wait for unread output without sending input.
Return unread output immediately with no waiting.
Wait for the session to exit and return its exit code, or None on timeout.
Shut down the session or forward process termination signals.
Return the currently active session ids in this Python process.
Thin convenience wrapper over the sid-based API.
Each 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, orNonewhile 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 lost
The 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:
- old unread output may be dropped once the buffer fills
- one read may only return part of the available unread output
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_session, write_stdin
sid = start_session(["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 promptpip install -e .[dev]
pytestVersion lives in bgterm/__init__.py as __version__.
ship-bump --part 2 # patch
ship-bump --part 1 # minor
ship-bump --part 0 # major- Ensure GitHub issues are labeled
bug,enhancement, orbreaking. - Run:
ship-gh
ship-pypiIf 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.