Lightweight embedded, cell-oriented Python shell (notebooks/agents) with minimal dependencies.
Project description
Pynteract
Pynteract is a lightweight, dependency-minimal, embeddable Python “cell shell” with IPython-style ergonomics:
magics (%, %%), system commands (!, !!), per-cell filenames for better tracebacks, and a hookable execution pipeline.
It is designed to be used inside notebooks, web apps, AI agents, CLIs, and other embedded environments where you want: deterministic execution, controllable IO, and rich integration points.
Table of contents
- Install
- Quick start
- Core concepts
- Magics and system commands
- Hooks
- Interactive terminal mode
- CLI
- Threads and late output routing
- API reference
- Development
- License
Install
pip install pynteract
Optional terminal interactive mode (adds prompt_toolkit):
pip install "pynteract[terminal]"
Quick start
from pynteract import Shell
shell = Shell(display_mode="none")
resp = shell.run("x = 41 + 1\nx")
assert resp.result == 42
assert resp.stdout == ""
assert resp.stderr == ""
assert resp.exception is None
Core concepts
Execution + display
- Code is executed node-by-node (AST-level), enabling pre/post hooks per statement/expression.
display_modecontrols what expression results are displayed:"last"(default): display only the last expression value"all": display every expression value"none": never display expression values
- A semicolon after an expression suppresses display, mirroring IPython:
x;.
Namespaces + filenames
Pynteract runs in a module-backed namespace by default (good __module__ behavior, better tracebacks).
You can embed it in an existing namespace:
import sys
from pynteract import Shell
shell = Shell(module_name="__main__", namespace=sys.modules["__main__"].__dict__)
Each run() is assigned a synthetic filename like <shell-input-3> (or a custom one via filename=...).
This filename is used for:
- tracebacks
Shell.historykeys- hook routing via
RunContext.name(handy in notebook-like UIs)
History
Shell.history is an OrderedDict of recent ShellResponse objects keyed by synthetic/custom filename.
The size is capped by history_size.
Magics and system commands
Registering magics
from pynteract import Shell
shell = Shell(display_mode="none")
@shell.register_magic(name="caps", mode="both") # "line" | "cell" | "both"
def caps(text: str) -> str:
return text.upper()
assert shell.run("%caps hello").result == "HELLO"
Magic forms
- Line magic:
%name rest of line - Cell magic:
%%nameon the first line; the remaining cell body is passed as a string - Inline magic:
x = %name rest of line(also works after;)
Templates
Inside magics and system commands, {expr} is evaluated in the current namespace and replaced with str(value).
- Escape literal braces with
{{/}}.
System commands
!cmd ...runs a system command and streams stdout/stderr into the captured streams.!!cmd ...runs a system command, captures stdout, and returns it as the cell result (stderr still streams).
shell = Shell(display_mode="none")
shell.run('!python3 -c "print(123)"').stdout
shell.run('!!python3 -c "print(456)"').result
Built-in magics
Pynteract ships a small set of “IPython-like” convenience magics (registered automatically by Shell.ensure_builtins()):
%pwd,%cd [path|-],%ls [path]%env,%env KEY,%env KEY=value,%env -u KEY%who,%whos%run [-i] script.py [args...]%time <code>,%timeit [-n N] [-r R] <code>%%bash
Hooks
Hooks are stored in shell.hooks (a dict) and are looked up dynamically during execution. Updating a hook
while the shell is running takes effect immediately.
from pynteract import Shell
def stdout_hook(data: str, buffer: str) -> None:
print("STDOUT:", data, end="")
shell = Shell(display_mode="none", stdout_hook=stdout_hook)
shell.hooks["stdout_hook"] = stdout_hook # can be swapped dynamically
Hooks may optionally accept a final ctx argument (RunContext) for routing. ctx.name matches the synthetic/custom
filename of the current run (or another routing name if you override it).
Interactive terminal mode
from pynteract import Shell
Shell().interact()
Persistent history
Interactive sessions store prompt histories under ~/.pynteract/ (or PYNTERACT_CONFIG_DIR):
history_python.txt: interactive>>>input historyhistory_text.txt: stdin “text” history used forinput()reads
Startup script
If present and non-empty, ~/.pynteract/startup.py is executed at the start of interactive sessions.
- The startup file is executed via
Shell.run(..., silent=True)so magics/system commands are supported. - If the startup fails, Pynteract prints the enriched traceback and exits (non-zero status in the CLI).
- When using
pynteract -i script.py, the startup script runs before the script (same namespace kept for the REPL).
Restarting a session
In interactive mode, you can reset the namespace and rerun startup:
__shell__.restart_session()
CLI
Installing the package provides a pynteract executable.
# Interactive session
pynteract
# Run a script (similar to `python script.py`)
pynteract path/to/script.py [args...]
# Run a script, then enter interactive mode with the same namespace
pynteract -i path/to/script.py [args...]
Threads and late output routing
To keep output from background threads routed to the originating cell/run, call shell.enable_stdio_proxy() once,
then capture and propagate a contextvars.Context:
import threading, time
from pynteract import Shell
shell = Shell(display_mode="none")
shell.enable_stdio_proxy()
def worker():
for i in range(3):
print(f"tick {i}")
time.sleep(0.1)
shell.run("print('cell start')", filename="<cell-1>")
ctx = shell.capture_context() # captures ctx.name == "<cell-1>"
threading.Thread(target=lambda: ctx.run(worker), daemon=True).start()
Notebook-style “late streaming” example
In a notebook UI, you typically want each cell to own its own stdout/stderr widget, and you want background threads spawned by a cell to keep streaming into that same widget even after the cell finished.
Use the run filename= (exposed as ctx.name to hooks) as your routing key:
from pynteract import Shell
cell_widgets = {} # e.g. {"cell-42": StdoutTextArea(...)}
def cell_id_from_ctx(ctx) -> str:
return ctx.name.split(":")[1] # e.g. "<nb:cell-42:run-7>" -> "cell-42"
def stdout_router(data: str, _buffer: str, ctx) -> None:
cell_widgets[cell_id_from_ctx(ctx)].append(data)
shell = Shell(display_mode="none", stdout_hook=stdout_router)
shell.enable_stdio_proxy()
def run_cell(cell_id: str, run_no: int, code: str) -> None:
shell.run(code, filename=f"<nb:{cell_id}:run-{run_no}>")
API reference
Shell
Constructor:
Shell(
namespace: dict | None = None,
module_name: str | None = None,
ensure_cwd_on_syspath: bool = True,
display_mode: Literal["all", "last", "none"] = "last",
history_size: int = 200,
**hooks,
)
Key methods:
| Method | Signature | Notes |
|---|---|---|
| Execute | run(code, globals=None, locals=None, silent=False, filename=None) -> ShellResponse |
silent=True suppresses stdout/stderr hooks (output still captured). |
| Interactive | interact() -> int |
Terminal REPL; returns process-like exit code. |
| Restart | restart_session(rerun_startup=True, announce=True) -> int |
Resets namespace and (optionally) reruns startup. |
| Namespace | update_namespace(**kwargs) |
Adds symbols to the execution namespace. |
| Namespace | set_namespace(namespace: dict) |
Switches to a different dict namespace. |
| Namespace | reset_namespace() |
Clears user symbols and resets __future__ flags. |
| Magics | register_magic(func=None, *, name=None, mode="both") |
Decorator or direct call; mode `"line" |
| Threads | enable_stdio_proxy() |
Installs a routing proxy on sys.stdout/sys.stderr for late-thread capture. |
| Threads | capture_context(name=None) -> contextvars.Context |
Capture routing context for another thread. |
| Builtins | ensure_builtins() |
(Re)adds __shell__, __magics__, display, and built-in magics. |
Important public attributes:
| Attribute | Type | Meaning |
|---|---|---|
namespace |
dict |
Module-backed execution namespace. |
hooks |
dict[str, Any] |
Hook registry; updated dynamically. |
magics |
dict[str, Any] |
Registered magics. |
history |
OrderedDict[str, ShellResponse] |
Recent run history keyed by filename. |
last_result |
Any |
Last expression value. |
ShellResponse
Returned by Shell.run(...).
| Field | Type | Meaning |
|---|---|---|
input |
str |
Original source string. |
processed_input |
str |
Expanded source (magics/system commands). |
stdout / stderr |
str |
Captured output for the run. |
result |
Any |
Last expression value (depending on display_mode). |
exception |
`Exception | None` |
RunContext and context capture
Hooks may receive a RunContext object (ctx) with:
ctx.name: routing name (by default the synthetic/customfilenameof the currentrun()).
Use shell.capture_context() to propagate routing/capture context to a new thread:
ctx = shell.capture_context()
threading.Thread(target=lambda: ctx.run(worker)).start()
Hook reference
All hooks are optional. Hook keys live in shell.hooks and are passed to Shell(...) via **hooks (kwargs must end with _hook).
Most hooks may optionally accept a final ctx: RunContext.
| Hook key | Signature | When it runs |
|---|---|---|
input_hook |
input_hook(code: str[, ctx]) -> None |
Before parsing. |
pre_run_hook |
pre_run_hook(code: str[, ctx]) -> str |
Before tokenization/execution; can rewrite source. |
code_block_hook |
code_block_hook(code_block: str[, ctx]) -> None |
For each executed AST block. |
pre_execute_hook |
pre_execute_hook(node, source[, ctx]) -> ast.AST |
Before compiling a node. |
post_execute_hook |
post_execute_hook(node, result[, ctx]) -> None |
After a node executes. |
display_hook |
display_hook(obj, **kwargs) or display_hook(obj, ctx, **kwargs) |
When displaying expression values. |
stdout_hook |
stdout_hook(data: str, buffer: str[, ctx]) -> None |
As stdout is flushed. |
stderr_hook |
stderr_hook(data: str, buffer: str[, ctx]) -> None |
As stderr is flushed. |
stdin_hook |
`stdin_hook([ctx]) -> str | None` |
exception_hook |
exception_hook(exc: Exception[, ctx]) -> None |
When a run finishes with an error. |
namespace_change_hook |
namespace_change_hook(old, new, locals[, ctx]) -> None |
After a run, with before/after namespaces. |
post_run_hook |
post_run_hook(resp: ShellResponse[, ctx]) -> ShellResponse |
Final response override/hook. |
Configuration files
Pynteract stores user-facing state under ~/.pynteract/ (override with PYNTERACT_CONFIG_DIR):
startup.py: optional startup file (interactive +-ionly)history_python.txt: persistent REPL historyhistory_text.txt: persistent stdin history
Development
pip install -e ".[dev]"
pytest -q
To develop terminal interactive mode:
pip install -e ".[dev,terminal]"
License
MIT. See LICENSE file.
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 pynteract-0.1.0.tar.gz.
File metadata
- Download URL: pynteract-0.1.0.tar.gz
- Upload date:
- Size: 44.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1a38be583472a72fb966f559a9a5862c496e9102876f1529a11f55f60952629
|
|
| MD5 |
1d9939577afd1419fbf592ab36f82c55
|
|
| BLAKE2b-256 |
95d190f58148b4d1c8724265efef19086411b804996c60ac7ea7cb6a41733963
|
File details
Details for the file pynteract-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pynteract-0.1.0-py3-none-any.whl
- Upload date:
- Size: 39.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
291335b4add2a5640e41061bb7dc410b42d37b4bb431517dec69051b8beb8471
|
|
| MD5 |
ee2b7e8f1922988056163506f6a0a6d2
|
|
| BLAKE2b-256 |
b05d7940202a792ae6f215be25aca703e0c1ca8916c566cc44631b5ab69e7a76
|