PEP 667 -- Consistent views of namespaces

I really don’t see how that changes whether locals() is well defined.
Even if sys._getframe is not implemented, the semantics is clear.

It’s clear given CPython’s frame API semantics, hence it being fine for 667’s purposes (fixing the behavioural issues in CPython)

It isn’t enough for the goal of PEP 558, which is to replace the “mutating the result of locals() has undefined behaviour” text in the function’s written specification with the actual expected semantics (as 667 is implementing them) written in a way that makes sense to a reader that may not even know that the frame API exists.

This is getting off-topic for this thread though: whether 558 is Withdrawn in favour of 667 or revised to depend on 667 instead of being an alternative to it, the update reflects agreement that 667 is a desirable change. Since 667 isn’t proposing to document the updated locals() semantics, the question of exactly how to do so isn’t a topic the PEP needs to address.

This is just about updating the docs for locals(). Just propose appropriate words in a PR once the implementation has landed (or as part of the implementation). No separate PEP is needed, nor does PEP 667 have to specify the documentation.

1 Like

That seems like a good way forward to me, so I posted a DO-NOT-MERGE PR (with the proposed wording from PEP 558) that can have the merge blocking label removed once the PEP 667 implementation has been merged: locals() documentation update for PEP 667 by ncoghlan · Pull Request #118265 · python/cpython · GitHub

The Python Steering Council is happy to accept “PEP 667 - Consistent views of namespaces” - under the condition that the PEP text be updated to take out just the planned “to be removed in” dates for the newly deprecated APIs as part of making your PR to mark it Accepted. @markshannon @gaogaotiantian

We’ll leave those present even if deprecated or seemingly pointless for the sake of backwards compatibility and can reconsider starting their actual removal with an announcement and multi-release ticking clock at some far future date. We found no need to try and prescribe exactly when that’ll be within PEP 667.

Thanks C API WG for your review and input.
And thanks Mark and Tian and everyone who has worked on this (including Alyssa via the earlier 558!).

We believe this PEP leads us to a simpler view of namespaces that everyone wants.

4 Likes

Hello,

the implementation of PEP 667 is a part of Python 3.13.0b1.
I’ve attempted to rebuild all the Fedora Python packages with the new release and found that some of the projects attempt to copy f_locals. This, newly, fails as 'FrameLocalsProxy' object has no attribute 'copy'.
Before opening a bug report, I’d like to ask whether this is an expected behavior or a regression. It’s not clear to me from the PEP’s description.

Example bug reports to Fedora packages: murano, bioframe.

3 Likes

Hi Karolina,

Thanks for the bug report.

I consider this a regression that we can fix in beta 2. @gaogaotiantian can you work on this? From the linked bug reports it is now clear that people expect f_locals.copy() to return a plain dict.

In the meantime, Karonina, a workaround could be to use dict(frame.f_locals) instead of frame.f_locals.copy().

–Guido

Oh, it’s probably also worth clarifying this in the PEP, @gaogaotiantian .

I’m not sure a single report justifies that users expects f_locals.copy() to return a dict. We need to consider the fact that no one knows f_locals is a proxy now - everyone thinks it’s a dict, so of course they would expect f_locals.copy() to return a dict just like any other dict. However, once people start to realize it’s a write-through proxy, would they expect f_locals.copy() to return a snapshot dict, instead of a proxy anymore? I would highly doubt that. obj.copy() returning a very different object that has very different features compared to obj is not expected.

I think this is a transitional pain we should take, rather than a compromise we should make which would make things confusing when people understand the meaning of the new feature.

I disagree. Both reports used essentially the same idiom, clearly expecting a snapshot. I don’t want to incur the transitional pain, and the alternative interpretation of f_locals.copy() (make another proxy) seems pretty useless – the two proxies would behave exactly the same. So let’s make a snapshot – it’s both more useful and more backwards compatible.

Do we have any precedence where type(obj) != type(obj.copy())?

>>> type(MappingProxyType({}).copy())
<class 'dict'>

Okay if the copy of the mapping proxy is a dict then it makes sense.

It seems to me that if I think something is a dict and I want to copy it, the fact that it’s actually a write-through proxy doesn’t change my mind. If anything, it makes my desire stronger: part of the point of copying is that I can update the copy without writing through to the source.

And yes, more generally I would expect the semantics of a “copy” method on a view (write-through proxy, etc.) to be that I get a separate chunk of data; and it’s not a big deal if I get an instance of the type that was being emulated - since it will have both the expected interface (the view’s methods, plus others I may not care about) and semantics (modifying separate data). E.g. if memoryview.copy() existed (I am not proposing here to add it), I would expect either a bytes or bytearray matching the mutability of the underlying data, or else a memoryview on such a new bytes or bytearray.

Github issue: gh-118921: Add `copy()` method for `FrameLocalsProxy` by gaogaotiantian · Pull Request #118923 · python/cpython · GitHub

A PR is made to add the copy() method.

1 Like

Pytest has some non-critical (and kinda terrifying) testing code like this (paraphrasing):

backlocals = sys._getframe(1).f_locals
eval(..., backlocals, ...)

which started failing in Python 3.13 beta with “TypeError: globals must be a real dict; try eval(expr, {}, mapping)”.

I am changing this to backlocals = dict(sys._getframe(1).f_locals), just wanted to leave a note in case this was somehow not intended.

4 Likes

While the website hasn’t updated yet, the following is one of the notes that has been added to the What’s New in 3.13 porting guide:

Accessing FrameType.f_locals <frame.f_locals> in an optimized scope now returns a write-through proxy rather than a snapshot that gets updated at ill-specified times. If a snapshot is desired, it must be created explicitly with dict or the proxy’s .copy() method.

Anything that requires a real dict falls into that “snapshot is desired” category.

1 Like

Note that .copy() doesn’t work in beta1. It will work in beta2.

1 Like