You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Malformed builtin __text_signature__ makes inspect.signature(len/sorted/open) raise ValueError #8383
inspect.signature() fails on common builtins (len, sorted, open, …) with ValueError: <...> builtin has invalid signature, where CPython returns a proper Signature. The cause is that the auto-generated __text_signature__ values are malformed: they expose the Rust parameter names and omit the positional-only (/) and keyword-only (*) markers, so they are either rejected by inspect or semantically wrong.
Reproduction
importinspectprint(inspect.signature(len))
# RustPython: ValueError: <built-in function len> builtin has invalid signature# CPython: (obj, /)
Same ValueError for inspect.signature(sorted) and inspect.signature(open).
RustPython actually populates __text_signature__ for more builtins than CPython, but the values are generated from the Rust function's parameters (opts, args, iter_target, …) and lack the / / * separators, so:
inspect.signature raises ValueError instead of returning a signature, and
where it would parse, the signature is wrong (e.g. sorted would show (iterable, opts) rather than (iterable, /, *, key=None, reverse=False)).
Context
This is a follow-up to #2410 ("Builtin functions and methods lack a .__text_signature__ property"), which is closed — the attribute now exists, but its values do not match CPython and break inspect.signature.
Producing fully accurate signatures for every builtin is a large effort (CPython hand-authors them via Argument Clinic), so a reasonable minimum would be that inspect.signature() returns something usable (or raises the same way CPython does for genuinely signature-less builtins) rather than ValueError on len/sorted/open.
Reference
Verified against CPython 3.14.5.
Investigated and drafted by Claude; reviewed before filing.
Status: the reported ValueError no longer reproduces
>>>importinspect; inspect.signature(len)
(obj, /) # CPython 3.14
(module, /, obj) # RustPython today; was ValueError when this issue was filed
#8390 (1613741f8) made TokenKind::Unknown map to TOKEN_OP, so tokenize yields $ as an OP token and _signature_strip_non_python_syntax removes it. Only the parse changed; the signature is still wrong. The same commit added an expectedFailure to test_autospec_on_bound_builtin_function for this reason, since time.ctime gained a parseable but incorrect signature. That is the one currently failing CPython test attributable to this issue.
Current state
All 45 builtin functions common to both, against CPython 3.14:
Behaviour matching CPython
0 / 45
Phantom module parameter
45 / 45
Missing / marker
45 / 45
Still raising ValueError
2 (round, sum)
Root cause
Every builtin signature comes from text_signature() in crates/derive-impl/src/util.rs, which reads the Rust parameter list and joins the names with ", ". Four defects.
(1) $module is emitted with nothing able to strip it. CPython emits it too, but its C functions receive the module as self, so __self__ is the module and _signature_fromstr drops the parameter. RustPython's #[pyfunction]s take no module argument, PyNativeFunction.zelf (the field behind __self__) is None, and the parameter survives as a real one.
(2) / is never emitted.util.rs contains no code that produces it. Plain arguments bind through take_positional() (function/argument.rs) and are positional-only in practice, so inspect reporting POSITIONAL_OR_KEYWORD contradicts len(obj=[1,2]) raising TypeError.
(3) Non-identifier patterns are stringified verbatim.
func_sig() falls back to quote!(#other).to_string() for any non-ident pattern, producing text that is not parseable Python. round and sum here, plus os.pathconf, binascii.b2a_base64 and binascii.b2a_uu elsewhere in the stdlib.
opts is not a parameter. SortOptions expands to key=None, reverse=False, both keyword-only. The proc macro sees only the token SortOptions and cannot resolve the type, so it can neither expand it nor tell it apart from an ordinary PyObjectRef. Option A below cannot address this.
Options considered
A. Improve the generator. Bail on non-ident patterns and OptionalArg, drop $module, emit /, rename parameters to CPython's.
B. Source signatures from the doc DB.crates/doc/generate.py already introspects real CPython for docstrings, and __text_signature__ sits on the same objects. Under B, (3) and (4) do not arise: the string never passes through the macro, so opts and RoundArgs { .. } never appear. Copying alone does not fix (1), since __self__ is still None, but stripping the leading parameter at store time does:
remove the first ($-prefixed) parameter
if a bare `/` is then leading, remove it too
The second step applies to signatures shaped ($module, /, ...), where the / marked only the removed parameter (176 across the stdlib).
Measured on the 45 builtins:
A
B
Behaviour matching CPython
24 / 45
45 / 45
ValueError remaining
0
0
Struct cases (sorted, open, …)
wrong
correct
RustPython binds some of these through FromArgs structs CPython knows nothing about, so I checked the copied signatures are truthful: all 37 keyword-capable parameters documented across the 10 affected builtins are accepted or rejected identically by both, and the defaults agree behaviourally.
Two further notes on B. generate.py can reassemble name(sig)\n--\n\ndoc, the format get_text_signature_from_internal_doc already parses, so the consuming side needs no change. Scope reaches beyond builtins to the rest of the stdlib and to type methods (6 of 10,135 currently carry a signature), at +186 KB on data.inc.rs.
Proposal
Take B, keeping A's generator as the fallback for entries the DB does not cover. Staged so the first PR is behaviour-neutral:
generate.py emits the combined form, with the leading parameter stripped for module-level functions.
The macro uses a DB entry as-is when it already carries a signature, otherwise falls back to the current generator.
Regenerating data.inc.rs turns it on.
Methods need no stripping, since __self__ is already correct for bound methods and $self signatures resolve identically to CPython.
Questions
Is extending the doc DB to carry signatures acceptable?
Stripping $module makes __text_signature__ differ textually from CPython ('(obj, /)' vs '($module, obj, /)') while inspect.signature() matches exactly. Acceptable, or would you prefer fixing __self__? Setting zelf to the module would prepend it on every call and disable the CallLen/CallIsinstance specializations that key on native.zelf.is_none(), so I would rather file that separately.
Could you run update-doc-db once the generator change lands? Contributors cannot dispatch it.
Investigated and drafted by Claude; reviewed before posting.
Activity
leehanjeong commentedon Jul 26, 2026
take
leehanjeong commentedon Aug 2, 2026
Status: the reported
ValueErrorno longer reproduces#8390 (
1613741f8) madeTokenKind::Unknownmap toTOKEN_OP, sotokenizeyields$as anOPtoken and_signature_strip_non_python_syntaxremoves it. Only the parse changed; the signature is still wrong. The same commit added anexpectedFailuretotest_autospec_on_bound_builtin_functionfor this reason, sincetime.ctimegained a parseable but incorrect signature. That is the one currently failing CPython test attributable to this issue.Current state
All 45 builtin functions common to both, against CPython 3.14:
moduleparameter/markerValueErrorround,sum)Root cause
Every builtin signature comes from
text_signature()incrates/derive-impl/src/util.rs, which reads the Rust parameter list and joins the names with", ". Four defects.(1)
$moduleis emitted with nothing able to strip it. CPython emits it too, but its C functions receive the module asself, so__self__is the module and_signature_fromstrdrops the parameter. RustPython's#[pyfunction]s take no module argument,PyNativeFunction.zelf(the field behind__self__) isNone, and the parameter survives as a real one.(2)
/is never emitted.util.rscontains no code that produces it. Plain arguments bind throughtake_positional()(function/argument.rs) and are positional-only in practice, soinspectreportingPOSITIONAL_OR_KEYWORDcontradictslen(obj=[1,2])raisingTypeError.(3) Non-identifier patterns are stringified verbatim.
func_sig()falls back toquote!(#other).to_string()for any non-ident pattern, producing text that is not parseable Python.roundandsumhere, plusos.pathconf,binascii.b2a_base64andbinascii.b2a_uuelsewhere in the stdlib.(4)
FromArgsstructs collapse to a single name.optsis not a parameter.SortOptionsexpands tokey=None, reverse=False, both keyword-only. The proc macro sees only the tokenSortOptionsand cannot resolve the type, so it can neither expand it nor tell it apart from an ordinaryPyObjectRef. Option A below cannot address this.Options considered
A. Improve the generator. Bail on non-ident patterns and
OptionalArg, drop$module, emit/, rename parameters to CPython's.B. Source signatures from the doc DB.
crates/doc/generate.pyalready introspects real CPython for docstrings, and__text_signature__sits on the same objects. Under B, (3) and (4) do not arise: the string never passes through the macro, sooptsandRoundArgs { .. }never appear. Copying alone does not fix (1), since__self__is stillNone, but stripping the leading parameter at store time does:The second step applies to signatures shaped
($module, /, ...), where the/marked only the removed parameter (176 across the stdlib).Measured on the 45 builtins:
ValueErrorremainingsorted,open, …)RustPython binds some of these through
FromArgsstructs CPython knows nothing about, so I checked the copied signatures are truthful: all 37 keyword-capable parameters documented across the 10 affected builtins are accepted or rejected identically by both, and the defaults agree behaviourally.Two further notes on B.
generate.pycan reassemblename(sig)\n--\n\ndoc, the formatget_text_signature_from_internal_docalready parses, so the consuming side needs no change. Scope reaches beyond builtins to the rest of the stdlib and to type methods (6 of 10,135 currently carry a signature), at +186 KB ondata.inc.rs.Proposal
Take B, keeping A's generator as the fallback for entries the DB does not cover. Staged so the first PR is behaviour-neutral:
generate.pyemits the combined form, with the leading parameter stripped for module-level functions.data.inc.rsturns it on.Methods need no stripping, since
__self__is already correct for bound methods and$selfsignatures resolve identically to CPython.Questions
$modulemakes__text_signature__differ textually from CPython ('(obj, /)'vs'($module, obj, /)') whileinspect.signature()matches exactly. Acceptable, or would you prefer fixing__self__? Settingzelfto the module would prepend it on every call and disable theCallLen/CallIsinstancespecializations that key onnative.zelf.is_none(), so I would rather file that separately.update-doc-dbonce the generator change lands? Contributors cannot dispatch it.Investigated and drafted by Claude; reviewed before posting.