Skip to content

Commit 933fbf8

Browse files
authored
PEP 558: Note compatibility constraints on locals(), other updates (#1069)
- new design discussion section to cover the requirement that the semantics of locals() itself at function scope be left alone - propose a C level API that exactly matches Python level frame.f_locals semantics - other minor text formatting and wording updates
1 parent 44f2986 commit 933fbf8

File tree

1 file changed

+68
-15
lines changed

1 file changed

+68
-15
lines changed

pep-0558.rst

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,11 @@ may not affect the values of local and free variables used by the interpreter."
206206
This PEP proposes to change that text to instead say:
207207

208208
At function scope (including for generators and coroutines), [this function]
209-
returns a
210-
dynamic snapshot of the function's local variables and any nonlocal cell
211-
references. In this case, changes made via the snapshot are *not* written
212-
back to the corresponding local variables or nonlocal cell references, and
213-
any such changes to the snapshot will be overwritten if the snapshot is
214-
subsequently refreshed (e.g. by another call to ``locals()``).
209+
returns a dynamic snapshot of the function's local variables and any
210+
nonlocal cell references. In this case, changes made via the snapshot are
211+
*not* written back to the corresponding local variables or nonlocal cell
212+
references, and any such changes to the snapshot will be overwritten if the
213+
snapshot is subsequently refreshed (e.g. by another call to ``locals()``).
215214

216215
CPython implementation detail: the dynamic snapshot for the currently
217216
executing frame will be implicitly refreshed before each call to the trace
@@ -281,13 +280,15 @@ return a reference to the dynamic snapshot rather than to the write-through
281280
proxy.
282281

283282
At the C API layer, ``PyEval_GetLocals()`` will implement the same semantics
284-
as the Python level ``locals()`` builtin, and a new ``PyFrame_GetLocals(frame)``
285-
accessor API will be provided to allow the proxy bypass logic to be encapsulated
286-
entirely inside the frame implementation. The C level equivalent of accessing
287-
``pyframe.f_locals`` in Python will be to access ``cframe->f_locals`` directly
288-
(the one difference is that accessing ``pyframe.f_locals`` will continue to
289-
implicitly refresh the dynamic snapshot, whereas C code will need to explicitly
290-
call ``PyFrame_GetLocals(frame)`` to refresh the snapshot).
283+
as the Python level ``locals()`` builtin, and a new
284+
``PyFrame_GetPyLocals(frame)`` accessor API will be provided to allow the
285+
function level proxy bypass logic to be encapsulated entirely inside the frame
286+
implementation.
287+
288+
The C level equivalent of accessing ``pyframe.f_locals`` in Python will be a
289+
new ``PyFrame_GetLocalsAttr(frame)`` API. Like the Python level descriptor, the
290+
new API will implicitly refresh the dynamic snapshot at function scope before
291+
returning a reference to the write-through proxy.
291292

292293
The ``PyFrame_LocalsToFast()`` function will be changed to always emit
293294
``RuntimeError``, explaining that it is no longer a supported operation, and
@@ -324,7 +325,59 @@ The proposal in this PEP aims to retain the first two properties (to maintain
324325
backwards compatibility with as much code as possible) while ensuring that
325326
simply installing a trace hook can't enable rebinding of function locals via
326327
the ``locals()`` builtin (whereas enabling rebinding via
327-
``inspect.currentframe().f_locals`` is fully intended).
328+
``frame.f_locals`` inside the tracehook implementation is fully intended).
329+
330+
331+
Keeping ``locals()`` as a dynamic snapshot at function scope
332+
------------------------------------------------------------
333+
334+
It would theoretically be possible to change the semantics of the ``locals()``
335+
builtin to return the write-through proxy at function scope, rather than
336+
continuing to return a dynamic snapshot.
337+
338+
This PEP doesn't (and won't) propose this as it's a backwards incompatible
339+
change in practice, even though code that relies on the current behaviour is
340+
technically operating in an undefined area of the language specification.
341+
342+
Consider the following code snippet::
343+
344+
def example():
345+
x = 1
346+
locals()["x"] = 2
347+
print(x)
348+
349+
Even with a trace hook installed, that function will consistently print ``1``
350+
on the current reference interpreter implementation::
351+
352+
>>> example()
353+
1
354+
>>> import sys
355+
>>> def basic_hook(*args):
356+
... return basic_hook
357+
...
358+
>>> sys.settrace(basic_hook)
359+
>>> example()
360+
1
361+
362+
Similarly, ``locals()`` can be passed to the ``exec()`` and ``eval()`` builtins
363+
at function scope without risking unexpected rebinding of local variables.
364+
365+
Provoking the reference interpreter into incorrectly mutating the local variable
366+
state requires a more complex setup where a nested function closes over a
367+
variable being rebound in the outer function, and due to the use of either
368+
threads, generators, or coroutines, it's possible for a trace function to start
369+
running for the nested function before the rebinding operation in the outer
370+
function, but finish running after the rebinding operation has taken place (in
371+
which case the rebinding will be reverted, which is the bug reported in [1]_).
372+
373+
In addition to preserving the de facto semantics which have been in place since
374+
PEP 227 introduced nested scopes in Python 2.1, the other benefit of restricting
375+
the write-through proxy support to the implementation-defined frame object API
376+
is that it means that only interpreter implementations which emulate the full
377+
frame API need to offer the write-through capability at all, and that
378+
JIT-compiled implementations only need to enable it when a frame introspection
379+
API is invoked, or a trace hook is installed, not whenever ``locals()`` is
380+
accessed at function scope.
328381

329382

330383
What happens with the default args for ``eval()`` and ``exec()``?
@@ -333,7 +386,7 @@ What happens with the default args for ``eval()`` and ``exec()``?
333386
These are formally defined as inheriting ``globals()`` and ``locals()`` from
334387
the calling scope by default.
335388

336-
There doesn't seem to be any reason for the PEP to change this.
389+
There isn't any need for the PEP to change these defaults, so it doesn't.
337390

338391

339392
Changing the frame API semantics in regular operation

0 commit comments

Comments
 (0)