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.

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. That means that the UI will not update.

There are two 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.

The wrong approach

First, let's see what happens when you use subprocess and block the event loop.

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() blocked the Qt event loop until it was finished.

Now, let's look at the two solutions to this problem:

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.

Over 15,000 developers have bought Create GUI Applications with Python & Qt!
Create GUI Applications with Python & Qt6
Get the book

Downloadable ebook (PDF, ePub) & Complete Source code

[[ discount.discount_pc ]]% OFF for the next [[ discount.duration ]] [[discount.description ]] with the code [[ discount.coupon_code ]]

Purchasing Power Parity

Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]

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.

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.

PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks

See the course

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

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

Martin Fitzpatrick is the creator of Python GUIs, and has been developing Python/Qt applications for the past 12+ years. He has written a number of popular Python books and provides Python software development & consulting for teams and startups.