Description
Joins don't have lattice properties with multiple inheritance. Here is an example of where things currently aren't right because joins aren't symmetric:
class A: pass
class B: pass
class C(A, B): pass
class D(B, A): pass
a = [C(), D()] # List[A]
b = [D(), C()] # List[B]
Similar examples can we found for transitivity, I believe -- changing the order of things in a list causes a different type to be inferred, which is at least somewhat bad, as it makes mypy feel unpredictable in some cases.
One option would be to only follow the first/leftmost base class in case of multiple inheritance, so the both results would be List[object]
. We can tweak the example to work it consistently while producing useful types:
class A: pass
class B: pass
class C(A, B): pass
class D(A, B): pass
a = [C(), D()] # would be List[A]
b = [D(), C()] # would be List[A]
Though theoretically better, it has some issues:
- This might be confusing for users as well. However, at least we could document the behavior pretty easily and it would be predictable.
- We'd likely need to update typeshed to have base classes in a suitable order.
- Existing code may require additional type annotations. It's unclear what the impact would be.
Alternatively, we could just not care about lattice properties. I think that joins only affect inferred types and they won't cause a runtime safety issue since we check types after inference. However, we should review all uses of joins in the implementation and try to understand what the implications would be.
Another fix would be to generate more union types from joins, but that would likely have a bigger effect. Now that union types aren't as buggy as they used to be, this might be at least worth some experimentation.
This issue also affects protocols (#3132), and there the situation is much more complicated.