https://github.com/python/cpython/commit/c72b435023772118defd96baee5e4cf3a69e4a34
commit: c72b435023772118defd96baee5e4cf3a69e4a34
branch: main
author: Furkan Onder <[email protected]>
committer: ambv <[email protected]>
date: 2026-10-21T16:40:38-07:00
summary:

gh-61448: Fix inspect.getargvalues fails if arg name is not bound to a value 
(#101318)

Co-authored-by: Muse Code (Meta Muse Spark)
Co-authored-by: Lukasz Langa <[email protected]>

files:
A Misc/NEWS.d/next/Library/2023-01-26-09-36-24.gh-issue-61448.SOAkvt.rst
M Doc/library/inspect.rst
M Lib/inspect.py
M Lib/test/test_inspect/test_inspect.py

diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 196479eb7550e2..29bdae28b1fe26 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -1347,6 +1347,11 @@ Classes and functions
    :func:`getargvalues`.  The format\* arguments are the corresponding optional
    formatting functions that are called to turn names and values into strings.
 
+   Arguments whose names are absent from *locals*, including ``*`` and ``**``
+   arguments, are displayed with a value of ``<deleted>``. A custom 
*formatvalue*
+   function receives a placeholder object whose :func:`repr` is ``<deleted>``
+   for these arguments.
+
    .. note::
       This function was inadvertently marked as deprecated in Python 3.5.
 
diff --git a/Lib/inspect.py b/Lib/inspect.py
index db9129d47db6ab..d06c8078ded870 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -1397,6 +1397,14 @@ def _formatannotation(annotation):
         return formatannotation(annotation, module)
     return _formatannotation
 
+# A placeholder with <deleted> as repr (using a string would have '<deleted>'
+# with quotes). This is used in formatargvalues function.
+class _Deleted:
+    def __repr__(self):
+        return '<deleted>'
+
+_deleted = _Deleted()
+del _Deleted
 
 def formatargvalues(args, varargs, varkw, locals,
                     formatarg=str,
@@ -1411,14 +1419,19 @@ def formatargvalues(args, varargs, varkw, locals,
     argument is an optional function to format the sequence of arguments."""
     def convert(name, locals=locals,
                 formatarg=formatarg, formatvalue=formatvalue):
-        return formatarg(name) + formatvalue(locals[name])
+        formatted_name = formatarg(name)
+        try:
+            value = locals[name]
+        except KeyError:
+            value = _deleted
+        return formatted_name + formatvalue(value)
     specs = []
     for i in range(len(args)):
         specs.append(convert(args[i]))
     if varargs:
-        specs.append(formatvarargs(varargs) + formatvalue(locals[varargs]))
+        specs.append(convert(varargs, formatarg=formatvarargs))
     if varkw:
-        specs.append(formatvarkw(varkw) + formatvalue(locals[varkw]))
+        specs.append(convert(varkw, formatarg=formatvarkw))
     return '(' + ', '.join(specs) + ')'
 
 def _missing_arguments(f_name, argnames, pos, values):
diff --git a/Lib/test/test_inspect/test_inspect.py 
b/Lib/test/test_inspect/test_inspect.py
index 5a3aa3e43fb2b2..c5581041f044b1 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -595,6 +595,90 @@ def test_frame(self):
         self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals),
                          '(x=11, y=14)')
 
+        def return_current_frame(x):
+            return inspect.currentframe()
+
+        self.assertEqual(inspect.formatargvalues(
+            *inspect.getargvalues(return_current_frame(5))), '(x=5)')
+        self.assertEqual(inspect.formatargvalues(
+            *inspect.getargvalues(return_current_frame(9))), '(x=9)')
+
+    # See issue-61448
+    def test_formatargvalues_deleted_args(self):
+        def fun(a, b, x, y):
+            del b, y
+            return inspect.currentframe()
+
+        args = inspect.getargvalues(fun(2, 4, 8, 16))
+        self.assertEqual(inspect.formatargvalues(*args),
+                         '(a=2, b=<deleted>, x=8, y=<deleted>)')
+
+    def test_formatargvalues_deleted_argument_kinds(self):
+        cases = (
+            ((), "(x=None, a=2, k=4, *args=(3,), **kwargs={'z': 5})"),
+            (('x',),
+             "(x=<deleted>, a=2, k=4, *args=(3,), **kwargs={'z': 5})"),
+            (('a',),
+             "(x=None, a=<deleted>, k=4, *args=(3,), **kwargs={'z': 5})"),
+            (('k',),
+             "(x=None, a=2, k=<deleted>, *args=(3,), **kwargs={'z': 5})"),
+            (('args',),
+             "(x=None, a=2, k=4, *args=<deleted>, **kwargs={'z': 5})"),
+            (('kwargs',),
+             '(x=None, a=2, k=4, *args=(3,), **kwargs=<deleted>)'),
+            (('x', 'a', 'k', 'args', 'kwargs'),
+             '(x=<deleted>, a=<deleted>, k=<deleted>, '
+             '*args=<deleted>, **kwargs=<deleted>)'),
+        )
+        for deleted, expected in cases:
+            with self.subTest(deleted=deleted):
+                def fun(x, /, a, *args, k, **kwargs):
+                    if 'x' in deleted:
+                        del x
+                    if 'a' in deleted:
+                        del a
+                    if 'k' in deleted:
+                        del k
+                    if 'args' in deleted:
+                        del args
+                    if 'kwargs' in deleted:
+                        del kwargs
+                    return inspect.currentframe()
+
+                frame = fun(None, 2, 3, k=4, z=5)
+                try:
+                    self.assertEqual(inspect.formatargvalues(
+                        *inspect.getargvalues(frame)), expected)
+                finally:
+                    frame.clear()
+
+    def test_formatargvalues_mapping_lookup(self):
+        class LookupDict(dict):
+            def __getitem__(self, name):
+                return super().__getitem__(name) + 1
+
+        for args, varargs, varkw, name in (
+            (['x'], None, None, 'x'),
+            ([], 'args', None, '*args'),
+            ([], None, 'kwargs', '**kwargs'),
+        ):
+            with self.subTest(name=name):
+                values = collections.defaultdict(lambda: 42)
+                self.assertEqual(inspect.formatargvalues(
+                    args, varargs, varkw, values), f'({name}=42)')
+                values = LookupDict({name.lstrip('*'): 5})
+                self.assertEqual(inspect.formatargvalues(
+                    args, varargs, varkw, values), f'({name}=6)')
+
+    def test_formatargvalues_deleted_custom_formatters(self):
+        self.assertEqual(inspect.formatargvalues(
+            ['x'], 'args', 'kwargs', {},
+            formatarg=lambda name: name.upper(),
+            formatvarargs=lambda name: '*' + name.upper(),
+            formatvarkw=lambda name: '**' + name.upper(),
+            formatvalue=lambda value: ':' + repr(value)),
+            '(X:<deleted>, *ARGS:<deleted>, **KWARGS:<deleted>)')
+
     def test_previous_frame(self):
         args, varargs, varkw, locals = inspect.getargvalues(mod.fr.f_back)
         self.assertEqual(args, ['a', 'b', 'c', 'd', 'e', 'f'])
diff --git 
a/Misc/NEWS.d/next/Library/2023-01-26-09-36-24.gh-issue-61448.SOAkvt.rst 
b/Misc/NEWS.d/next/Library/2023-01-26-09-36-24.gh-issue-61448.SOAkvt.rst
new file mode 100644
index 00000000000000..6ea99ba3310b7c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-01-26-09-36-24.gh-issue-61448.SOAkvt.rst
@@ -0,0 +1,3 @@
+Fix :func:`inspect.formatargvalues` raising :exc:`KeyError` for deleted
+arguments, including ``*args`` and ``**kwargs``, by displaying ``<deleted>``.
+Patch by Ezio Melotti and Furkan Onder.

_______________________________________________
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