Python REPL in Production
I found myself needing the python REPL in production, with access to the real config and objects of a running Flask application.
Here is a minimal approach using the builtin code.InteractiveConsole to expose the REPL over a Unix socket.
# repl_socket.py
from __future__ import annotations
import code
import io
import os
import socketserver
import threading
from contextlib import redirect_stderr, redirect_stdout, suppress
from typing import Any
def start_repl_socket(
app: Any,
*,
namespace: dict[str, Any],
) -> str | None:
class Handler(socketserver.StreamRequestHandler):
def handle(self) -> None:
locals_ = {**namespace, "app": app}
text_rfile = io.TextIOWrapper(
self.rfile,
encoding="utf-8",
errors="replace",
newline=None,
)
text_wfile = io.TextIOWrapper(
self.wfile,
encoding="utf-8",
line_buffering=True,
write_through=True,
)
with suppress(Exception):
with (
app.app_context(),
redirect_stdout(text_wfile),
redirect_stderr(text_wfile),
):
console = code.InteractiveConsole(locals=locals_)
print(f"{app.name} REPL (pid={os.getpid()})\n")
more = False
while True:
text_wfile.write("... " if more else ">>> ")
text_wfile.flush()
if not (line := text_rfile.readline()):
break
more = console.push(line.rstrip("\r\n"))
socket_path = f"/tmp/api-repl.{os.getpid()}.sock"
server = socketserver.ThreadingUnixStreamServer(socket_path, Handler)
os.chmod(socket_path, 0o600)
threading.Thread(target=server.serve_forever, daemon=True).start()
return socket_path
The nice thing about console.push(...) is that it behaves like a real Python REPL:
- multi-line blocks work
- bare expressions print their values
- tracebacks show up correctly
The rest is just plumbing:
TextIOWrapperturns the socket into normal text streamsredirect_stdout(...)andredirect_stderr(...)makeprint()and errors show up in the clientapp.app_context()makes Flask globals such ascurrent_appwork immediately
Then wire it into your app and expose whatever objects you want in the session:
# main.py
from flask import Flask
from repl_socket import start_repl_socket
app = Flask(__name__)
@app.get("/")
def healthcheck():
return {"status": "ok"}
if __name__ == "__main__":
socket_path = start_repl_socket(
app,
namespace={"config": app.config, "db": None},
)
if socket_path:
print(f"production REPL listening on {socket_path}")
app.run()
Start the app, then attach with nc:
uv run python main.py
nc -U /tmp/api-repl.<pid>.sock
An example session looks like this:
main REPL (pid=63503)
>>> 1 + 1
2
>>> from flask import current_app
>>> current_app.name
'main'
>>> with app.test_request_context("/"):
... from flask import request
... print(request.path)
...
/
A few nice details:
- the transport is local-only; no TCP debug port needed
chmod 600keeps the socket owner-only- each worker process gets its own
/tmp/api-repl.<pid>.sock
For request-bound objects such as request or session, create a request context manually:
with app.test_request_context("/"):
...
That is it. No extra dependency, and no need to build your own eval() loop.