Skip to content

Conversation

fangyi-zhou
Copy link
Contributor

@fangyi-zhou fangyi-zhou commented Sep 28, 2025

This diff is an attempt to address the issue in #943.

When a class inherits from multiple base classes, we check whether the intersection of the field types is never. If so, we raise an error.

Next step: Handle method overrides / overloads.

@meta-cla meta-cla bot added the cla signed label Sep 28, 2025
@connernilsen
Copy link
Contributor

Hey @fangyi-zhou, thanks for working on this!

It looks like there are a few broken tests, would you be able to take a look at those? In the meantime, I'll tag @stroxler and @yangdanny97, since they were active on the initial issue.

@fangyi-zhou
Copy link
Contributor Author

It looks like there are a few broken tests, would you be able to take a look at those?

This is the tricky part (and hence why I marked this diff as RFC). This change surfaces some issues with standard library in vendored typeshed.

Error messages are re-formatted for readability

(1) There are some issues with name mismatches in importlib. I raised a PR to typeshed to reconcile the differences python/typeshed#14809. These errors are due to we require parameter names in function to match when subtyping.

ERROR _frozen_importlib_external.pyi:132:7-27: Inconsistent types for field `load_module` inherited from multiple base classes: 
 `(self: Self@FileLoader, name: str | None = None) -> ModuleType` from `FileLoader`, 
 `(self: Self@_LoaderBasics, fullname: str) -> ModuleType` from `_LoaderBasics` [inconsistent-overload]
ERROR _frozen_importlib_external.pyi:136:7-26: Inconsistent types for field `load_module` inherited from multiple base classes: 
 `(self: Self@FileLoader, name: str | None = None) -> ModuleType` from `FileLoader`, 
 `(self: Self@_LoaderBasics, fullname: str) -> ModuleType` from `_LoaderBasics` [inconsistent-overload]
ERROR _frozen_importlib_external.pyi:188:11-31: Inconsistent types for field `__init__` inherited from multiple base classes: 
 `(self: Self@ExtensionFileLoader, name: str, path: str) -> None` from `ExtensionFileLoader`, 
 `(self: Self@FileLoader, fullname: str, path: str) -> None` from `FileLoader` [inconsistent-overload]
ERROR _frozen_importlib_external.pyi:188:11-31: Inconsistent types for field `load_module` inherited from multiple base classes: 
 `(self: Self@FileLoader, name: str | None = None) -> ModuleType` from `FileLoader`, 
 `(self: Self@_LoaderBasics, fullname: str) -> ModuleType` from `_LoaderBasics` [inconsistent-overload]

(2) There are some issues with subtyping generic function (?)

ERROR typing.pyi:583:7-25: Inconsistent types for field `__next__` inherited from multiple base classes: 
 `(self: Self@Generator) -> _YieldT_co` from `Generator`, 
 `(self: Self@Iterator) -> _T_co` from `Iterator` [inconsistent-overload]
ERROR typing.pyi:583:7-25: Inconsistent types for field `__iter__` inherited from multiple base classes: 
 `(self: Self@Generator) -> Generator[_YieldT_co, _SendT_contra, _ReturnT_co]` from `Generator`, 
 `(self: Self@Iterator) -> Iterator[_T_co]` from `Iterator`, 
 `(self: Self@Iterable) -> Iterator[_T_co]` from `Iterable` [inconsistent-overload]

(3) Handling overloads (?)

ERROR _typeshed/__init__.pyi:294:7-28: Inconsistent types for field `__getitem__` inherited from multiple base classes: 
 `(self: Self@SliceableBuffer, slice: slice[Any, Any, Any], /) -> Sequence[int]` from `SliceableBuffer`, 
 `(self: Self@IndexableBuffer, i: int, /) -> int` from `IndexableBuffer` [inconsistent-overload]

@fangyi-zhou fangyi-zhou force-pushed the inconsistent-inheritance-check branch from 74caf7c to 6d70077 Compare September 30, 2025 23:35
@yangdanny97
Copy link
Contributor

yangdanny97 commented Oct 3, 2025

Nice work on the typeshed PR!

The overall approach makes sense, I'll do a more detailed review soon.

Our intersection operation is a bit shaky, so I'm not surprised if there's a bug or two buried there that affects this PR. If it turns out to be very difficult to resolve, we could merge this with the check restricted to TypedDicts (or restricted to non-function types), and iterate till we eventually can run this for everything.

self.error(
errors,
cls.range(),
ErrorInfo::Kind(ErrorKind::InconsistentOverload),
Copy link
Contributor

@yangdanny97 yangdanny97 Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might need a new error code here

cls.range(),
ErrorInfo::Kind(ErrorKind::InconsistentOverload),
format!(
"Inconsistent types for field `{field_name}` inherited from multiple base classes: {class_and_types_str}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can split this into multiple lines,

pub fn add(&self, range: TextRange, info: ErrorInfo, mut msg: Vec1<String>) {

takes a vec1 of lines

let mut inherited_fields: SmallMap<&Name, Vec<(&Name, Type)>> = SmallMap::new();

for parent_cls in mro.ancestors_no_object().iter() {
let class_fields = parent_cls.class_object().fields();
Copy link
Contributor

@yangdanny97 yangdanny97 Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some extra work being done here? For example given the class hierarchy

class A:
  x: T1
class B:
  x: T2
class C(A, B):
  x: T3
class D:
  x: T4
class E(C, D): ...

Also, when doing the check for E, we're also checking whether A and B's impls are compatible with each other, but that should have already been checked and the error raised in C's definition, so we'd be raising an extra error.

Maybe that's not a bad thing, it does affect E as well, but just something to consider.

Given this snippet

class A:
  x: int
class B:
  x: str
class C(A, B): pass
class D:
  x: int
class E(C, D): pass

mypy errors on both C and E, pyright only errors on C

This logic also doesn't seem to account for overrides. For example,

class A:
  x: int
class B:
  x: str
class C(A, B):
  x: int
class D:
  x: int
class E(C, D): pass

Here we would check the impl of x from A and B, even though they're both overridden by the impl from C. Mypy only errors on C in this case, but I suspect we would error on E as well.

I wonder if this means we could get away with looking up a single copy of the field from each base class, using the MRO. That would be fewer things to check than looking up every occurrence of that field from anywhere in the class hierarchy.

@yangdanny97
Copy link
Contributor

yangdanny97 commented Oct 3, 2025

For the 3rd example you gave

ERROR _typeshed/__init__.pyi:294:7-28: Inconsistent types for field `__getitem__` inherited from multiple base classes: 
 `(self: Self@SliceableBuffer, slice: slice[Any, Any, Any], /) -> Sequence[int]` from `SliceableBuffer`, 
 `(self: Self@IndexableBuffer, i: int, /) -> int` from `IndexableBuffer` [inconsistent-overload]

it's from here

https://github.com/python/typeshed/blob/bee1e1f551c1c4b74b1cbb250ffd1152290b40a5/stdlib/_typeshed/__init__.pyi#L305

The parent implementations are definitely not compatible with each other, but as long as the child's overload is compatible with both I think it's fine.

I'm not sure what the fix here should be - do we skip methods entirely? (that seems excessive) or could we make an intersection of two incompatible methods generate an overload with both signatures?

@yangdanny97
Copy link
Contributor

yangdanny97 commented Oct 3, 2025

For the second issue you raised, maybe @samwgoldman has an opinion on this.

Maybe we need to do some sort of type var substitution (replacing all the parent class's type vars with the child class's type vars so that these checks are all using the same type vars).

@fangyi-zhou fangyi-zhou force-pushed the inconsistent-inheritance-check branch from 6d70077 to b7bd774 Compare October 5, 2025 00:32
@fangyi-zhou
Copy link
Contributor Author

I'm made some changes according to the comments, but this stack is not ready for another review yet. I'll investigate another day how to deal with the overloads.

@fangyi-zhou
Copy link
Contributor Author

fangyi-zhou commented Oct 5, 2025

ERROR _frozen_importlib_external.pyi:188:11-31: Inconsistent types for field __init__ inherited from multiple base classes:
(self: Self@ExtensionFileLoader, name: str, path: str) -> None from ExtensionFileLoader,
(self: Self@FileLoader, fullname: str, path: str) -> None from FileLoader [inconsistent-overload]

This seems to be a problem in the standard library.

For FileLoader:
https://github.com/python/cpython/blob/d1ca001d357400d3f1f64e7fa48ace99a59c558f/Lib/importlib/_bootstrap_external.py#L921

For ExtensionFileLoader:
https://github.com/python/cpython/blob/d1ca001d357400d3f1f64e7fa48ace99a59c558f/Lib/importlib/_bootstrap_external.py#L1044

Maybe we should exclude constructors from this check?

@yangdanny97
Copy link
Contributor

yangdanny97 commented Oct 6, 2025

If the overloads ends up being tricky or affecting a lot of other behaviors, we could merge a version of this that excludes overloads & implkement that part separately.

Re: the constructor stuff, i think that makes sense to skip it. Oftentimes classes will override constructors with completely different signatures, and our override consistency check also skips it. The override consistency check implementation may be a useful reference for what should/should not be skipped for this analysis

https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/class/class_field.rs#L1751

This diff is an attempt to address the issue in facebook#943.

When a class inherits from multiple base classes, we check whether the
intersection of the types of the fields is never. If that's the case, we
raise an error.

Note 1:
There are some errors when type checking the builtins (which is
reflected by errors in scrut tests). They seem to be caused by
subtyping checks of self types in functions and generic function types.

Note 2:
Should this be a new error category? I'm currently reusing the closest
error category for inconsistent overloads, but that looks very specific.
@fangyi-zhou fangyi-zhou force-pushed the inconsistent-inheritance-check branch from dbfadfe to 9e00839 Compare October 7, 2025 22:05
@fangyi-zhou fangyi-zhou changed the title [RFC] Check for compatibility when inheriting from multiple classes Check for compatibility when inheriting from multiple classes for fields Oct 7, 2025
@fangyi-zhou
Copy link
Contributor Author

Let's get the field check merged in first, and deal with methods in a follow up diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants