Search code examples
pythonpython-3.xpyqt5

Python multiple inheritance does not work as expected


I get an error TypeError: __init__() missing 1 required positional argument: 'label_text' in line QLineEdit.__init__(self) which doesn't make sense to me. I create Input object by passing one keyword argument label_text as Input(label_text="Example label"). I assume an error happens because of how constructors of base classes are called, though I am not sure what's exactly wrong.

from enum import IntEnum, auto
from PyQt5.QtGui import QFocusEvent
from PyQt5.QtWidgets import QLineEdit, QLabel, QTextEdit

class InputType(IntEnum):
    Text = auto()
    Password = auto()

class InputBase:
    def __init__(self, label_text: str) -> None:
        super().__init__()
        self.label_text = label_text
        self.label = QLabel(label_text)
        self.label.setObjectName('input-label')
        self.setPlaceholderText('')

    def focusInEvent(self, event: QFocusEvent) -> None:
        if self.is_empty():
            self.setPlaceholderText("")
            self.label.setText(self.label_text)

        super().focusInEvent(event)
    
    def focusOutEvent(self, event: QFocusEvent) -> None:
        if self.is_empty():
            self.setPlaceholderText(self.label_text)
            self.label.setText(" ")

        super().focusOutEvent(event)
    
    def is_empty(self) -> bool:
        raise NotImplementedError()

class Input(QLineEdit, InputBase):
    def __init__(self, label_text: str, type: InputType = InputType.Text) -> None:
        QLineEdit.__init__(self)
        InputBase.__init__(self, label_text)

        if type == InputType.Password:
            self.setEchoMode(QLineEdit.EchoMode.Password)

    def is_empty(self) -> bool:
        return self.text() == ""

class TextArea(QTextEdit, InputBase):
    def __init__(self, label_text: str) -> None:
        QLineEdit.__init__(self)
        InputBase.__init__(self, label_text)

    def is_empty(self) -> bool:
        return self.toPlainText() == ""

Solution

  • This is caused by the Method Resolution Order (MRO), which you are not respecting in calling the __init__ of the super classes, and is related to the Linkov substitution which should respect the positional arguments in the MRO.

    Shortly put, since the __init__ of InputBase requires a positional argument, and the inheritance is put after the QLineEdit base class, the __init__ of QLineEdit should have that positional argument too.

    A simple solution would be to call the super class methods in the correct order (the opposite of the MRO), but that would not be valid for two reasons:

    1. InputBase relies setPlaceholderText(), which is an attribute that is part of QLineEdit;
    2. based on your code, InputBase should override the Qt functions, but your MRO tries to do the opposite;

    So, the real solution is to use the correct inheritance order: first InputBase, then QLineEdit.

    Consider that it's usually better to avoid mixing super() with explicit calls (especially if they have different positional arguments), unless you really know what you're doing. By doing the above, that would also be unnecessary, so you can just call super() in the init too.

    class Input(InputBase, QLineEdit):
        def __init__(self, label_text: str, type: InputType = InputType.Text) -> None:
            super().__init__(label_text)
    

    Note that you should do the same for TextArea too, which is also wrong right now, since its __init__ calls QLineEdit instead of QTextEdit.