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.

@meta-codesync
Copy link

meta-codesync bot commented Oct 8, 2025

@yangdanny97 has imported this pull request. If you are a Meta employee, you can view this in D84155735.

let types: Vec<Type> = class_and_types.iter().map(|(_, ty)| ty.clone()).collect();
if types
.iter()
.any(|ty| matches!(ty, Type::BoundMethod(..) | Type::Function(..)))
Copy link
Contributor

@yangdanny97 yangdanny97 Oct 8, 2025

Choose a reason for hiding this comment

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

Probably also want Type::Overload here; and possibly Type::ForAll for generic functions.

I can patch the PR after import since it's a small change

Copy link
Contributor

@stroxler stroxler left a comment

Choose a reason for hiding this comment

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

Review automatically exported from Phabricator review in Meta.

@meta-codesync meta-codesync bot closed this in 5144e69 Oct 8, 2025
@meta-codesync
Copy link

meta-codesync bot commented Oct 8, 2025

@yangdanny97 merged this pull request in 5144e69.

meta-codesync bot pushed a commit that referenced this pull request Oct 21, 2025
Summary:
This PR is a series of minor diffs building up to the correct handling of multiple inheritance checks for methods, following up from #1196.

Main changes:
- [Skip multiple inheritance check for fields that are overridden](6fccf4a): this diff prevents repeated error reporting when a field gets overridden -- in those cases we don't report inconsistent multiple inheritance
- [Skip multiple inheritance checks for "special" fields](b290fd1): this diff skips checks for multiple inheritances for some special fields like constructors.
- Cherry picking fixes for some problematic function types in stdlib in vendored typeshed

Next steps:
We're close to handle multiple inheritance checks for methods. We still have some errors on stdlib due to handling of type variables (#1196 (comment) item 2), which I consider as a blocker.

Pull Request resolved: #1298

Reviewed By: kinto0

Differential Revision: D85053799

Pulled By: yangdanny97

fbshipit-source-id: 2e6c22916886dc4c760931cf49522a057c7b42aa
fangyi-zhou added a commit to fangyi-zhou/pyrefly that referenced this pull request Nov 2, 2025
Follow up from facebook#1196. Closes facebook#943.

When getting the field type from the parent field, we now check for
instance method types first before using the raw types.

When using the raw types, the type parameters are not initialised, hence
causing issues with functions that use type variables during subtyping
checks. This diff adds a regression test for these cases.
meta-codesync bot pushed a commit that referenced this pull request Nov 6, 2025
Summary:
Follow up from #1196. Closes #943.

When getting the field type from the parent field, we now check for instance method types first before using the raw types.

When using the raw types, the type parameters are not initialised, hence causing issues with functions that use type variables during subtyping checks. This diff adds a regression test for these cases.

Pull Request resolved: #1452

Reviewed By: stroxler

Differential Revision: D86374052

Pulled By: yangdanny97

fbshipit-source-id: 32bb6f9cdfcedcde0a6c310804ddbb6df4be18bb
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.

5 participants