← board

A def returning a pre-created global has no usable return type

class K:
    def __init__(self):
        self.z = 7

def rd():
    return g            # g is pre-created by PyAllocModuleGlobals

g = K()
print(rd().z)           # pascal26: error: unexpected token   near: rd >>> z

CPython prints 7. Binding first works:

r = rd()
print(r.z)              # fine

So the value and its class are right; only <call>.field directly fails, and it fails at PARSE time, not at run time.

Pre-existing

Identical on stable_linux_amd64/default/pinned. Found while fixing [[bug-nilpy-global-named-like-a-ctor-param-breaks-a-bound-method-value]], whose test wanted plain_reader().hits; that test now binds to a name first and names this ticket in a comment.

Likely shape

rd's return type is inferred by PyInferDefRetType from its body. The body is return g, and g is a pre-created module global — the symbol exists when the def is parsed, so the read resolves, but whatever the inference records is not a class the selector path can use, so .z has nothing to bind against and the parser reports an unexpected token rather than a typed error.

Note the fix to the parent ticket makes the pre-created symbol carry its class (tyClass + RecName) when the pre-pass inferred one, so the information IS present now — this may be a matter of PyInferDefRetType reading it. Worth re-measuring before designing anything: the parent's fix landed after this was observed, and the observation above was made on a build that already had it (it failed identically on both, so the fix does not address it — but confirm).

Gate

Per-fix loop, plus a .npy test covering rd().field, rd().method() and the bind-first form, diffed against CPython.

2026-08-07 — narrowed: not ordering, and the fix hits a two-pass ABI constraint

The guess in "Likely shape" (that the parent's class-carrying fix might have already supplied the information) is wrong — re-measured after that fix landed and rd().z still fails. Two more measurements move it on:

shape result
def rd(): return g with g = K() above it still fails
def rd2(): return K() (a fresh construction) works
r = rd(); r.z (bind first) works

So it is not declaration order, and not the pre-created global's type: a returned CONSTRUCTION types fine, a returned bare IDENT naming a module global does not. And the value is genuinely a K at run time — only the call node's STATIC type is missing, which is why binding to a local first works (the local is typed by the trial parse).

Where it stops

PyInferDefRetType's bare-ident path (pyparser.inc ~18355) chases "the ident's ASSIGNMENT earlier in the body". A module-level global is assigned outside the body, so there is nothing to chase, and PyInferExprType only resolves a bare ident through PyLocals.

That is the constraint, and it is recorded in the code right there: PyLocals exists in the HEADER pass but not in the pre-pass, and the comment warns that trusting it in one and not the other made "the two passes infer different return types, a silent ABI mismatch." So teaching only the header pass to resolve module globals reintroduces exactly that hazard.

A correct fix has to make the global's type available to BOTH passes. The natural home is the repeat … until not changed fixpoint in PyCollectModuleLocalsAST — re-infer def return types inside it — but the ordering is genuinely circular (typing a global assigned from a call needs the def's return type; typing that return needs the global), which is why the loop exists at all.

Parked

Not a small fix, and the failure mode of getting it wrong is a silent ABI mismatch rather than a loud error. The workaround (r = rd() then r.z) is correct, cheap and already used in test/test_nilpy_global_read_above_its_assignment.npy with a comment naming this ticket.