A PyQt5 terminal widget usually combines a text display, an input field, and a background process. The important rule is to avoid blocking the GUI thread. Use QProcess to run the command process and connect its output signals to a widget such as QPlainTextEdit.
Qt’s QProcess documentation describes how a Qt app can start and communicate with external programs asynchronously. Qt’s QPlainTextEdit documentation is relevant because terminal output is plain text and should stay efficient as lines are appended.
Quick answer
Create a read-only QPlainTextEdit for output, a QLineEdit for commands, and a QProcess for the running interpreter or command process. Connect readyReadStandardOutput and readyReadStandardError to slots that append text, and send user input through process.write().
1. Build the terminal widget layout
Start with a compact widget that separates output from input. The output area should be read-only so command results cannot be accidentally edited by the user.
from PyQt5.QtWidgets import QLineEdit, QPlainTextEdit, QVBoxLayout, QWidget
class TerminalWidget(QWidget):
def __init__(self):
super().__init__()
self.output = QPlainTextEdit(readOnly=True)
self.input = QLineEdit()
self.input.setPlaceholderText("Type Python code and press Enter")
layout = QVBoxLayout(self)
layout.addWidget(self.output)
layout.addWidget(self.input)
This is only the visual shell. The next step is adding a process that produces output without freezing the window.
2. Add a QProcess
QProcess runs separately from the GUI event loop, so your window can keep repainting while the process is active. Connect its output signals once during widget setup.
import sys
from PyQt5.QtCore import QProcess
class ProcessMixin:
def configure_process(self):
self.process = QProcess(self)
self.process.setProgram(sys.executable)
self.process.setArguments(["-i"])
self.process.readyReadStandardOutput.connect(self.read_stdout)
self.process.readyReadStandardError.connect(self.read_stderr)
Using sys.executable starts the same Python interpreter that launched your GUI. If that is not the interpreter you expected, check it with our Python version guide.
3. Append stdout and stderr
When the process writes output, read the available bytes, decode them, and append the text to the terminal display. Keep the method small because it can run many times.
def append_text(widget, text):
cursor = widget.textCursor()
cursor.movePosition(cursor.End)
cursor.insertText(text)
widget.setTextCursor(cursor)
def read_stdout(self):
data = self.process.readAllStandardOutput().data().decode("utf-8", errors="replace")
append_text(self.output, data)
def read_stderr(self):
data = self.process.readAllStandardError().data().decode("utf-8", errors="replace")
append_text(self.output, data)
Separating stdout and stderr lets you style them differently later, but even plain text output is enough for a useful embedded terminal panel.
4. Start the process safely
Start the process after the widget has connected its signals. A simple embedded Python prompt is safer for a tutorial than launching a system shell because it avoids platform-specific command syntax.
def start_terminal(self):
if self.process.state() == QProcess.NotRunning:
self.output.appendPlainText("Starting Python process...")
self.process.start()
def handle_started(self):
self.output.appendPlainText("Process is ready.")
For production tools, decide exactly which program is allowed to run. Do not turn a GUI input box into an unrestricted command runner unless that is truly the product requirement.
5. Send user input to the process
Connect the input field’s return signal to a method that writes a newline-terminated command. Clear the input after sending it so the widget feels like a terminal.
def send_input(self):
text = self.input.text().strip()
if not text:
return
self.output.appendPlainText(f">>> {text}")
command = text + chr(10)
self.process.write(command.encode("utf-8"))
self.input.clear()
If you need a real command shell, handle quoting, current working directory, and permissions deliberately. For many desktop apps, a restricted command palette is safer than a full shell.
6. Stop the process cleanly
A terminal widget should close its child process when the widget or application exits. Ask the process to terminate first, then kill it only if it does not exit.
def stop_terminal(self):
if self.process.state() == QProcess.NotRunning:
return
self.process.terminate()
if not self.process.waitForFinished(1500):
self.process.kill()
self.process.waitForFinished(1500)
This avoids leaving background Python processes open after the GUI closes. It also makes repeated testing easier because each run starts with a clean process.
Security and platform notes
A terminal widget can become a high-risk feature if it accepts arbitrary user commands. Prefer a known executable, a controlled working directory, and a small set of allowed actions. On Windows, macOS, and Linux, process startup details differ, so test the exact program and arguments on every platform you support.
Design notes for a better terminal widget
A useful terminal widget should handle large output, keyboard shortcuts, a clear button, copy support, and a visible running state. If the output panel needs to resize cleanly with the window, review our PyQt resize guide. If the widget includes a clear-console action, our clear Python shell guide explains related terminal behavior.
Common mistakes
- Running
subprocess.run()directly on the GUI thread and freezing the app. - Using a rich text widget when plain terminal output only needs
QPlainTextEdit. - Ignoring stderr, which hides useful error messages from the user.
- Leaving child processes running when the window closes.
- Allowing arbitrary commands when the app only needs a small set of safe actions.
When to use this pattern
Use a PyQt5 terminal widget when your desktop app needs to show logs, run controlled Python snippets, display tool output, or provide an interactive developer panel. Use a full terminal emulator only when you need advanced behavior such as ANSI control sequences, job control, shell history, and platform-specific terminal features.