Summary
constructors_call_init fires only when the constructor call is the outermost expression of a statement (or the RHS of an assignment). The moment the result is used — C(1).attr, C(1).method() — the arity check is skipped.
Found via the "type torture" puzzle on X: https://x.com/charliermarsh/status/2082885991686090885
Reproduced on a clean cargo build --release of main @ 5b756d3; identical on CLI and playground.
Minimal reproduction
class C: ...
C(1) # line 2 — CAUGHT
C(1).__class__ # line 3 — SILENT
x = C(1) # line 4 — CAUGHT
Output:
error[constructors_call_init]: Class `C` does not define `__init__` or `__new__` and inherits only from `object`; constructor does not accept arguments
--> k1.py:2:1
error[constructors_call_init]: Class `C` does not define `__init__` or `__new__` and inherits only from `object`; constructor does not accept arguments
--> k1.py:4:5
Line 3 is the same erroneous call and is not reported.
Same shape with a method call:
def f() -> int: return 0
class C:
g = f
C(1) # CAUGHT
C(1).g() # SILENT
Why it matters beyond the toy case
This is what hid the error in the reported torture puzzle. C(C()()).f() should report that C inherits object.__init__ and takes no arguments, but because the constructor call is the receiver of a trailing .f(), the check never runs. Chained construction — Config(path).load(), Session(url).get() — is an extremely common Python idiom, so this is a large real-world hole, not an edge case.
Controls (all CAUGHT, so the class shape is not the trigger)
class C: ...
C(1) # literal arg
C(x) # name arg
C(mk()) # call arg
C(C()) # constructor arg
C(C()()) # nested-call arg
class C:
g = 1 # assigned attribute
class C:
g: int # annotated attribute
class C:
__call__: "C"
class C:
def m(self) -> None: ...
Every one of the above reports correctly when written as a bare C(1). Only wrapping the call in an attribute access or method call suppresses it.
Expected
Constructor arity is a property of the call expression, not of its syntactic position. The rule should visit every Call node whose callee resolves to a class, wherever that call appears — statement, receiver, argument, subscript base, comprehension element, or return value.
Summary
constructors_call_initfires only when the constructor call is the outermost expression of a statement (or the RHS of an assignment). The moment the result is used —C(1).attr,C(1).method()— the arity check is skipped.Found via the "type torture" puzzle on X: https://x.com/charliermarsh/status/2082885991686090885
Reproduced on a clean
cargo build --releaseofmain@ 5b756d3; identical on CLI and playground.Minimal reproduction
Output:
Line 3 is the same erroneous call and is not reported.
Same shape with a method call:
Why it matters beyond the toy case
This is what hid the error in the reported torture puzzle.
C(C()()).f()should report thatCinheritsobject.__init__and takes no arguments, but because the constructor call is the receiver of a trailing.f(), the check never runs. Chained construction —Config(path).load(),Session(url).get()— is an extremely common Python idiom, so this is a large real-world hole, not an edge case.Controls (all CAUGHT, so the class shape is not the trigger)
Every one of the above reports correctly when written as a bare
C(1). Only wrapping the call in an attribute access or method call suppresses it.Expected
Constructor arity is a property of the call expression, not of its syntactic position. The rule should visit every
Callnode whose callee resolves to a class, wherever that call appears — statement, receiver, argument, subscript base, comprehension element, or return value.