https://github.com/python/cpython/commit/881cc06bac7d59a5bd8f4d49717e2e1d05fcc435
commit: 881cc06bac7d59a5bd8f4d49717e2e1d05fcc435
branch: 3.13
author: Łukasz Langa <[email protected]>
committer: ambv <[email protected]>
date: 2026-03-11T22:02:01+01:00
summary:

[3.13] gh-139933: correctly suggest attributes for classes with a custom 
`__dir__` (GH-139950) (GH-145827) (GH-145833)

(cherry picked from commit 4722202a1a81974089801e6173d269836b6a074f)
(cherry picked from commit 0a80015ac26d60bff54f57ce9f73ffc5386249b1)

Co-authored-by: Bénédikt Tran <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst
M Lib/test/test_traceback.py
M Lib/traceback.py

diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 915055fdc743f8..fec6750afa9d2f 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -4113,6 +4113,27 @@ def method(self, name):
         self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, 
'_luch')))
         self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, 
'bluch')))
 
+    def test_getattr_suggestions_with_custom___dir__(self):
+        class M(type):
+            def __dir__(cls):
+                return [None, "fox"]
+
+        class C0:
+            def __dir__(self):
+                return [..., "bluch"]
+
+        class C1(C0, metaclass=M):
+            pass
+
+        self.assertNotIn("'bluch'", self.get_suggestion(C0, "blach"))
+        self.assertIn("'bluch'", self.get_suggestion(C0(), "blach"))
+
+        self.assertIn("'fox'", self.get_suggestion(C1, "foo"))
+        self.assertNotIn("'fox'", self.get_suggestion(C1(), "foo"))
+
+        self.assertNotIn("'bluch'", self.get_suggestion(C1, "blach"))
+        self.assertIn("'bluch'", self.get_suggestion(C1(), "blach"))
+
     def test_getattr_suggestions_do_not_trigger_for_long_attributes(self):
         class A:
             blech = None
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 572a3177cb0e14..b412954bd53286 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -1484,17 +1484,23 @@ def _substitution_cost(ch_a, ch_b):
     return _MOVE_COST
 
 
+def _get_safe___dir__(obj):
+    # Use obj.__dir__() to avoid a TypeError when calling dir(obj).
+    # See gh-131001 and gh-139933.
+    try:
+        d = obj.__dir__()
+    except TypeError:  # when obj is a class
+        d = type(obj).__dir__(obj)
+    return sorted(x for x in d if isinstance(x, str))
+
+
 def _compute_suggestion_error(exc_value, tb, wrong_name):
     if wrong_name is None or not isinstance(wrong_name, str):
         return None
     if isinstance(exc_value, AttributeError):
         obj = exc_value.obj
         try:
-            try:
-                d = dir(obj)
-            except TypeError:  # Attributes are unsortable, e.g. int and str
-                d = list(obj.__class__.__dict__.keys()) + 
list(obj.__dict__.keys())
-            d = sorted([x for x in d if isinstance(x, str)])
+            d = _get_safe___dir__(obj)
             hide_underscored = (wrong_name[:1] != '_')
             if hide_underscored and tb is not None:
                 while tb.tb_next is not None:
@@ -1509,11 +1515,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
     elif isinstance(exc_value, ImportError):
         try:
             mod = __import__(exc_value.name)
-            try:
-                d = dir(mod)
-            except TypeError:  # Attributes are unsortable, e.g. int and str
-                d = list(mod.__dict__.keys())
-            d = sorted([x for x in d if isinstance(x, str)])
+            d = _get_safe___dir__(mod)
             if wrong_name[:1] != '_':
                 d = [x for x in d if x[:1] != '_']
         except Exception:
diff --git 
a/Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst 
b/Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst
new file mode 100644
index 00000000000000..d76f0873d77265
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-11-11-50-59.gh-issue-139933.05MHlx.rst
@@ -0,0 +1,3 @@
+Improve :exc:`AttributeError` suggestions for classes with a custom
+:meth:`~object.__dir__` method returning a list of unsortable values.
+Patch by Bénédikt Tran.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to