https://github.com/python/cpython/commit/b5bffb9b4ce8f4515df1e0474c8f439e9f580d65
commit: b5bffb9b4ce8f4515df1e0474c8f439e9f580d65
branch: main
author: Guilherme Leobas <[email protected]>
committer: encukou <[email protected]>
date: 2026-11-05T18:03:44+01:00
summary:
gh-156133: Add `PyUnstable_InterpreterFrame_GetLocal` (GH-156134)
Also, add :c:func:`PyUnstable_Code_GetLocalPlusNames` to get the matching
variable names.
files:
A Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst
M Doc/c-api/code.rst
M Doc/c-api/frame.rst
M Doc/whatsnew/3.16.rst
M Include/cpython/code.h
M Include/cpython/pyframe.h
M Lib/test/test_capi/test_misc.py
M Modules/_testinternalcapi.c
M Objects/frameobject.c
diff --git a/Doc/c-api/code.rst b/Doc/c-api/code.rst
index 6f0b7838d6cfed..48d9b83f437ff7 100644
--- a/Doc/c-api/code.rst
+++ b/Doc/c-api/code.rst
@@ -44,6 +44,14 @@ bound into a function.
The old name is deprecated, but will remain available until the
signature changes again.
+.. c:function:: PyObject* PyUnstable_Code_GetLocalPlusNames(PyCodeObject *co)
+
+ Return a :term:`strong reference` to a tuple of names of the local, cell and
+ free variables of a code object. The names are in the same order as the
+ values returned by :c:func:`PyUnstable_InterpreterFrame_GetLocal`.
+
+ .. versionadded:: next
+
.. c:function:: PyCodeObject* PyUnstable_Code_New(int argcount, int
kwonlyargcount, int nlocals, int stacksize, int flags, PyObject *code, PyObject
*consts, PyObject *names, PyObject *varnames, PyObject *freevars, PyObject
*cellvars, PyObject *filename, PyObject *name, PyObject *qualname, int
firstlineno, PyObject *linetable, PyObject *exceptiontable)
Return a new code object. If you need a dummy code object to create a
frame,
diff --git a/Doc/c-api/frame.rst b/Doc/c-api/frame.rst
index 9e15804c941ce5..ea684630627355 100644
--- a/Doc/c-api/frame.rst
+++ b/Doc/c-api/frame.rst
@@ -241,3 +241,21 @@ Unless using :pep:`523`, you will not need this.
Return the currently executing line number, or -1 if there is no line
number.
.. versionadded:: 3.12
+
+
+.. c:function:: int PyUnstable_InterpreterFrame_GetLocal(struct
_PyInterpreterFrame *frame, Py_ssize_t index, PyObject **result)
+
+ Retrieve the local variable at *index*.
+ To determine the index of a particular variable, use
+ :c:func:`PyUnstable_InterpreterFrame_GetCode` and
+ :c:func:`PyUnstable_Code_GetLocalPlusNames`.
+
+ Cell and free variables are unboxed to their contents. Free variables are
+ resolved from the function closure, so this also works on a frame that has
+ not started executing.
+
+ * On success, store a new :term:`strong reference` in *result* and return
``1``.
+ * If the slot is unset or hidden, store ``NULL`` in *result* and return
``0``.
+ * On error, raise an exception, store ``NULL`` in *result* and return
``-1``.
+
+ .. versionadded:: next
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 514ab2605e3716..ce622d37e3c3f4 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -1394,6 +1394,12 @@ New features
* Add the :ref:`PyUnicodeWriter API <pyunicodewriter>` to the limited C API.
(Contributed by Victor Stinner in :gh:`161172`.)
+* Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable
+ of an internal interpreter frame by its localsplus index, and
+ :c:func:`PyUnstable_Code_GetLocalPlusNames` to get the matching variable
+ names.
+ (Contributed by Guilherme Leobas in :gh:`156133`.)
+
Porting to Python 3.16
----------------------
diff --git a/Include/cpython/code.h b/Include/cpython/code.h
index 5238447070ee54..5ab7e6586ec8a8 100644
--- a/Include/cpython/code.h
+++ b/Include/cpython/code.h
@@ -177,6 +177,11 @@ static inline int
PyUnstable_Code_GetFirstFree(PyCodeObject *op) {
return op->co_nlocalsplus - op->co_nfreevars;
}
+static inline PyObject *PyUnstable_Code_GetLocalPlusNames(PyCodeObject *op) {
+ assert(PyCode_Check(op));
+ return Py_NewRef(op->co_localsplusnames);
+}
+
Py_DEPRECATED(3.13) static inline int PyCode_GetFirstFree(PyCodeObject *op) {
return PyUnstable_Code_GetFirstFree(op);
}
diff --git a/Include/cpython/pyframe.h b/Include/cpython/pyframe.h
index 24a947de1ede1e..88f074a9ac8d86 100644
--- a/Include/cpython/pyframe.h
+++ b/Include/cpython/pyframe.h
@@ -35,3 +35,8 @@ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct
_PyInterpreterFrame
/* Returns the currently executing line number, or -1 if there is no line
number.
* Does not raise an exception. */
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLine(struct _PyInterpreterFrame
*frame);
+
+/* Returns a new (strong) reference to the local variable at `index` in the
+ * frame's localsplus array. */
+PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLocal(
+ struct _PyInterpreterFrame *frame, Py_ssize_t index, PyObject **result);
diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py
index b9c6882444ab76..b8f78144a380da 100644
--- a/Lib/test/test_capi/test_misc.py
+++ b/Lib/test/test_capi/test_misc.py
@@ -2913,6 +2913,80 @@ def test_line(self):
firstline = self.func.__code__.co_firstlineno
self.assertEqual(line, firstline + 2)
+ # get_frame_locals() returns the caller frame's locals as a name -> value
+ # dict via PyUnstable_InterpreterFrame_GetLocal (one strong reference per
+ # localsplus index).
+ def helper_plain(self, a, b):
+ c = a + b
+ return _testinternalcapi.get_frame_locals()
+
+ def test_get_local_plain(self):
+ d = self.helper_plain(3, 4)
+ self.assertEqual(d, {'a': 3, 'b': 4, 'c': 7, 'self': self})
+
+ def test_get_local_cell(self):
+ # y is a cell variable of this frame because inner closes over it.
+ y = 100
+
+ def inner():
+ return y
+
+ d = _testinternalcapi.get_frame_locals()
+ self.assertEqual(d['y'], 100)
+ self.assertIs(d['inner'], inner)
+
+ def test_get_local_free(self):
+ # z is a free variable of inner, read from the closure.
+ z = 7
+
+ def inner():
+ _ = z
+ return _testinternalcapi.get_frame_locals()
+
+ d = inner()
+ self.assertEqual(d['z'], 7)
+
+ def test_get_local_index_out_of_range(self):
+ def f(index):
+ return _testinternalcapi.get_frame_local(index)
+
+ n = len(_testinternalcapi.code_get_localsplus_names(f.__code__))
+ for index in (-1, n, n + 1):
+ with self.subTest(index=index):
+ with self.assertRaises(IndexError):
+ f(index)
+
+ def test_get_local_unset(self):
+ def f():
+ if False:
+ unset = 1
+ names = _testinternalcapi.code_get_localsplus_names(f.__code__)
+ return _testinternalcapi.get_frame_local(names.index('unset'))
+
+ self.assertEqual(f(), (0, None))
+
+ def test_get_local_set(self):
+ x = 5
+ code = self.test_get_local_set.__code__
+ names = _testinternalcapi.code_get_localsplus_names(code)
+ index = names.index('x')
+ self.assertEqual(_testinternalcapi.get_frame_local(index), (1, 5))
+
+ def test_code_get_localsplus_names(self):
+ def outer(a, b):
+ c = a
+ def inner():
+ return a, d
+ d = b
+ return inner
+
+ names = _testinternalcapi.code_get_localsplus_names(outer.__code__)
+ self.assertIsInstance(names, tuple)
+ self.assertCountEqual(names, ('a', 'b', 'c', 'd', 'inner'))
+ inner = outer(1, 2)
+ names = _testinternalcapi.code_get_localsplus_names(inner.__code__)
+ self.assertCountEqual(names, ('a', 'd'))
+
SUFFICIENT_TO_DEOPT_AND_SPECIALIZE = 100
diff --git
a/Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst
b/Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst
new file mode 100644
index 00000000000000..be780cba1214d3
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst
@@ -0,0 +1,3 @@
+Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable of
+an internal interpreter frame by its localsplus index, and
+:c:func:`PyUnstable_Code_GetLocalPlusNames` to get the matching variable names.
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 3d0058fc11c452..3efb70f662698b 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -1603,6 +1603,80 @@ iframe_getlasti(PyObject *self, PyObject *frame)
return PyLong_FromLong(PyUnstable_InterpreterFrame_GetLasti(f));
}
+// Reads the locals of the Python frame that called this C function using
+// PyUnstable_InterpreterFrame_GetLocal and returns them as a name -> value
+// dict, skipping NULL (unset or hidden) slots.
+static PyObject *
+get_frame_locals(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+ PyThreadState *tstate = _PyThreadState_GET();
+ _PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate);
+ assert(frame != NULL); // there must be a caller frame
+ PyCodeObject *co = _PyFrame_GetCode(frame);
+ Py_ssize_t n = co->co_nlocalsplus;
+ PyObject *dict = PyDict_New();
+ if (dict == NULL) {
+ return NULL;
+ }
+ PyObject *names = PyUnstable_Code_GetLocalPlusNames(co);
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyObject *value;
+ int rc = PyUnstable_InterpreterFrame_GetLocal(frame, i, &value);
+ if (rc < 0) {
+ Py_DECREF(names);
+ Py_DECREF(dict);
+ assert(value == NULL);
+ return NULL;
+ }
+ if (rc == 0) {
+ assert(value == NULL);
+ continue; // unset or hidden slot
+ }
+ PyObject *name = PyTuple_GET_ITEM(names, i);
+ int err = PyDict_SetItem(dict, name, value);
+ Py_DECREF(value);
+ if (err < 0) {
+ Py_DECREF(names);
+ Py_DECREF(dict);
+ return NULL;
+ }
+ }
+ Py_DECREF(names);
+ return dict;
+}
+
+// Calls PyUnstable_InterpreterFrame_GetLocal on the caller's frame for a
single
+// index and returns (rc, value), or raises if rc is -1.
+static PyObject *
+get_frame_local(PyObject *self, PyObject *arg)
+{
+ Py_ssize_t index = PyLong_AsSsize_t(arg);
+ if (index == -1 && PyErr_Occurred()) {
+ return NULL;
+ }
+ PyThreadState *tstate = _PyThreadState_GET();
+ _PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate);
+ assert(frame != NULL); // there must be a caller frame
+ PyObject *value;
+ int rc = PyUnstable_InterpreterFrame_GetLocal(frame, index, &value);
+ if (rc < 0) {
+ assert(value == NULL);
+ return NULL;
+ }
+ if (rc == 0) {
+ assert(value == NULL);
+ return Py_BuildValue("iO", rc, Py_None);
+ }
+ return Py_BuildValue("iN", rc, value);
+}
+
+static PyObject *
+code_get_localsplus_names(PyObject *self, PyObject *arg)
+{
+ assert(PyCode_Check(arg));
+ return PyUnstable_Code_GetLocalPlusNames((PyCodeObject *)arg);
+}
+
static PyObject *
code_returns_only_none(PyObject *self, PyObject *arg)
{
@@ -3434,6 +3508,9 @@ static PyMethodDef module_functions[] = {
{"iframe_getcode", iframe_getcode, METH_O, NULL},
{"iframe_getline", iframe_getline, METH_O, NULL},
{"iframe_getlasti", iframe_getlasti, METH_O, NULL},
+ {"get_frame_locals", get_frame_locals, METH_NOARGS, NULL},
+ {"get_frame_local", get_frame_local, METH_O, NULL},
+ {"code_get_localsplus_names", code_get_localsplus_names, METH_O, NULL},
{"code_returns_only_none", code_returns_only_none, METH_O, NULL},
{"get_co_framesize", get_co_framesize, METH_O, NULL},
{"get_co_localskinds", get_co_localskinds, METH_O, NULL},
diff --git a/Objects/frameobject.c b/Objects/frameobject.c
index 61be2beca22d15..475be76dc3da21 100644
--- a/Objects/frameobject.c
+++ b/Objects/frameobject.c
@@ -2327,6 +2327,53 @@ frame_get_var(_PyInterpreterFrame *frame, PyCodeObject
*co, int i,
}
+int
+PyUnstable_InterpreterFrame_GetLocal(_PyInterpreterFrame *frame,
+ Py_ssize_t index, PyObject **result)
+{
+ PyCodeObject *co = _PyFrame_GetCode(frame);
+ *result = NULL;
+ if (index < 0 || index >= co->co_nlocalsplus) {
+ PyErr_Format(
+ PyExc_IndexError,
+ "PyUnstable_InterpreterFrame_GetLocal: index %zd out of range [0,
%d)",
+ index, co->co_nlocalsplus);
+ return -1;
+ }
+
+ int offset = PyUnstable_Code_GetFirstFree(co); // co_nlocalsplus -
co_nfreevars
+ if (index < offset) {
+ // Local or cell variable. frame_get_var unboxes cells and copes with
+ // not-yet-started frames and arguments not yet promoted by MAKE_CELL.
+ if (_PyLocals_GetKind(co->co_localspluskinds, (int)index) &
CO_FAST_HIDDEN) {
+ return 0;
+ }
+ PyObject *value = NULL;
+ frame_get_var(frame, co, (int)index, &value);
+ if (value == NULL) {
+ return 0;
+ }
+ *result = value; // strong reference
+ return 1;
+ }
+
+ // Free variable: read from the function closure rather than localsplus.
+ if ((co->co_flags & CO_OPTIMIZED)
+ && PyStackRef_FunctionCheck(frame->f_funcobj))
+ {
+ PyFunctionObject *func = _PyFrame_GetFunction(frame);
+ PyObject *cell = PyTuple_GET_ITEM(func->func_closure, index - offset);
+ PyObject *value = PyCell_GetRef((PyCellObject *)cell);
+ if (value == NULL) {
+ return 0;
+ }
+ *result = value;
+ return 1;
+ }
+ return 0;
+}
+
+
bool
_PyFrame_HasHiddenLocals(_PyInterpreterFrame *frame)
{
_______________________________________________
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]