← board

A builtin function is not a first-class value

print — and, by the same mechanism, presumably every other builtin — cannot be passed, stored or returned. Referencing one anywhere but a call position is reported as an undefined variable.

Repro

def call_it(f, text):
    f(text)

def own(text):
    print("own:", text)

call_it(own, "a user function as a value works")   # fine
call_it(print, "print as a value")                 # pascal26:8: error: undefined variable (print)

CPython prints both lines. Measured at 120eeb39f, binary 7cf02ff177ca.

The discriminating half is the first call. A user-defined function already works as a value, so the function-value machinery exists and this is specifically about how builtins are bound: they are recognised at a CALL site rather than being bound to a name the expression parser can reach. That is also why the diagnostic says undefined variable — from the name resolver's point of view there is genuinely nothing called print.

Why it is worth more than its size suggests

A builtin as a callback is a plain Python idiom — announce=print, key=len, sorted(xs, key=abs), map(str, xs). lekkerzeilen uses the first of those three times, identically, to make a loader's progress reporting silenceable:

self.session = session.load(announce=None if quiet else print)

Nothing exotic, and the workaround is a two-line module-level wrapper — which is what the corpus now carries, with a comment pointing here. That workaround is in the DEMO's source, not in pxx, and it is recorded rather than silent: the lekkerzeilen seam is allowed to change (standing owner rule) but a gap papered over without a ticket is how a gap survives.

Scope is unmeasured and the quantifier is the part to check: I verified print only. Whether len, str, abs, sorted behave the same way is a one-line sweep nobody has run, and "presumably every builtin" above is an inference, not a measurement. Run the sweep before ranking this on population.

Where to look

The name resolver's builtin handling, reached from the expression parser's identifier arm rather than its call arm — pyparser.inc. A builtin that is recognised while parsing a CALL needs a value binding too, so that a bare reference yields something callable instead of failing resolution.