From d0c1da9d5e59c6933762aa58f18d809f6abf95b8 Mon Sep 17 00:00:00 2001 From: Steven Troxler Date: Tue, 14 Apr 2026 17:33:46 -0700 Subject: [PATCH 1/2] Add a test for issue 3121 (return type inference on `__new__`) Summary: Issue 3121 demonstrates bad behavior of recursive return type inference in `__new__` for networkx code, which is using highly dynamic logic to return a "`_dispatch`-like" partial function from `functools.partial`. Inferring something like this is clearly well out of the scope of a type checker; I don't think the static type is even denotable in any useful way, it's relying heavily on duck typing in ways the static type system does not allow. The best we can reasonably do is just pretend constructor returns `_dispatch`, which would avoid many thousands of downstream errors. This specific issue is very important because getting an inferred `__new__` type wrong is likely to cause exceptionally noisy downstream behavior, since almost every instance winds up with a bogus type. This test case demonstrates the issue (in a simpler example where there's no nonconvergence); I'll stack a fix to assume, in accordance with the typing spec, that an unannotated `__new__` means the constructor behaves normally returning an instance of `Self`. Differential Revision: D100705390 --- pyrefly/lib/test/constructors.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyrefly/lib/test/constructors.rs b/pyrefly/lib/test/constructors.rs index 22b268a654..a89e7f75a5 100644 --- a/pyrefly/lib/test/constructors.rs +++ b/pyrefly/lib/test/constructors.rs @@ -989,6 +989,28 @@ C("5") # E: Argument `Literal['5']` is not assignable to parameter `x` with typ "#, ); +// Regression test for a problem in networkx: https://github.com/facebook/pyrefly/issues/3121 +testcase!( + bug = "Following the typing spec, we may assume unannotated `__new__` returns Self, which would avoid bad behaviors in some complex, dynamically-typed codebases", + test_return_type_inference_for_constructors, + r#" +from typing import assert_type + +class A: + def __new__(cls, x: int | None = None): + if x is None: + return cls.__new__(cls, 5) + else: + return object.__new__(cls) + + def __init__(cls): + return "x" + +a = A() +assert_type(a, A) # E: assert_type(A | Unknown, A) +"#, +); + testcase!( test_redundant_dict_constructor_call_ok, r#" From 03c0459e40ff36399bde06d3d0ba4710940eedff Mon Sep 17 00:00:00 2001 From: Steven Troxler Date: Tue, 14 Apr 2026 17:33:46 -0700 Subject: [PATCH 2/2] Assume unannotated `__new__` returns `Self` Summary: This fixes some pretty bad behaviors when dealing with return type inference on complex code that is really dynamically typed, but can be approximated by normal static typing (specifically, the networkx `_dispatch` class). In general, getting bad return type inference on constructors can fan out to a lot of errors, so this is a nice way to reduce FPs against untyped dependencies that might do complicated, dynamic things. It is consistent with the typing spec to do this. Note that `networkx` isn't in mypy primer, but we actually get a significant error reduction on `sympy` which is a good sign that this is a real improvement in practice on untyped code beyond just the one motivating example. Fixes https://github.com/facebook/pyrefly/issues/3120 Differential Revision: D100686394 --- pyrefly/lib/alt/class/class_field.rs | 17 ++++++++++++++++- pyrefly/lib/test/attributes.rs | 2 +- pyrefly/lib/test/constructors.rs | 7 +++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index e2793f3fa9..1915c6294c 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -4089,7 +4089,22 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } else { Instance::of_class(cls) }; - Arc::unwrap_or_clone(new_member.value).as_raw_special_method_type(self.heap, &instance) + let assume_self_return = new_member.value.is_function_without_return_annotation(); + let mut new_ty = Arc::unwrap_or_clone(new_member.value) + .as_raw_special_method_type(self.heap, &instance)?; + if assume_self_return { + // Per the constructor typing spec, unannotated `__new__` may be assumed to + // return `Self` for constructor analysis. + let ret = if preserve_self { + self.heap.mk_self_type(cls.clone()) + } else { + self.heap.mk_class_type(cls.clone()) + }; + new_ty.transform_toplevel_callable(&mut |callable: &mut Callable| { + callable.ret = ret.clone(); + }); + } + Some(new_ty) } } diff --git a/pyrefly/lib/test/attributes.rs b/pyrefly/lib/test/attributes.rs index c2ca21d44d..b899f8e4ac 100644 --- a/pyrefly/lib/test/attributes.rs +++ b/pyrefly/lib/test/attributes.rs @@ -1189,7 +1189,7 @@ class C: if orig_func is None: return super().__new__(cls) def f(): - with C(): # E: `NoneType` has no attribute `__enter__` # E: `NoneType` has no attribute `__exit__` + with C(): pass "#, ); diff --git a/pyrefly/lib/test/constructors.rs b/pyrefly/lib/test/constructors.rs index a89e7f75a5..29cc5c4cb3 100644 --- a/pyrefly/lib/test/constructors.rs +++ b/pyrefly/lib/test/constructors.rs @@ -991,7 +991,6 @@ C("5") # E: Argument `Literal['5']` is not assignable to parameter `x` with typ // Regression test for a problem in networkx: https://github.com/facebook/pyrefly/issues/3121 testcase!( - bug = "Following the typing spec, we may assume unannotated `__new__` returns Self, which would avoid bad behaviors in some complex, dynamically-typed codebases", test_return_type_inference_for_constructors, r#" from typing import assert_type @@ -1006,8 +1005,12 @@ class A: def __init__(cls): return "x" +class B(A): ... + a = A() -assert_type(a, A) # E: assert_type(A | Unknown, A) +assert_type(a, A) +b = B() +assert_type(b, B) "#, );