Python's isinstance method result is unexpected for subclass instance

125 views Asked by At

Defining two classes (a base "ClassA" and a subclass "ClassB" in two separate files), gives unexpected results when using Python's isinstance method. Output appears to be impacted by the module name (namespace?) used while running (__main__). This behavior appears on both Python 3.8.5 and 3.10.4.

File ClassA.py contains:

class ClassA:
    def __init__(self, id):
        self.id = id
    def __str__(self) -> str:
        class_name = type(self).__name__
        return f"{class_name} WITH id: {self.id}"

def main():
    from ClassB import ClassB
    id = 42
    for i, instance in enumerate([ClassA(id), ClassB(id)]):
        label = f"{type(instance).__name__}:"
        print("#" * 50)
        print(f"{label}   type: {type(instance)}")
        label = " " * len(label)  # Convert label to appropriate number of spaces
        is_a = isinstance(instance, ClassA)
        is_b = isinstance(instance, ClassB)
        print(f"{label} is_a/b: {is_a}/{is_b}")
        print(f"{label}    str: {instance}")

if __name__ == "__main__":
    main()

File ClassB.py contains:

from ClassA import ClassA

class ClassB(ClassA):
    def __init__(self, id):
        super().__init__(id)
        self.id *= -1

File main.py contains:

if __name__ == "__main__":
    from ClassA import main
    main()

The output from running ClassA.py gives:

01: ##################################################
02: ClassA:   type: <class '__main__.ClassA'>
03:         is_a/b: True/False
04:            str: ClassA WITH id: 42
05: ##################################################
06: ClassB:   type: <class 'ClassB.ClassB'>
07:         is_a/b: False/True
08:            str: ClassB WITH id: -42

While the output from running main.py (which calls ClassA.main) gives:

01: ##################################################
02: ClassA:   type: <class 'ClassA.ClassA'>
03:         is_a/b: True/False
04:            str: ClassA WITH id: 42
05: ##################################################
06: ClassB:   type: <class 'ClassB.ClassB'>
07:         is_a/b: True/True
08:            str: ClassB WITH id: -42

Notice how the type of the ClassA instance changes (on Lines 02) from '__main__.ClassA' (when run from ClassA.py) to 'ClassA.ClassA' (when run from main.py). Similarly, the isinstance type checks for ClassA and ClassB (on Lines 07) change from 'False/True' (unexpected) to 'True/True' (desired, expected).

Any comments/suggestions/explanations would be helpful. Thanks.

1

There are 1 answers

7
ShadowRanger On

The problem you have here is due to ClassA being defined in your main script, and you having circular imports. You actually have three modules involved in your script, not two:

  1. The main script, under the name __main__ (defines __main__.ClassA), which imports...
  2. ClassB (defines ClassB.ClassB) which imports...
  3. ClassA (defines ClassA.ClassA that is defined identically to __main__.ClassA, but is a unique and independent class) a completely separate copy of the main script, but imported independently under a different name so __main__ related behaviors don't trigger

Importantly, ClassB.ClassB is inheriting from ClassA.ClassA, but main is type-checking against __main__.ClassA, a completely unrelated class.

I've gone over why this doesn't work as expected before (and again in another context), so I'll simplify the answer here for your specific case to: Don't involve __main__ in any circular imports. It can import whatever it likes, but it should not have anything else importing from it. In this case, your main.py refactoring is enough to fix the issue (by ensuring there is only one version of class ClassA, ClassA.ClassA). It does have a circular import dependency, which is always a little weird (I'd recommend moving the function main to main.py to avoid that), but since you're deferring one of the imports it's safe enough.