Suggest similar member names on AttributeError for .NET objects#124
Merged
jhonabreul merged 5 commits intoJul 1, 2026
Merged
Conversation
When accessing a missing attribute on a .NET object from Python, enrich the resulting AttributeError message with a "Did you mean ...?" hint listing similarly-named members of the managed type. The work is done inside tp_getattro where the object and attribute name are available directly, so it does not depend on the AttributeError .name/.obj attributes (Python 3.10+) and works across all supported Python versions (3.7-3.11). It only runs on the exceptional miss path, keeping normal attribute access untouched, and always re-raises an AttributeError so hasattr()/getattr(default) keep working. - ClassObject gets a tp_getattro that delegates to PyObject_GenericGetAttr and appends suggestions on a miss (inherited by EnumObject, LookUpObject, ExceptionClassObject, ClassDerivedObject). - DynamicClassObject appends suggestions on its RuntimeBinder miss path. - Shared helpers live in ClassBase (Levenshtein-based ranking, dunder names skipped, original CPython message preserved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Martin-Molinero
approved these changes
Jun 30, 2026
The fork exposes .NET members under PEP8-style snake_case aliases, so the "Did you mean ...?" suggestions now use that form (e.g. 'length' instead of 'Length'). Conversion reuses the existing ToSnakeCase helpers, so const and static-readonly members are rendered UPPER_CASE to match how they are exposed to Python. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Style cleanup: prefer var over explicit types where the type is apparent from the right-hand side. Also drops two now-unnecessary null-forgiving operators that the compiler's flow analysis already proves non-null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the suggestion logic for regular reflected types off the hot attribute-access path. Previously ClassObject overrode tp_getattro (__getattribute__), so every successful attribute access paid a managed round-trip (~17 ns/access measured). Instead, install a shared __getattr__ on each reflected type, which CPython only invokes on a miss via slot_tp_getattr_hook; hits go straight through the native generic getattr with no managed transition. pythonnet's metatype does not run CPython's slot-fixup when attributes are set on a type, so AttributeErrorHint wires tp_getattro to the hook manually (the hook address is read from a probe class). Only types still using the native generic getattr are redirected, so dynamic objects, modules and interfaces (which have their own tp_getattro) are untouched; DynamicClassObject keeps enriching on its own miss path. Benchmark (System.Version, best-of-N, ns/access): hit path: ~109 ns (baseline ~105; tp_getattro was ~122) miss path: ~11 us (rare; reflection + Levenshtein, as before) The hot-path overhead is effectively eliminated; the extra miss-path cost only applies when an attribute is actually absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 tasks
Martin-Molinero
approved these changes
Jul 1, 2026
This was referenced Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When Python code accesses a missing attribute on a .NET object, the resulting
AttributeErrornow includes aDid you mean ...?hint listing similarly-named members of the managed type, in snake_case (matching the fork's PEP8-style API surface):The suggestion is computed from the .NET type's members and only appears when an attribute is actually missing.
Design — miss-only
__getattr__hook (zero hot-path cost)The enrichment runs through a miss-only
__getattr__hook, so there is no cost on the common, successful attribute-access path:__getattr__after the normal lookup fails (via the nativeslot_tp_getattr_hook). On a hit it calls the generic getattr directly with no managed transition; only on a miss does it call our handler.AttributeErrorHintwirestp_getattroto the hook manually (the hook address is read from a probe class at startup). Only types still using the native generic getattr are redirected — dynamic objects, modules and interfaces (which have their owntp_getattro) are left untouched, andDynamicClassObjectenriches on its own miss path.ToSnakeCasehelpers so const/static-readonly members render asUPPER_CASE.AttributeError.name/.objattributes that only exist in 3.10+.Performance
Measured on
System.Versionproperty access, best-of-N:An earlier
tp_getattro(__getattribute__) implementation was also benchmarked; it added a consistent ~17–19 ns per attribute access on every access. A property-access-dominated Lean algorithm (~117M C# property gets) reproduced this at ~19 ns/access (~9% on that pathological loop). Across realistic Lean algorithms and a 50-algorithm regression subset, that overhead was within run-to-run noise (< 0.1% aggregate). The miss-only hook in this PR removes even that per-access cost, keeping the hit path at native speed.Changes
AttributeErrorHint.cs— sets up and installs the shared miss-only__getattr__hook on reflected types.ClassBase.cs— builds the enriched message / snake_case member suggestions (Levenshtein ranking, dunder skip); shared by the hook and byDynamicClassObject.TypeManager.cs/PythonEngine.cs— install the hook per reflected type; initialize/teardown.DynamicClassObject.cs— enriches its own attribute-miss path.tests/test_class.pyandsrc/embed_tests/TestPropertyAccess.cs.🤖 Generated with Claude Code