-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PEP 667: Clarify specification ambiguities and the expected impact #3845
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good!
I think it might also make sense to add a "Rejected Alternatives" section, with a sub-section "Maintaining existing behavior of exec", discussing some of what we talked about in Discourse about the problems with trying to do this.
The new section looks good to me. FWIW, What's New in 3.13 does discuss this aspect of the change (https://docs.python.org/dev/whatsnew/3.13.html#defined-mutation-semantics-for-locals), but having more details in the PEP definitely won't hurt. |
It may also be worth referencing back to the PEP 558 discussions of the impact of changing the way
And while PEP 667's text didn't explicitly mention the impact on This is a case where the use of competing PEPs may have let us down a bit - PEP 667 (quite reasonably) didn't bother repeating the rationale for the aspects where it agreed with the design decisions in PEP 558, so reading it in isolation from PEP 558 doesn't quite give the full picture of the change. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So should I post this on disclosure first? |
peps/pep-0667.rst
Outdated
Impact on ``exec()`` | ||
==================== | ||
|
||
Even though this PEP does not modify ``exec()`` directly, the semantic change |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the first pass at trying to document this behavior. I would suggest reframing this a bit to:
- Call out the specific semantic change to
locals()
to give the reader context - Explain concisely how this PEP impacts or explicitly documents why a behavior change may be seen when using
exec()
with performance optimizedlocals().
- Make clear the expected behavior before explaining the examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the semantic changes to locals()
, it's mentioned in this PEP a bit earlier. We should assume the reader already reads that part right? How should I either refer back or summarize that?
The reason of the behavior change is basically
The core difference is that the current implementation returns the same dict
whenlocals()
is called multiple times in function scope, so the following
code magically works
About the expected behavior, do you mean something like "the local variable update will not be propagated between exec()
"? I feel difficult to describe the exact behavior otherwise I would probably just summarize it in words instead of examples. This is a rather subtle change and the original behavior is kind of odd. I would love any help from people who are better at documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @gaogaotiantian. We can definitely iterate on this. Looking at @carljm's message, I think there are three concepts that the reader may confuse: locals()
, locals
argument, and frame.f_locals
for functions such as exec()
.
I'm not sure what the best wording would be but I think we should iterate until it seems clear to folks. Mostly, I want to make sure that someone reading the PEP and using exec() has a clear idea of what the expected behavior should be.
P.S. I'm very happy that you are taking the time to make this PEP addition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@willingc I feel that these concepts are pretty well communicated by the current draft, with my suggested changes incorporated. First the current behavior of exec()
is explained (with an example), then the reason for that behavior (and the connection to locals()
is explained, then some additional confusing implications of the current behavior are shown (with an example), and then the new behavior is explained (which doesn't take very long, since it's much simpler), and then examples are given to demonstrate how to accomplish some things that are currently possible, under the new behavior. Are there particular aspects of this presentation that you don't find sufficiently clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @carljm. Let's accept your suggestions, and I will give it another read. I'm primarily concerned that the expected behavior for 3.13+ is clear to readers. That may be so after your changes are included. I have a hard time following the wording flow until I see it as one prose document.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After my Discord comments about even PEP 558 not explaining the locals()
changes particularly well, I went back to PEP 558, the related python-dev thread, and even PEP 3100 to find the material that actually went into the decision to make locals()
and exec()
work as PEP 558 and PEP 667 both propose.
The early iterations of PEP 558 actually tried to standardise locals()
without changing it, so hopefully the new text makes it clear why that approach didn't last long :)
Yes, this should be posted on Discourse to get community feedback, but it probably makes sense to address the existing feedback first? |
Co-authored-by: Carl Meyer <[email protected]>
Of course, just want to make sure about the procedure. I'm a bit swamped recently. |
I ticked the SC approval box in the template, since we have that in python/steering-council#245 (comment) |
I could be misunderstanding, but I don't see an indication in that comment that the SC considered the issue of |
peps/pep-0667.rst
Outdated
Impact on ``exec()`` | ||
==================== | ||
|
||
Even though this PEP does not modify ``exec()`` directly, the semantic change |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@willingc I feel that these concepts are pretty well communicated by the current draft, with my suggested changes incorporated. First the current behavior of exec()
is explained (with an example), then the reason for that behavior (and the connection to locals()
is explained, then some additional confusing implications of the current behavior are shown (with an example), and then the new behavior is explained (which doesn't take very long, since it's much simpler), and then examples are given to demonstrate how to accomplish some things that are currently possible, under the new behavior. Are there particular aspects of this presentation that you don't find sufficiently clear?
Co-authored-by: Carl Meyer <[email protected]>
@gaogaotiantian I accepted @carljm's suggestions so that I could look at the prose as it would display to a reader. I am going to add a few suggestions re: grammar, and then I believe this will be acceptable from my viewpoint. Thanks so much for doing this. It adds important clarity for all. 💐 |
peps/pep-0667.rst
Outdated
|
||
To work with a copy of ``locals()`` on all versions without making | ||
redundant copies on Python 3.13+, a helper function will need to | ||
be defined in the affected code:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And by "affected code", do you mean "optimized scopes"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded to hopefully be clearer
peps/pep-0667.rst
Outdated
|
||
This was historically also equivalent to using the calling frame's | ||
``frame.f_globals`` and ``frame.f_locals`` attributes, but this PEP maps | ||
them to ``globals()`` and ``locals()`` in order to preserve the property |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"them" is ambiguous. Does it refer to frame.f_globals
and frame.f_locals
or the arguments to exec()
and eval()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded to remove the ambiguity
peps/pep-0667.rst
Outdated
``exec()`` refreshes the cached dict with the actual values on the frame. | ||
This means that, unlike the "fake" locals created by writing back to ``locals()`` | ||
(including via previous calls to ``exec()``), the real locals known by the | ||
compiler can't easily be modified by ``exec()``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"easily" or at all? What would "difficult" code look like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a reference back to the ctypes
code that's part of the Rationale section above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gaogaotiantian Thank you so much for this PEP update. To me, it really clarifies all the (known!) ambiguities that have been discovered, especially around exec()
and eval()
. Really fantastic work on this, and thanks also to everyone who has contributed comments in this PR.
I left a number of little one-off comments and suggestions. I don't think any of them are blockers, but to me would help add just a little more clarity in a few places. Please consider them.
In my role as a member of the SC, I'm approving these changes. Before merging, please wait for at least a few other SC members to weigh in. I will ping them privately and we'll reach resolution asap.
peps/pep-0667.rst
Outdated
each other. Writes to the ``f_locals`` frame atribute may not show up as | ||
modifications to local variables if ``PyFrame_LocalsToFast()`` is never | ||
called. Writes to local variables can get lost if the dictionary snapshot | ||
is not refreshed before being written back to the frame. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was a bit confused here. I thought writes to local variables get lost if the snapshot is refreshed before being written back to the frame. So if you do exec("x = 1")
then locals()
(to refresh the snapshot), your x=1
is lost?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded to hopefully be clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest update should cover Barry & Tian's review comments (I'll go through and resolve the conversions I'm 100% sure are addressed, but there are a few more ambiguous ones that I'll leave unresolved until they've had a chance to look at the updated wording)
peps/pep-0667.rst
Outdated
each other. Writes to the ``f_locals`` frame atribute may not show up as | ||
modifications to local variables if ``PyFrame_LocalsToFast()`` is never | ||
called. Writes to local variables can get lost if the dictionary snapshot | ||
is not refreshed before being written back to the frame. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded to hopefully be clearer.
peps/pep-0667.rst
Outdated
|
||
To work with a copy of ``locals()`` on all versions without making | ||
redundant copies on Python 3.13+, a helper function will need to | ||
be defined in the affected code:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded to hopefully be clearer
peps/pep-0667.rst
Outdated
|
||
This was historically also equivalent to using the calling frame's | ||
``frame.f_globals`` and ``frame.f_locals`` attributes, but this PEP maps | ||
them to ``globals()`` and ``locals()`` in order to preserve the property |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded to remove the ambiguity
peps/pep-0667.rst
Outdated
``exec()`` refreshes the cached dict with the actual values on the frame. | ||
This means that, unlike the "fake" locals created by writing back to ``locals()`` | ||
(including via previous calls to ``exec()``), the real locals known by the | ||
compiler can't easily be modified by ``exec()``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a reference back to the ctypes
code that's part of the Rationale section above.
This PR mores than doubles the size of PEP 667. That seems excessive. The impact on """
Similarly, for All the extra history and exposition only serves to obscure the semantics, IMO. Fleshing the out the rationale a bit seems reasonable, but not too much. |
peps/pep-0667.rst
Outdated
|
||
``frame.f_locals`` will return a view object on the frame that | ||
implements the ``collections.abc.Mapping`` interface. | ||
implements the ``collections.abc.MutableMapping`` interface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect. It should implement the Mapping
interface. It may be mutable, but it is not a fully mutable mapping. You can't delete some values, and only strings are supported as keys.
No harm in adding some clarification that is has some mutability, but not the full MutableMapping
interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that's not the current situation either. The current implementation does not allow any deletion from the object. It also supports all kinds of keys per your request : ) .
Personally, I believe it's nicer to only support pure strings (we do have an open issue about this, allowing other types is kind of a mess for the locals proxy). Originally it was implemented that way because the PEP says so, but if we are editing the PEP, should we reconsider that decision? If we still support all types of keys, we need to answer the issue mentioned - what happens if something that looks like a string got passed in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After checking at the 3.14 interactive prompt, I moved this paragraph to a clearer location and changed it to say:
The view objects fully implement the ``collections.abc.Mapping`` interface,
and also implement the following mutable mapping operations:
* using assignment to add new key/value pairs
* using assignment to update the value associated with a key
* conditional assignment via the `setdefault()` method
* bulk updates via the `update()` method
Removing keys with `del` statements or the `pop()` method is NOT supported.
The first three could be simplified to item assignment (including via the setdefault() method)
, but calling out addition and rebinding separately seemed worthwhile (since they're genuinely different proxy operations)
I had omitted Unfortunately, I don't see a neat way of expressing the semantics of It's possible to get the semantics correct using
However, I'll see how the code version looks in context, since I agree that precision is desirable here. Edit: the semantically correct code version ended up looking fine, so I included it in the specification section (it's also useful for anyone else wanting to emulate this argument handling in their own Python code):
(Related: it's unfortunate it is too late in the release cycle to add a I agree the extra words in the backwards compatibility section aren't useful to implementers, but I do think they're potentially useful to regular Python users bitten by the change to make |
* exec/eval semantics should be clearly specified * proxies do not fully implement MutableMapping
PEP 123: Summary of changes
)📚 Documentation preview 📚: https://pep-previews--3845.org.readthedocs.build/