Tyler Thornock
Technical Animator
Home Tutorials Tools Rigs About/Resume
Qt Designer – Type Hinting (python)

One of the drawbacks to using Qt Designer to create your UIs is that your IDE, like JetBrains PyCharm or Visual Studio Code, loses the ability to auto-complete and type hint. This can be extremely annoying, especially if you are using promoted widgets, where even your own class members aren’t working. I will go over some of the possible techniques here.

The first thing you will need to do is to parse the xml of the .ui file to extract the widgets and actions that will be created. NOTE: Technically you could use uic to convert a .ui to file to python code that you might be able to use for hinting instead, but I found the parsing method more flexible.

from xml.etree import ElementTree

# UI saved from Qt Designer
ui_path = 'D:/window.ui'

# storage for imports
header_lines = [
'from PySide2 import QtWidgets',    # replace this if you are not using PySide
]

# storage for widget/action, where each line will represent a type hint
lines = []

# parse the xml
form = ElementTree.parse(ui_path)

# get the import line for promoted widgets
promoted = []
for widget in form.iter('customwidget'):
    p_class = widget.find('class')
    p_header = widget.find('header')
    if any([p_class, p_header]):
        # probably an incorrect promoted widget setting
        continue

    # real class name
    p_class = p_class.text
    # header represents the module containing the promoted widget
    p_header = p_header.text

    # I do a from import here since it is easier, shorter, and should never get called
    # buuut could have broken edge cases (eg. a widget named QtWidgets)
    header_lines.append('    from {} import {}'.format(p_header, p_class))
    promoted.append(p_class)

# get the widgets and build its line
form_name = ''
form_class = ''
for w, widget in enumerate(form.iter('widget')):
    if w == 0:
        # depending on how you load your ui file, you might need this
        form_name = widget.attrib['name']
        form_class = widget.attrib['class']
        continue
    
    # objectName must be specified or skip
    w_name = widget.attrib['name']
    if not w_name:
        continue
    
    # if this is not a promoted widget (where above we did from import), we need to get the right QtWidget
    w_class = widget.attrib['class']
    if w_class not in promoted:
        # handle special case separators from designer named "Line"
        if w_class == 'Line':
            w_class = 'QFrame'
        
        # standard widgets should always come from QtWidgets
        w_class = 'QtWidgets.{}'.format(w_class)
        
    # the objectName will represent the variable for the window class, while the w_class is the widget's class
    lines.append('{} = {}()'.format(w_name, w_class))

# now the easy part, actions
for action in form.iter('action'):
    lines.append('{} = QtWidgets.QAction()'.format(action.attrib['name']))

# this will represent the bulk of what is used in type-hinting
header_lines.append('')
print('\n'.join(header_lines))
print('\n'.join(lines))

Sidecar .pyi File:

A pyi file is just used for hinting and is never imported. The name of the file must match the name of the file with your Window. This declutters your window file and you could even make a generate button that remakes the file without having to paste in updates. If you decided to try the uic method for generating the python code, you might want to look into this method too.

For this you will want to add this step after the original generation code above.

# this just inserts the class line and tabs the rest
lines.insert(0, 'class {} (QtWidgets.{}):'.format(form_name, form_class)) 
print('\n'.join(header_lines))
print('\n\t'.join(lines))

Now just copy that into the sidecar file and your IDE should pick it up and allow auto-complete.

Stub inside Window:

With this technique, there is no sidecar file to risk being out of date or to manage and is built right into the Window (assuming their IDE understands this technique). However, it is slightly annoying to paste in updates and can kinda feel like clutter.

What is next could require a bit of trail and error, and depends on how you actually load your UI file. More commonly I have seen people loading into self.ui, where the objectName of each widget is a variable. Another method populates self in a similar way (which risks overwriting built-ins). And there may be other ways. But the main trick IDEs typically support, is an if False statement that never gets ran, but can provide type hinting.

# self.ui method
class Window(QtWidgets.QMainWindow):
    def __init__(*args, **kwargs):
        super(Window, self).__init__(*args, **kwargs)
        
        # simplified, but this can vary, the point is where do you access your widgets
        self.ui = your_load_ui(ui_path)
        self.ui.SetupUi(self)
        
        if False:
            # hax
            self.ui = self._Stub()
            
    def another_func(self):
        # make sure type hinting for any method works outside of the init
    
    class _Stub():
        # place code generated from the ui file here


# self method
form_class = your_load_ui(ui_path)

class Window(form_class):
    def __init__(*args, **kwargs):
        super(Window, self).__init__(*args, **kwargs)

    def another_func(self):
        # make sure type hinting for any method works outside of the init

    def _stub(self):
        # place code generated from the ui file here (prefix each widget/action line with self
        # depending on your load technique, this may not work for self's inherited members, you may want a .pyi file instead

Hopefully this at least explains some options, even if the way you set your UI up is different. Goodluck!