Python Extension Dev: Node with PyQt6/PySide6 Widget freezes on show()

Dear all,

using the new Python Extension Development (Labs) documented here Pure Python Node Extensions Guide | KNIME Documentation and here Python Extension Development (Labs) — KNIME Python API documentation I’m currently trying to implement my own Python-based extension with nodes which should open separate GUIs for the user to interact with - from a simple custom input to complex data interaction.

Unluckily Knime AP freezes when trying to execute the Node. The window of the GUI opens, but only the outline of the window is shown in 95% of the cases (in the remaining 5% the content - which exists in my “real” code is displayed, but the GUI crashes nevertheless). Afterwards the Qt-Window freezes. In some cases, I can kill the window by clicking the “X”, in some cases I have to kill Knime AP.

The entry in the config.yml looks like:

org.my_org.my_extension:
  debug_mode: false
  python_executable: /absolute/path/to/my/python/executable/bin/python
  src: /absolute/path/to/my/extension

I’ll skip the content of the knime.yml since I think this is irrelevant for now.

A minimal working example of the extension’s node code is:

"""PyQt/PySide Qt GUI test node."""

import logging
import sys

import knime.extension as knext  # type: ignore[import-untyped]
from PyQt6.QtWidgets import QApplication, QWidget
# from PySide6.QtWidgets import QApplication, QWidget


LOGGER = logging.getLogger(__name__)

__all__: list[str] = ["MyGUITestNode"]


@knext.node(
    name="GUI Test Node",
    node_type=knext.NodeType.OTHER,
    icon_path="../../icons/my_icon.png",
    category="/",  # defining something else doesn't work as well, btw...
)
class MyGUITestNode:
    """
    Short one-line description of the node.

    Long description of the node which is in fact short.
    """

    def configure(
        self,
        configure_context: knext.ConfigurationContext,  # noqa: ARG002
    ) -> None:
        """
        Configure the Python node.

        Parameters
        ----------
        configure_context
            _description_
        """
        return

    def execute(
        self,
        exec_context: knext.ExecutionContext,
    ) -> None:
        """
        Execute node.

        Parameters
        ----------
        exec_context
            _description_
        """
        # PyQt6 and PySide6 - both equal:
        app = QApplication(sys.argv)
        window = QWidget()
        # this freezes Knime AP:
        window.show()
        # We don't even get here:
        app.exec()

As soon as the window is killed, the log/terminal shows the following stdout/stderr:

ERROR KNIME-Worker-0-MyGUITestNode 3:1 Node Execute failed: Error while sending a command.
py4j.Py4JException: Error while sending a command.
at py4j.CallbackClient.sendCommand(CallbackClient.java:397)
at py4j.CallbackClient.sendCommand(CallbackClient.java:356)
at py4j.reflection.PythonProxyHandler.invoke(PythonProxyHandler.java:106)
at jdk.proxy6/jdk.proxy6.$Proxy61.execute(Unknown Source)
at org.knime.python3.nodes.CloseablePythonNodeProxy.execute(CloseablePythonNodeProxy.java:570)
at org.knime.python3.nodes.DelegatingNodeModel.lambda$4(DelegatingNodeModel.java:233)
at org.knime.python3.nodes.DelegatingNodeModel.runWithProxy(DelegatingNodeModel.java:295)
at org.knime.python3.nodes.DelegatingNodeModel.execute(DelegatingNodeModel.java:231)
at org.knime.core.node.NodeModel.executeModel(NodeModel.java:605)
at org.knime.core.node.Node.invokeFullyNodeModelExecute(Node.java:1331)
at org.knime.core.node.Node.execute(Node.java:1038)
at org.knime.core.node.workflow.NativeNodeContainer.performExecuteNode(NativeNodeContainer.java:618)
at org.knime.core.node.exec.LocalNodeExecutionJob.mainExecute(LocalNodeExecutionJob.java:98)
at org.knime.core.node.workflow.NodeExecutionJob.internalRun(NodeExecutionJob.java:201)
at org.knime.core.node.workflow.NodeExecutionJob.run(NodeExecutionJob.java:120)
at org.knime.core.util.ThreadUtils$RunnableWithContextImpl.runWithContext(ThreadUtils.java:369)
at org.knime.core.util.ThreadUtils$RunnableWithContext.run(ThreadUtils.java:223)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at org.knime.core.util.ThreadPool$MyFuture.run(ThreadPool.java:143)
at org.knime.core.util.ThreadPool$Worker.run(ThreadPool.java:277)
Caused by: py4j.Py4JNetworkException: Error while sending a command: c
p0
execute
to10
ro11
e

    at py4j.ClientServerConnection.sendCommand(ClientServerConnection.java:253)
    at py4j.CallbackClient.sendCommand(CallbackClient.java:384)
    ... 20 more

Caused by: py4j.Py4JException: Received empty command
at py4j.ClientServerConnection.sendCommand(ClientServerConnection.java:236)
… 21 more

Instead opening a GUI from within Knime works like a charm when:

  • Defining anything else in the configure()/execute() methods, also defining inputs/outputs works. –> Extension setup in config.yml, knime.yml and Python Venv should be defined correctly.
  • setting debug: true in the config.yml. Unluckily this is not applicable for a “production quality” extension.
  • executing a Python script containing the code using a Python subprocess call from within the node’s execute() method, see blow for a code snippet. This is not applicable due not allowing for communication with the GUI (except for using pipes, which is far from desired).
  • Using a Python Script Node with the code from execute() instead. This is not applicable for obvious reasons.

I’m using Knime AP 5.8.1 LTS with Python 3.13.7 - but also tried this on Knima AP 5.4 and Python versions 3.10, 3.11 and 3.12 - all combinations result in the same problem. The packages installed in my Python venv are listed at the end of this post.

I’m on Linux with SUSE Linux Enterprise Server 15.

Can you reproduce the error with your setup or do you have an idea how to resolve this?

Many thanks in advance and best regards,

Johannes

1 Like

Can I somehow edit my post? I have to delete my Python Venv details and reduce it to the essential information.

If I can’t delete it on my own: Could you please delete the Venv package list for me?

Thanks in advance!

I deleted the venv package list for you. @carstenhaubold, any input here?

1 Like

@ScottF thanks for editing!

Here is a shortened list of my packages - please tell me if something is missing:

annotated-types==0.7.0
arrow==1.3.0
asttokens==3.0.0
attrs==25.4.0
babel==2.17.0
bcrypt==5.0.0
beautifulsoup4==4.14.2
bidict==0.23.1
binaryornot==0.4.4
bleach==6.2.0
blinker==1.9.0
blosc2==3.10.2
Brotli==1.1.0
build==1.3.0
certifi==2025.10.5
cffi==2.0.0
charset-normalizer==3.4.4
cloudpickle==3.1.2
colorama==0.4.6
colorlog==6.9.0
comm==0.2.3
contourpy==1.3.3
cssselect2==0.8.0
ct3d-parser==1.13.0
cycler==0.12.1
debugpy==1.8.17
decorator==5.2.1
defusedxml==0.7.1
dill==0.4.0
execnet==2.1.1
executing==2.2.1
flake8==7.3.0
Flask==3.1.2
flask-cors==6.0.1
Flask-Login==0.6.3
fonttools==4.60.1
fqdn==1.5.1
future==1.0.0
gevent==25.5.1
geventhttpclient==2.3.4
git-cliff==2.10.1
h11==0.16.0
h5py==3.15.0
id==1.5.0
idna==3.11
imagesize==1.4.1
importlib_metadata==8.7.0
iniconfig==2.1.0
ipykernel==7.0.1
ipython==9.6.0
ipython_pygments_lexers==1.1.1
isoduration==20.11.0
isort==6.1.0
itsdangerous==2.2.0
jedi==0.19.2
jeepney==0.9.0
Jinja2==3.1.6
joblib==1.5.3
json5==0.12.1
jsonpointer==3.0.0
jsonschema==4.25.1
jsonschema-specifications==2025.9.1
jupyter_client==8.6.3
jupyter_core==5.8.1
jupyterlab_pygments==0.3.0
keyring==25.6.0
kiwisolver==1.4.9
libcst==1.8.6
line_profiler==5.0.0
lxml==6.0.2
Markdown==3.10
matplotlib==3.10.7
matplotlib-inline==0.1.7
mdurl==0.1.2
more-itertools==10.8.0
msgpack==1.1.2
mypy==1.18.2
mypy_extensions==1.1.0
narwhals==2.12.0
ndindex==1.10.0
nest-asyncio==1.6.0
nltk==3.9.1
numexpr==2.14.1
numpy==2.3.4
numpy-stl==3.2.0
openpyxl==3.1.5
packaging==25.0
pandas==2.3.3
pandas-stubs==2.3.2.250926
paramiko==4.0.0
parso==0.8.5
pathspec==0.12.1
patsy==1.0.1
pexpect==4.9.0
pillow==12.0.0
platformdirs==4.5.0
plotly==6.5.0
pluggy==1.6.0
plumbum==1.9.0
ply==3.11
prompt_toolkit==3.0.52
protobuf==6.32.1
psutil==7.1.0
py-cpuinfo==9.0.0
py4j==0.10.9.7
pyarrow==22.0.0
pycparser==2.23
pydyf==0.11.0
PyNaCl==1.6.0
pypandoc_binary==1.15
pyparsing==3.2.5
pypdf==6.1.1
PyQt5==5.15.11
PyQt5-Qt5==5.15.17
PyQt5_sip==12.17.1
PyQt6==6.9.1
PyQt6-Qt6==6.9.2
PyQt6_sip==13.10.2
PySide6==6.9.1
PySide6_Addons==6.9.1
PySide6_Essentials==6.9.1
python-dateutil==2.9.0.post0
python-engineio==4.12.3
python-slugify==8.0.4
python-socketio==5.14.1
python-statemachine==2.5.0
python-utils==3.9.1
pytz==2025.2
PyYAML==6.0.3
pyzmq==27.1.0
referencing==0.37.0
regex==2024.11.6
requests==2.32.5
requests-toolbelt==1.0.0
rich==14.2.0
rpds-py==0.27.1
ruff==0.14.0
scikit-learn==1.8.0
scipy==1.16.2
seaborn==0.13.2
setuptools==80.9.0
setuptools-scm==9.2.1
shellingham==1.5.4
shiboken6==6.9.1
simple-websocket==1.1.0
six==1.17.0
snowballstemmer==3.0.1
soupsieve==2.8
stack-data==0.6.3
statsmodels==0.14.4
tables==3.10.2
tabulate==0.9.0
tap.py==3.2.1
text-unidecode==1.3
threadpoolctl==3.6.0
tinycss2==1.4.0
tinyhtml5==2.0.0
tornado==6.5.2
typer==0.19.2
typer-slim==0.19.2
typing-inspect==0.9.0
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2025.2
uri-template==1.3.0
urllib3==2.5.0
wcwidth==0.2.14
weasyprint==66.0
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.9.0
Werkzeug==3.1.3
wheel==0.45.1
wrapt==1.17.3
wsproto==1.2.0
xarray==2025.10.1
zipp==3.23.0
zope.event==6.0
zope.interface==8.0.1
zopfli==0.2.3.post1

Do I get it right that you are building your UI with pyQT instead of using the options that the KNIME API offers?

I think even if you get the windows to open and the user to enter data you will find the next challenge: How to pass any data back from your custom UI to the node…

I don’t think the extension API allows for what you are trying to do.

Hi Martin,

thanks for your reply.

Of course I’m also using the UI provided by the Knime API for configuring my nodes and I really like the new Knime API to define the node configuration via Python code. I’m using this alot. But there are several tasks where building the UI using the Knime API will not work:

  • Complex and application specific UIs which are tailored for a very efficient usage of a very specific task
  • Embedding legacy software written in Qt, PyQt, PySide (well for PySide this is probably not “legacy” but at least already existing software) and other GUI toolkits.
  • Embedding platform independent (not in the OS-meaning) GUI-based software, f.i. software which is written in Python using PyQt for the GUI and meant to be used in the same way in Knime, in a local terminal, …
  • Modular GUIs (separation of functionality) which consist of several small and independent GUI blocks (“widgets”) which may - or may not, depending on the use case - be combined into larger widgets

I can get the windows to open as long as I don’t set debug: false or when using Python Script Node, subprocesses etc., please see the bullet list in my first post for more details and why none of these approaches work in a “productive” environment.

Passing data back from the GUI is no problem at all. When sticking to the Python ecosystem, we can easily return any kind of Python object without even requiring to (de-)serializing/pickling/… it.

Imho the problem is related to threading - something where the traceback also seems to point us to. Maybe there is a way to isolate threads in py4j or define extension/node specific concurrency settings? Or is there any setting in Knime AP, epf-files, … related to threading which could help?

Unluckily I can’t edit my previous post anymore, so I’ll have to make a new one…

Reading the documentation with respect to debug_mode and caching and given the fact that opening GUIs works for debug_mode: true, it feels like the “Gateway Caching” may also be the culprit. @carstenhaubold what do you think?

Is there any way to fine tune caching such that specific code sections may be excluded from caching, f.i. cache everything except for the execute() method?

Hi @joh_elf,

oh wow, that sounds like some serious KNIME node development in Python! Very cool! May I ask for which domain you’re building these nodes?

I am puzzled why your Qt window works with debug_mode=true, but not with false. You’re right, the difference between the two is “gateway caching”, which means that a) we “warm start” Python processes so they are already ready when you click “execute”, and b) that we keep the Python process for configure() running all the time (well, one per extension). With debug_mode=true we always start a fresh process.

As you’re in case a) you do get a new Python process for each execution anyways, it’s just that the Python process might have been running for a while before you click execute. That shouldn’t really be an issue.

However, the processes use a MainLoop that waits for calls from KNIME (see code). I don’t know whether that interferes with Qt eventloops. Probably it does.

And on top, which is my main concern (at least if you were to share the extension outside your organization): opening other UI frameworks from KNIME only works locally and gives a mixed-UI feeling, while we’re striving to make everything available in the WebUI so that at some point KNIME’s frontend can be fully run in a browser. Is that something you’re aware of / is it of importance to you?

Lastly, I remember I did set up an example and used tkinter to get some input from the user from a Python Script node. Maybe that’s somehow interesting for you: python_tkinter_dialog_ask_for_pw – KNIME Community Hub

Hope that helps,
Carsten

4 Likes

Hi @carstenhaubold,

thanks a lot for your detailed and extensive answer!

Let’s talk about side information via mail. I’ll contact you within the next days. :slight_smile:

Thanks for the details and especially the link to GitHub. This helps understanding what could be the problem.

I tried to make a new - not so minimal anymore :wink: - MWE using Queue. I hope this boils it down to the essence of your queueing-logic. But to be honest: That’s just a wiiiiiild guess since I have no idea how you fill/trigger this queue from the calling scope. :slight_smile:

At least this doesn’t freeze the GUIs and correctly awaits until the PyQt eventloop is exited.

Please see below for the MWE:

from queue import Full, Queue
import sys
from threading import Event, Lock

from PyQt6.QtWidgets import QApplication, QWidget


class QtGUIWidget:
    def __init__(self) -> None:
        self.app: QApplication
        self.window: QWidget
        self._execute_finished = Event()

    def run(self) -> None:
        # PyQt6 and PySide6 - both equal:
        self.app = QApplication(sys.argv)
        self.window = QWidget()
        # this freezes Knime AP:
        self.window.show()
        # We don't even get here:
        self.app.exec()

        self._execute_finished.set()

    def result(self) -> list[str]:
        return ["a", "b"]


class QueuedQtGUI:
    def __init__(self) -> None:
        self.queue: Queue[QtGUIWidget | None] = Queue()
        self._main_loop_lock: Lock = Lock()
        self._main_loop_stopped: bool = False

    def enter(self) -> None:
        while True:
            task: QtGUIWidget | None = self.queue.get_nowait()

            if task is not None:
                task.run()
                self.queue.task_done()
            else:
                # Poison pill, exit loop.
                self.queue.task_done()
                return

    def exit(self) -> None:
        """
        Exit the main loop and stop further execution.

        This method is used to gracefully exit the main loop and stop any
        further execution. It empties the queue and puts a None value to the
        queue to signal the completion of all tasks.
        If the queue is full, it will continue to try until it can
        successfully put the `None` value into the queue.
        """
        with self._main_loop_lock:
            self._main_loop_stopped = True
            queue: Queue[QtGUIWidget | None] = self.queue
            # Aggressively try to clear queue and insert poison pill.
            while True:
                with queue.all_tasks_done:
                    queue.queue.clear()
                    queue.unfinished_tasks = 0
                try:
                    queue.put_nowait(None)  # Poison pill
                except Full:
                    continue
                else:
                    break

    def execute(self) -> list[str]:
        """
        Run the given function on the main thread, wait for its results and
        return those.
        """
        with self._main_loop_lock:
            if self._main_loop_stopped:
                msg: str = (
                    "Cannot schedule executions on the main thread after the "
                    "main loop stopped."
                )
                raise RuntimeError(msg)
            execute_task: QtGUIWidget = QtGUIWidget()
            self.queue.put(execute_task)
            return execute_task.result()


qt_queue: QueuedQtGUI = QueuedQtGUI()

qt_queue.execute()
qt_queue.execute()

qt_queue.enter()

Any idea why this could fail when called from within the Knime AP?

Regarding the mixed-UI feeling: Yep, that’s something I’m aware of this but it shouldn’t be a problem for us.

make everything available in the WebUI so that at some point KNIME’s frontend can be fully run in a browser

Does this mean that we’ll have to run a local server (per user, e.g. localhost on 127.0.0.1) to which the Knime frontend connects (as with Jupyter Notebooks) or will this require one central dedicated server? In other words: Will it still be possible to open GUIs and run external processes from the new WebUI?

Thanks in advance and best regards,

Johannes

Hi @joh_elf,

I played around with your example a bit and got it to work via multiprocessing. Is that a workaround that’s suitable for you?

import logging
import knime.extension as knext
import sys

from threading import Event
import multiprocessing

from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout

LOGGER = logging.getLogger(__name__)

class QtGUIWidget:
    def __init__(self) -> None:
        self.app: QApplication
        self.window: QWidget
        self.label: QLabel
        self.button: QPushButton
        self._execute_finished = Event()
        self._button_clicked = False

    def run(self) -> None:
        # PyQt6 and PySide6 - both equal:
        self.app = QApplication(sys.argv)
        self.window = QWidget()
        self.window.setWindowTitle("Qt GUI Widget")

        layout = QVBoxLayout()

        self.label = QLabel("Initial text")
        layout.addWidget(self.label)

        self.button = QPushButton("Click me!")
        self.button.clicked.connect(self.on_button_clicked)
        layout.addWidget(self.button)

        self.window.setLayout(layout)

        # this freezes Knime AP:
        self.window.show()
        # We don't even get here:
        self.app.exec()

        self._execute_finished.set()

    def on_button_clicked(self) -> None:
        if not self._button_clicked:
            # First click: change label and button text
            self.label.setText("Button clicked!")
            self.button.setText("Close me")
            self._button_clicked = True
        else:
            # Second click: close the window
            self.window.close()

    def result(self) -> list[str]:
        return ["a", "b"]


def run_gui_process():
    """Function to be run in a separate process."""
    widget = QtGUIWidget()
    widget.run()

@knext.node(
    name="QT Test Node",
    node_type=knext.NodeType.OTHER,
    icon_path="../../icons/my_icon.png",
    category="/", 
)
class MyGUITestNode:
    """
    Short one-line description of the node.

    Long description of the node which is in fact short.
    """

    def configure(
        self,
        configure_context: knext.ConfigurationContext,  # noqa: ARG002
    ) -> None:
        return

    def execute(
        self,
        exec_context: knext.ExecutionContext,
    ) -> None:

        # Start a second process to run the Qt App
        p = multiprocessing.Process(target=run_gui_process)
        p.start()

        # We can wait for it or continue
        p.join()

2 Likes

Hi @carstenhaubold,

thanks for testing and your feedback. Unluckily it’s still freezing on my machine. :frowning: