← board

sorted(xs, key=str) jumps to address 13

xs = [3, 1, 20]
print(sorted(xs, key=str))     # CPython [1, 20, 3]   pxx: Segmentation fault
print(min(xs, key=int))        # CPython 1            pxx: TypeError: expected a number, got object

key=str, key=int, key=float crashed; key=len raised; key=abs failed to compile; key=repr, key=<lambda> and key=<def> were fine. Found 2026-08-15 by a CPython differential sweep — the probe died with SIGSEGV and no output.

Cause — the three callable representations, crossed

A builtin type in value position is pybtype(code), a VT_BTYPE variant (t = str; t(5) works through it). A key= parameter is a raw code Pointer: PyCallKey1 calls straight through it. Coercing the variant into the pointer parameter passed the variant's FIRST WORD — the tag, 13 — as the code address, and f1 := TPyKeyCbF1(13); f1(a0) jumped to address 13.

Exactly project_nilpy_callable_has_three_representations, whose tell is recorded there: the faulting PC is a small tag number.

repr survived because it is a pylib proc, not a type name, so it took the function-value path instead.

Fix

PyFixCallableTypeArgs rewrites a pybtype(<code>) argument in a callback slot to the ADDRESS of the one-argument routine that type performs — new pylib pyconv_str / _int / _float / _bool / _list / _dict / _set, each a one-liner over the existing pybtype_call1. Only the codes that routine implements are mapped; the rest keep its own named refusal.

Before overload matching, not after — which is the measurement that shaped the fix. A post-match version fixed sorted and left min(xs, key=str) binding the scalar min(Variant, Variant) overload with the type as its second operand ("expected a number, got object"): the argument's TYPE is what picks the overload, so a fixup after the fact cannot undo a wrong pick. That is also why the callback question is asked of the NAME's whole overload family (PyNameWantsCallbackAt) rather than of a chosen proc.

The second bug it exposed

With the crash gone, sorted(d.items(), key=str) came out in the wrong ORDER: every key was the empty string. pystr_of(const v: Variant) had no VT_OBJECT arm and fell through to VariantToStr — builtin.pas's low-level SCALAR formatter, which knows nothing about pylib's objects and answers ''. So map(str, [(1,2), [3], {"k":1}]) gave ['', '', ''] while the identical [str(x) for x in ...] was correct, because only the DYNAMIC path comes here. Now renders through pyvar_print_of, the same renderer print uses — project_nilpy_three_rendering_paths_print_str_fstring, again.

Gate

test/test_nilpy_builtin_type_as_callback.npy (+.expected, in the Makefile), byte-identical to CPython: key=str/int/float/bool/repr/len/lambda over sorted, min, max, over ints, floats, strs and dict items; a type still usable as a VALUE (t = str; t(5), L = list; L("ab"), type(5) == int); and str() of a tuple, list, dict and set through map, through a comprehension and directly. gate.sh quick GREEN. Pinned (compiler/builtin/**).