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.
The problem you have here is due to
ClassAbeing defined in your main script, and you having circular imports. You actually have three modules involved in your script, not two:__main__(defines__main__.ClassA), which imports...ClassB(definesClassB.ClassB) which imports...ClassA(definesClassA.ClassAthat 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 triggerImportantly,
ClassB.ClassBis inheriting fromClassA.ClassA, butmainis 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, yourmain.pyrefactoring is enough to fix the issue (by ensuring there is only one version of classClassA,ClassA.ClassA). It does have a circular import dependency, which is always a little weird (I'd recommend moving the functionmaintomain.pyto avoid that), but since you're deferring one of the imports it's safe enough.