Skip to content

gh-156133: Add PyUnstable_InterpreterFrame_GetLocal - #156134

Open
guilhermeleobas wants to merge 8 commits into
python:mainfrom
guilhermeleobas:guilhermeleobas/frame-getlocals
Open

guilhermeleobas wants to merge 8 commits into
python:mainfrom
guilhermeleobas:guilhermeleobas/frame-getlocals

Conversation

@guilhermeleobas

@guilhermeleobas guilhermeleobas commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Add an unstable C API that reads a frame's local variables into a caller-provided array indexed by localsplus index, with cell and free variables unboxed to their contents. Free variables are resolved from the function closure, so the API also works on a frame that has not started executing (before COPY_FREE_VARS runs), which is the case that motivated it.

Includes the three PEP 689 deliverables:

  1. reference documentation in Doc/c-api/frame.rst
  2. a What's New entry for 3.16
  3. and tests (a C wrapper in Modules/_testinternalcapi.c driven by Lib/test/test_capi/test_frame_getlocals.py covering plain locals, a cell variable, and a free variable).

Authored with the assistance of an AI coding agent (Claude Opus)

@python-cla-bot

python-cla-bot Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@read-the-docs-community

read-the-docs-community Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Comment thread Objects/frameobject.c Outdated
Comment thread Modules/_testinternalcapi.c Outdated
@aisk aisk changed the title gh-156133: Add PyUnstable_InterpreterFrame_GetLocals gh-156133: Add PyUnstable_InterpreterFrame_GetLocal Aug 21, 2026
Comment thread Objects/frameobject.c Outdated
Add an unstable C API that returns a strong reference to a single local
variable of an internal interpreter frame, addressed by its localsplus index,
with cell and free variables unboxed to their contents. Free variables are
resolved from the function closure, so the API also works on a frame that has
not started executing (before COPY_FREE_VARS runs) -- the case that motivated
it -- and it does not modify the frame.

Includes the PEP 689 deliverables: reference documentation in
Doc/c-api/frame.rst, a What's New entry for 3.16, a Misc/NEWS.d blurb, and
tests in Lib/test/test_capi/test_misc.py (TestInternalFrameApi) covering plain
locals, a cell variable, and a free variable.

Authored with the assistance of an AI coding agent (Claude Opus)
@guilhermeleobas
guilhermeleobas force-pushed the guilhermeleobas/frame-getlocals branch from cbb346a to d29acd6 Compare August 24, 2026 13:52
@guilhermeleobas

Copy link
Copy Markdown
Contributor Author

Thanks @aisk. I've addressed your comments.

@aisk

aisk commented Aug 24, 2026

Copy link
Copy Markdown
Member

Hi, thank you for the contribution, but please avoid using force push in the future, see: https://devguide.python.org/getting-started/pull-request-lifecycle/#don-t-force-push

@guilhermeleobas

guilhermeleobas commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor Author

https://devguide.python.org/getting-started/pull-request-lifecycle/#don-t-force-push

Sorry, it won't happen again. I did because the e-mail used in the commit was wrong and the cla-bot was failing.

@guilhermeleobas
guilhermeleobas requested a review from aisk September 7, 2026 20:13
@guilhermeleobas

Copy link
Copy Markdown
Contributor Author

@aisk could you take a look at this PR again once you have some cycles to spare?

Comment thread Doc/c-api/frame.rst Outdated
.. versionadded:: 3.12


.. c:function:: PyObject* PyUnstable_InterpreterFrame_GetLocal(struct _PyInterpreterFrame *frame, Py_ssize_t index)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we should add an unstable API for the _PyInterpreterFrame struct too, right?

@guilhermeleobas guilhermeleobas Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the discuss topic, Petr said this can be done in a follow-up PR. I can add it here if you think it is necessary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll defer to him :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@encukou could you take a look at this PR once you have a chance?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will, after the 3.15.0 release.

Comment thread Doc/c-api/frame.rst Outdated
Comment thread Doc/c-api/frame.rst Outdated
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Oct 1, 2026
Comment thread Doc/c-api/frame.rst
Comment thread Lib/test/test_capi/test_misc.py

@encukou encukou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I have a bunch of small nitpicks, hopehully the last batch.

Comment on lines +2885 to +2887

with self.assertRaises(TypeError):
_testinternalcapi.code_get_localsplus_names(None)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only exercises the test helper.

Suggested change
with self.assertRaises(TypeError):
_testinternalcapi.code_get_localsplus_names(None)

Comment on lines +1612 to +1615
if (!PyCode_Check(arg)) {
PyErr_SetString(PyExc_TypeError, "argument must be a code object");
return NULL;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!PyCode_Check(arg)) {
PyErr_SetString(PyExc_TypeError, "argument must be a code object");
return NULL;
}
assert(PyCode_Check(arg));

Comment on lines +2812 to +2815
self.assertEqual(d['a'], 3)
self.assertEqual(d['b'], 4)
self.assertEqual(d['c'], 7)
self.assertIs(d['self'], self)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can test the whole dict at once.

Suggested change
self.assertEqual(d['a'], 3)
self.assertEqual(d['b'], 4)
self.assertEqual(d['c'], 7)
self.assertIs(d['self'], self)
self.assertEqual(d, {'a': 3, 'b': 4, 'c': 7, 'self': self})

Comment on lines +2843 to +2847
# f has no cell or free variables, so co_nlocalsplus == co_nlocals.
code = f.__code__
self.assertFalse(code.co_cellvars or code.co_freevars)
nlocalsplus = code.co_nlocals
for index in (-1, nlocalsplus, nlocalsplus + 1):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simpler now.

Suggested change
# f has no cell or free variables, so co_nlocalsplus == co_nlocals.
code = f.__code__
self.assertFalse(code.co_cellvars or code.co_freevars)
nlocalsplus = code.co_nlocals
for index in (-1, nlocalsplus, nlocalsplus + 1):
n = len(_testinternalcapi.code_get_localsplus_names(f.__code__))
for index in (-1, n, n + 1):

def f():
if False:
unset = 1
names = f.__code__.co_varnames

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer using the matching API

Suggested change
names = f.__code__.co_varnames
names = _testinternalcapi.code_get_localsplus_names(f.__code__)

Comment on lines +1546 to +1549
if (frame == NULL) {
PyErr_SetString(PyExc_RuntimeError, "no caller frame");
return NULL;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test functions can assert when they're used wrong.

Suggested change
if (frame == NULL) {
PyErr_SetString(PyExc_RuntimeError, "no caller frame");
return NULL;
}
assert(frame != NULL); // there must be a caller frame

return NULL;
}
if (rc == 0) {
continue; // unset or hidden slot

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
continue; // unset or hidden slot
assert(value == NULL);
continue; // unset or hidden slot

if (rc < 0) {
Py_DECREF(names);
Py_DECREF(dict);
return NULL;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return NULL;
assert(value == NULL);
return NULL;

Comment thread Doc/c-api/code.rst
Comment on lines +49 to +52
Return a new :term:`strong reference` to the tuple of names of the local,
cell and free variables of a code object. The tuple is indexed like the
*localsplus* array of a frame, so it can be used together with
:c:func:`PyUnstable_InterpreterFrame_GetLocal`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Return a new :term:`strong reference` to the tuple of names of the local,
cell and free variables of a code object. The tuple is indexed like the
*localsplus* array of a frame, so it can be used together with
:c:func:`PyUnstable_InterpreterFrame_GetLocal`.
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`.

Comment thread Doc/c-api/frame.rst
Comment on lines +250 to +253
Retrieve the local variable at *index* in the frame's localsplus array, with
cell and free variables unboxed to their contents. Free variables are
resolved from the function closure, so this also works on a frame that has
not started executing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Retrieve the local variable at *index* in the frame's localsplus array, with
cell and free variables unboxed to their contents. Free variables are
resolved from the function closure, so this also works on a frame that has
not started executing.
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants