Constantly Print Subprocess Output While Process is Running

How to stream live output from a subprocess into your PyQt6 GUI without freezing the interface
Heads up! You've already completed this tutorial.

I need to call a legacy Bash program and display the results in a Qt window. The problem is the subprocess doesn't return each output line as it happens — it waits until the entire command is finished, then dumps everything to the window at once. If the command takes a long time, the user thinks the system is frozen. How can I get live, line-by-line output from a subprocess into my Qt application?

If you've ever launched a long-running external command from a PyQt6 application and watched your entire GUI freeze until it finishes, you've hit one of the most common pitfalls in Python GUI development: blocking the event loop.

The root cause is straightforward. When you call subprocess.run(), Python stops and waits for the process to complete before moving on. While it's waiting, Qt's event loop — the mechanism responsible for redrawing the window, responding to clicks, and processing signals — is completely stalled. No updates, no repaints, nothing. The GUI appears frozen.

There are two solid approaches to stream subprocess output in real time in PyQt6:

  1. Use QProcess, which is Qt's built-in way to run external programs. It integrates directly with the event loop and emits signals as output becomes available.
  2. Use a background QThread with Python's subprocess.Popen to read output line by line and send it back to the GUI via signals.

We'll walk through both approaches with complete, runnable examples.

Why subprocess.run() Freezes Your PyQt6 GUI

Let's make this concrete. Here's a minimal example that demonstrates the problem:

python
import subprocess
import sys

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QPlainTextEdit,
    QPushButton, QVBoxLayout, QWidget,
)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Subprocess Demo - Blocking")

        self.text_area = QPlainTextEdit()
        self.text_area.setReadOnly(True)

        self.button = QPushButton("Run Command")
        self.button.clicked.connect(self.run_command)

        layout = QVBoxLayout()
        layout.addWidget(self.text_area)
        layout.addWidget(self.button)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def run_command(self):
        # This blocks the entire GUI until the command finishes!
        result = subprocess.run(
            ["bash", "-c", "for i in 1 2 3 4 5; do echo Line $i; sleep 1; done"],
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
        self.text_area.setPlainText(result.stdout.decode())


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

Click the button, and the window becomes unresponsive for five seconds. Then all the output appears at once. The GUI didn't update during that time because subprocess.run() held control the entire time.

Now let's fix it.

Approach 1: Streaming Subprocess Output with QProcess

QProcess is Qt's own class for running external programs asynchronously. It starts the process and returns immediately, letting the event loop continue. As the external program produces output, QProcess emits the readyReadStandardOutput signal, which you can connect to a slot that reads and displays the new data.

This is the most "Qt-native" solution for displaying real-time subprocess output in PyQt6 and works well for many use cases. For a deeper dive into QProcess including handling stdin, managing multiple processes, and parsing output, see the complete QProcess tutorial.

python
import sys

from PyQt6.QtCore import QProcess
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QPlainTextEdit,
    QPushButton, QVBoxLayout, QWidget,
)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QProcess Live Output")
        self.process = None

        self.text_area = QPlainTextEdit()
        self.text_area.setReadOnly(True)

        self.button = QPushButton("Run Command")
        self.button.clicked.connect(self.run_command)

        layout = QVBoxLayout()
        layout.addWidget(self.text_area)
        layout.addWidget(self.button)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def run_command(self):
        if self.process is not None:
            return  # Already running

        self.text_area.clear()
        self.button.setEnabled(False)

        self.process = QProcess(self)
        self.process.readyReadStandardOutput.connect(self.handle_stdout)
        self.process.readyReadStandardError.connect(self.handle_stderr)
        self.process.finished.connect(self.process_finished)

        # QProcess takes the program and arguments separately.
        # To run a bash command, pass "-c" and the command string as arguments.
        self.process.start(
            "bash",
            ["-c", "for i in 1 2 3 4 5; do echo \"Line $i\"; sleep 1; done"],
        )

    def handle_stdout(self):
        data = self.process.readAllStandardOutput()
        text = bytes(data).decode("utf-8")
        self.text_area.appendPlainText(text.rstrip())

    def handle_stderr(self):
        data = self.process.readAllStandardError()
        text = bytes(data).decode("utf-8")
        self.text_area.appendPlainText(text.rstrip())

    def process_finished(self):
        self.text_area.appendPlainText("--- Process finished ---")
        self.process = None
        self.button.setEnabled(True)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

Run this, click the button, and you'll see each line appear one at a time, with the GUI remaining fully responsive throughout.

How QProcess Streams Output in Real Time

When you call self.process.start(), the external command begins running in the background. Qt's event loop keeps spinning, so your window stays responsive.

Each time the external process writes to stdout, QProcess emits readyReadStandardOutput. The connected slot (handle_stdout) reads the available data and appends it to the text area. The same pattern applies for stderr.

When the process exits, the finished signal fires, and we clean up.

Running Complex Bash Commands with QProcess

If your actual command involves sourcing setup files, changing directories, and running build tools — like in the original question — you can pass the entire sequence as a single string to bash -c:

python
command = (
    "source /path/to/setup_file -r && "
    "cd /path/to/parent_directory && "
    "build_project_command"
)
self.process.start("bash", ["-c", command])

This works because bash -c accepts the whole pipeline as one argument.

Approach 2: Streaming Subprocess Output Using QThread and subprocess.Popen

Sometimes QProcess doesn't quite fit your needs. For example, you might need to do additional processing on each line of output before displaying it, or you might need to integrate with Python libraries that expect a file-like object. In these cases, running subprocess.Popen in a background QThread is a good alternative.

The idea: spin up a QThread that runs the subprocess, reads its output line by line, and emits a signal for each line. The main thread receives those signals and updates the GUI safely. If you're new to threading in PyQt6, our guide to multithreading with QThreadPool covers the fundamentals of running background tasks without freezing the GUI.

python
import subprocess
import sys

from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QPlainTextEdit,
    QPushButton, QVBoxLayout, QWidget,
)


class SubprocessWorker(QThread):
    """Runs a subprocess in a background thread and emits output line by line."""

    output_line = pyqtSignal(str)
    finished_signal = pyqtSignal(int)  # exit code

    def __init__(self, command):
        super().__init__()
        self.command = command

    def run(self):
        process = subprocess.Popen(
            self.command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,  # Line-buffered
        )

        for line in process.stdout:
            self.output_line.emit(line.rstrip())

        process.wait()
        self.finished_signal.emit(process.returncode)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QThread + Subprocess Live Output")
        self.worker = None

        self.text_area = QPlainTextEdit()
        self.text_area.setReadOnly(True)

        self.button = QPushButton("Run Command")
        self.button.clicked.connect(self.run_command)

        layout = QVBoxLayout()
        layout.addWidget(self.text_area)
        layout.addWidget(self.button)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def run_command(self):
        if self.worker is not None:
            return

        self.text_area.clear()
        self.button.setEnabled(False)

        self.worker = SubprocessWorker(
            ["bash", "-c", "for i in 1 2 3 4 5; do echo \"Line $i\"; sleep 1; done"]
        )
        self.worker.output_line.connect(self.on_output_line)
        self.worker.finished_signal.connect(self.on_finished)
        self.worker.start()

    def on_output_line(self, text):
        self.text_area.appendPlainText(text)

    def on_finished(self, exit_code):
        self.text_area.appendPlainText(f"--- Process finished (exit code {exit_code}) ---")
        self.worker = None
        self.button.setEnabled(True)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

How QThread with subprocess.Popen Works

subprocess.Popen (unlike subprocess.run) starts the process and returns immediately, giving you a handle to interact with it. By iterating over process.stdout, you get each line as it's produced.

Because this iteration is blocking (it waits for the next line), we run it in a QThread so it doesn't block the GUI. Each time a line arrives, the worker emits output_line, which is safely delivered to the main thread via Qt's signal-slot mechanism.

Setting bufsize=1 and text=True enables line-buffered mode, which means output is available to read as soon as a newline character is written by the subprocess.

Fixing Delayed Subprocess Output: Buffering Issues

Even with both approaches working correctly on the Qt side, you might still see delayed output if the external program itself buffers its stdout. Many programs buffer output differently when they detect they're writing to a pipe (which is what happens with both QProcess and subprocess.Popen) versus writing to a terminal.

If your external program supports it, you can try:

  • Setting the PYTHONUNBUFFERED=1 environment variable (for Python scripts).
  • Using stdbuf -oL to force line-buffered output: stdbuf -oL your_command.
  • Using script or unbuffer (from the expect package) to trick the program into thinking it's connected to a terminal.

For example, with the QProcess approach:

python
self.process.start(
    "bash",
    ["-c", "stdbuf -oL your_long_running_command"],
)

QProcess vs QThread: Which Approach Should You Use?

Use QProcess when you're running a simple external command and want a clean, Qt-integrated solution. It handles the event loop integration for you, supports signals for stdout, stderr, and process completion, and doesn't require managing threads.

Use a background QThread when you need more control over how you read the output — for example, if you want to parse each line, filter output, or interact with the subprocess's stdin in complex ways. The thread approach also makes it straightforward to use Python's subprocess module features that don't have direct equivalents in QProcess.

Both approaches keep the GUI responsive and deliver output in real time. Pick whichever fits your situation best.

Complete Example: Live Build Output Viewer in PyQt6

Here's a more polished example that combines the QProcess approach with a few usability improvements — a scrolling output view, a status indicator, and support for running a configurable command. This example uses layouts and basic widgets to build the interface:

python
import sys

from PyQt6.QtCore import QProcess
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import (
    QApplication, QHBoxLayout, QLabel, QLineEdit,
    QMainWindow, QPlainTextEdit, QPushButton,
    QVBoxLayout, QWidget,
)


class BuildOutputViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Live Build Output Viewer")
        self.resize(700, 500)
        self.process = None

        # Command input
        self.command_input = QLineEdit()
        self.command_input.setPlaceholderText(
            "Enter bash command, e.g.: for i in $(seq 1 10); do echo Building step $i; sleep 0.5; done"
        )
        self.command_input.setText(
            "for i in $(seq 1 10); do echo \"Building step $i...\"; sleep 0.5; done && echo Done!"
        )

        # Output area
        self.output_area = QPlainTextEdit()
        self.output_area.setReadOnly(True)
        self.output_area.setFont(QFont("Courier", 10))
        self.output_area.setStyleSheet(
            "QPlainTextEdit { background-color: #1e1e1e; color: #d4d4d4; }"
        )

        # Buttons and status
        self.run_button = QPushButton("Run")
        self.run_button.clicked.connect(self.start_process)

        self.stop_button = QPushButton("Stop")
        self.stop_button.clicked.connect(self.stop_process)
        self.stop_button.setEnabled(False)

        self.status_label = QLabel("Ready")

        button_layout = QHBoxLayout()
        button_layout.addWidget(self.run_button)
        button_layout.addWidget(self.stop_button)
        button_layout.addWidget(self.status_label)
        button_layout.addStretch()

        layout = QVBoxLayout()
        layout.addWidget(self.command_input)
        layout.addLayout(button_layout)
        layout.addWidget(self.output_area)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def start_process(self):
        command = self.command_input.text().strip()
        if not command:
            return

        self.output_area.clear()
        self.run_button.setEnabled(False)
        self.stop_button.setEnabled(True)
        self.status_label.setText("Running...")

        self.process = QProcess(self)
        self.process.readyReadStandardOutput.connect(self.handle_stdout)
        self.process.readyReadStandardError.connect(self.handle_stderr)
        self.process.finished.connect(self.process_finished)

        self.process.start("bash", ["-c", command])

    def stop_process(self):
        if self.process is not None:
            self.process.kill()

    def handle_stdout(self):
        data = self.process.readAllStandardOutput()
        text = bytes(data).decode("utf-8")
        self.output_area.appendPlainText(text.rstrip())

    def handle_stderr(self):
        data = self.process.readAllStandardError()
        text = bytes(data).decode("utf-8")
        self.output_area.appendPlainText(text.rstrip())

    def process_finished(self, exit_code, exit_status):
        status_text = "Finished" if exit_code == 0 else f"Exited with code {exit_code}"
        self.status_label.setText(status_text)
        self.run_button.setEnabled(True)
        self.stop_button.setEnabled(False)
        self.process = None


app = QApplication(sys.argv)
window = BuildOutputViewer()
window.show()
app.exec()

This gives you a terminal-styled output viewer where you can type in a command, run it, watch the output stream in line by line, and stop it if needed — all without the GUI ever locking up.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Packaging Python Applications with PyInstaller by Martin Fitzpatrick

This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

Martin Fitzpatrick

Constantly Print Subprocess Output While Process is Running was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.