← board

A field assigned from a bare local of the same method has no inferable type

The repro

class C:
    def __init__(self):
        cx = 1.0
        self.a = cx          # cannot infer the type of field self.a

c = C()
print(c.a)                   # CPython: 1.0

Four lines, and the diagnostic asks for an annotation on a field whose value is a float two lines above it.

What discriminates

spelling before
self.a = 1.0 OK
self.a = cx + 0.0 OK -- the LITERAL types the expression
self.a = t (bare parameter) OK -- it has its own arm
self.a = self.b OK
cx = 1.0 then self.a = cx refused
cx: float = 1.0 then self.a = cx OK -- the annotation is read
cx = 1 / cx = "s" then self.a = cx refused, so not about float

The type of the local is irrelevant. The one thing that fails is a bare name the pre-pass cannot read a type off, and a local's inferred type does not exist yet when the pre-pass runs -- PyCollectModuleLocalsAST has not run.

Why it is the same family as five arms already there

PyInferFieldDecl is a ladder of readers, and each rung was added for a shape that Python does not annotate: a qualified construction, a bare parameter, None, a module-level literal, a module global holding an instance, a module global holding a def. Every one of those has the identical blind spot -- the name is bound somewhere the token scanners cannot type -- and every one was answered rather than pushed onto the programmer. The method's own local was the rung nobody had needed yet.

The fix

PyMethodBindsLocal answers only "does this method bind this name", in the four shapes Python binds one at statement level: nm = ..., nm, other = ..., for nm in ... / for a, nm in ..., and ... as nm. PyRhsOnlyNamesThisMethodBinds accepts a right-hand side built only from parameters and such locals. The field then takes tyVariant -- the same answer the bare-parameter arm gives, and what an unannotated local carries anyway, so the runtime tag travels with the value.

A BINDING test and not a type reader, deliberately. The diagnostic exists to catch a typo, and a typo is not a parameter and not a local, so it still fires. A CALL ends the scan as well: self.v = f(cx) is the expression scanner's job and it types it correctly.

for and as are ordinary IDENTIFIERS to this lexer -- PyKeyword binds only the words the Pascal side needs a distinct kind for. A test against tkFor compiles and never fires. Found by writing one.

Log