python-peps/pep-0667/index.html

981 lines
95 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>PEP 667 Consistent views of namespaces | peps.python.org</title>
<link rel="shortcut icon" href="../_static/py.png">
<link rel="canonical" href="https://peps.python.org/pep-0667/">
<link rel="stylesheet" href="../_static/style.css" type="text/css">
<link rel="stylesheet" href="../_static/mq.css" type="text/css">
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" media="(prefers-color-scheme: light)" id="pyg-light">
<link rel="stylesheet" href="../_static/pygments_dark.css" type="text/css" media="(prefers-color-scheme: dark)" id="pyg-dark">
<link rel="alternate" type="application/rss+xml" title="Latest PEPs" href="https://peps.python.org/peps.rss">
<meta property="og:title" content='PEP 667 Consistent views of namespaces | peps.python.org'>
<meta property="og:description" content="In early versions of Python all namespaces, whether in functions, classes or modules, were all implemented the same way: as a dictionary.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://peps.python.org/pep-0667/">
<meta property="og:site_name" content="Python Enhancement Proposals (PEPs)">
<meta property="og:image" content="https://peps.python.org/_static/og-image.png">
<meta property="og:image:alt" content="Python PEPs">
<meta property="og:image:width" content="200">
<meta property="og:image:height" content="200">
<meta name="description" content="In early versions of Python all namespaces, whether in functions, classes or modules, were all implemented the same way: as a dictionary.">
<meta name="theme-color" content="#3776ab">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-sun-half" viewBox="0 0 24 24" pointer-events="all">
<title>Following system colour scheme</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"></circle>
<path d="M12 3v18m0-12l4.65-4.65M12 14.3l7.37-7.37M12 19.6l8.85-8.85"></path>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24" pointer-events="all">
<title>Selected dark colour scheme</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24" pointer-events="all">
<title>Selected light colour scheme</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
</svg>
<script>
document.documentElement.dataset.colour_scheme = localStorage.getItem("colour_scheme") || "auto"
</script>
<section id="pep-page-section">
<header>
<h1>Python Enhancement Proposals</h1>
<ul class="breadcrumbs">
<li><a href="https://www.python.org/" title="The Python Programming Language">Python</a> &raquo; </li>
<li><a href="../pep-0000/">PEP Index</a> &raquo; </li>
<li>PEP 667</li>
</ul>
<button id="colour-scheme-cycler" onClick="setColourScheme(nextColourScheme())">
<svg aria-hidden="true" class="colour-scheme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg aria-hidden="true" class="colour-scheme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg aria-hidden="true" class="colour-scheme-icon-when-light"><use href="#svg-sun"></use></svg>
<span class="visually-hidden">Toggle light / dark / auto colour theme</span>
</button>
</header>
<article>
<section id="pep-content">
<h1 class="page-title">PEP 667 Consistent views of namespaces</h1>
<dl class="rfc2822 field-list simple">
<dt class="field-odd">Author<span class="colon">:</span></dt>
<dd class="field-odd">Mark Shannon &lt;mark&#32;&#97;t&#32;hotpy.org&gt;,
Tian Gao &lt;gaogaotiantian&#32;&#97;t&#32;hotmail.com&gt;</dd>
<dt class="field-even">Discussions-To<span class="colon">:</span></dt>
<dd class="field-even"><a class="reference external" href="https://discuss.python.org/t/46631">Discourse thread</a></dd>
<dt class="field-odd">Status<span class="colon">:</span></dt>
<dd class="field-odd"><abbr title="Accepted and implementation complete, or no longer active">Final</abbr></dd>
<dt class="field-even">Type<span class="colon">:</span></dt>
<dd class="field-even"><abbr title="Normative PEP with a new feature for Python, implementation change for CPython or interoperability standard for the ecosystem">Standards Track</abbr></dd>
<dt class="field-odd">Created<span class="colon">:</span></dt>
<dd class="field-odd">30-Jul-2021</dd>
<dt class="field-even">Python-Version<span class="colon">:</span></dt>
<dd class="field-even">3.13</dd>
<dt class="field-odd">Post-History<span class="colon">:</span></dt>
<dd class="field-odd">20-Aug-2021, 22-Feb-2024</dd>
<dt class="field-even">Resolution<span class="colon">:</span></dt>
<dd class="field-even"><a class="reference external" href="https://discuss.python.org/t/46631/25">25-Apr-2024</a></dd>
</dl>
<hr class="docutils" />
<section id="contents">
<details><summary>Table of Contents</summary><ul class="simple">
<li><a class="reference internal" href="#abstract">Abstract</a></li>
<li><a class="reference internal" href="#motivation">Motivation</a></li>
<li><a class="reference internal" href="#rationale">Rationale</a><ul>
<li><a class="reference internal" href="#making-the-frame-f-locals-attribute-a-write-through-proxy">Making the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute a write-through proxy</a></li>
<li><a class="reference internal" href="#making-the-locals-builtin-return-independent-snapshots">Making the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin return independent snapshots</a></li>
</ul>
</li>
<li><a class="reference internal" href="#specification">Specification</a><ul>
<li><a class="reference internal" href="#python-api">Python API</a><ul>
<li><a class="reference internal" href="#the-frame-f-locals-attribute">The <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute</a></li>
<li><a class="reference internal" href="#the-locals-builtin">The <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin</a></li>
<li><a class="reference internal" href="#the-eval-and-exec-builtins">The <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> builtins</a></li>
</ul>
</li>
<li><a class="reference internal" href="#c-api">C API</a><ul>
<li><a class="reference internal" href="#additions-to-the-pyeval-c-api">Additions to the <code class="docutils literal notranslate"><span class="pre">PyEval</span></code> C API</a></li>
<li><a class="reference internal" href="#pyframe-getlocals-c-api"><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code> C API</a></li>
<li><a class="reference internal" href="#deprecated-c-apis">Deprecated C APIs</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#summary-of-changes">Summary of Changes</a><ul>
<li><a class="reference internal" href="#python-api-changes">Python API changes</a><ul>
<li><a class="reference internal" href="#frame-f-locals-changes"><code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> changes</a></li>
<li><a class="reference internal" href="#locals-changes"><code class="docutils literal notranslate"><span class="pre">locals()</span></code> changes</a></li>
<li><a class="reference internal" href="#eval-and-exec-changes"><code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> changes</a></li>
</ul>
</li>
<li><a class="reference internal" href="#c-api-changes">C API changes</a><ul>
<li><a class="reference internal" href="#pyframe-getlocals-change"><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code> change</a></li>
<li><a class="reference internal" href="#pyeval-getlocals-change"><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals</span></code> change</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#backwards-compatibility">Backwards Compatibility</a><ul>
<li><a class="reference internal" href="#python-api-compatibility">Python API compatibility</a></li>
<li><a class="reference internal" href="#frame-f-locals-compatibility"><code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> compatibility</a><ul>
<li><a class="reference internal" href="#locals-compatibility"><code class="docutils literal notranslate"><span class="pre">locals()</span></code> compatibility</a></li>
<li><a class="reference internal" href="#impact-on-exec-and-eval">Impact on <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code></a></li>
<li><a class="reference internal" href="#impact-on-other-code-execution-apis-in-the-standard-library">Impact on other code execution APIs in the standard library</a></li>
</ul>
</li>
<li><a class="reference internal" href="#c-api-compatibility">C API compatibility</a><ul>
<li><a class="reference internal" href="#pyeval-getlocals-compatibility"><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals</span></code> compatibility</a></li>
</ul>
</li>
<li><a class="reference internal" href="#impact-on-pep-709-inlined-comprehensions">Impact on PEP 709 inlined comprehensions</a></li>
</ul>
</li>
<li><a class="reference internal" href="#implementation">Implementation</a><ul>
<li><a class="reference internal" href="#id1">C API</a></li>
</ul>
</li>
<li><a class="reference internal" href="#implementation-notes">Implementation Notes</a></li>
<li><a class="reference internal" href="#comparison-with-pep-558">Comparison with PEP 558</a></li>
<li><a class="reference internal" href="#reference-implementation">Reference Implementation</a></li>
<li><a class="reference internal" href="#copyright">Copyright</a></li>
</ul>
</details></section>
<div class="pep-banner canonical-doc sticky-banner admonition important">
<p class="admonition-title">Important</p>
<p>This PEP is a historical document. The up-to-date, canonical documentation can now be found at <a class="reference external" href="https://docs.python.org/3.13/library/functions.html#locals" title="(in Python v3.13)"><code class="xref py py-func docutils literal notranslate"><span class="pre">locals()</span></code></a>.</p>
<p class="close-button">×</p>
<p>See <a class="pep reference internal" href="../pep-0001/" title="PEP 1 PEP Purpose and Guidelines">PEP 1</a> for how to propose changes.</p>
</div>
<section id="abstract">
<h2><a class="toc-backref" href="#abstract" role="doc-backlink">Abstract</a></h2>
<p>In early versions of Python all namespaces, whether in functions,
classes or modules, were all implemented the same way: as a dictionary.</p>
<p>For performance reasons, the implementation of function namespaces was
changed. Unfortunately this meant that accessing these namespaces through
<code class="docutils literal notranslate"><span class="pre">locals()</span></code> and <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> ceased to be consistent and some
odd bugs crept in over the years as threads, generators and coroutines
were added.</p>
<p>This PEP proposes making these namespaces consistent once more.
Modifications to <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> will always be visible in
the underlying variables. Modifications to local variables will
immediately be visible in <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>, and they will be
consistent regardless of threading or coroutines.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">locals()</span></code> function will act the same as it does now for class
and modules scopes. For function scopes it will return an instantaneous
snapshot of the underlying <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> rather than implicitly
refreshing a single shared dictionary cached on the frame object.</p>
</section>
<section id="motivation">
<span id="pep-667-motivation"></span><h2><a class="toc-backref" href="#motivation" role="doc-backlink">Motivation</a></h2>
<p>The implementation of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> and <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> in releases up to and
including Python 3.12 is slow, inconsistent and buggy.
We want to make it faster, consistent, and most importantly fix the bugs.</p>
<p>For example, when attempting to manipulate local variables via frame objects:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">C</span><span class="p">:</span>
<span class="n">x</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">sys</span><span class="o">.</span><span class="n">_getframe</span><span class="p">()</span><span class="o">.</span><span class="n">f_locals</span><span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">2</span>
<span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</pre></div>
</div>
<p>prints <code class="docutils literal notranslate"><span class="pre">2</span></code>, but:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="n">x</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">sys</span><span class="o">.</span><span class="n">_getframe</span><span class="p">()</span><span class="o">.</span><span class="n">f_locals</span><span class="p">[</span><span class="s1">&#39;x&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">2</span>
<span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="n">f</span><span class="p">()</span>
</pre></div>
</div>
<p>prints <code class="docutils literal notranslate"><span class="pre">1</span></code>.</p>
<p>This is inconsistent, and confusing. Worse than that, the Python 3.12 behavior can
result in strange <a class="reference external" href="https://github.com/python/cpython/issues/74929">bugs</a>.</p>
<p>With this PEP both examples would print <code class="docutils literal notranslate"><span class="pre">2</span></code> as the function level
change would be written directly to the optimized local variables in
the frame rather than to a cached dictionary snapshot.</p>
<p>There are no compensating advantages for the Python 3.12 behavior;
it is unreliable and slow.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin has its own undesirable behaviours. Refer to <a class="pep reference internal" href="../pep-0558/" title="PEP 558 Defined semantics for locals()">PEP 558</a>
for additional details on those concerns.</p>
</section>
<section id="rationale">
<span id="pep-667-rationale"></span><h2><a class="toc-backref" href="#rationale" role="doc-backlink">Rationale</a></h2>
<section id="making-the-frame-f-locals-attribute-a-write-through-proxy">
<h3><a class="toc-backref" href="#making-the-frame-f-locals-attribute-a-write-through-proxy" role="doc-backlink">Making the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute a write-through proxy</a></h3>
<p>The Python 3.12 implementation of <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> returns a dictionary
that is created on the fly from the array of local variables. The
<code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> C API is then called by debuggers and trace
functions that want to write their changes back to the array (until
Python 3.11, this API was called implicitly after every trace function
invocation rather than being called explicitly by the trace functions).</p>
<p>This can result in the array and dictionary getting out of sync with
each other. Writes to the <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> frame attribute may not show up as
modifications to local variables if <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> is never
called. Writes to local variables can get lost if a dictionary snapshot
created before the variables were modified is written back to the frame
(since <em>every</em> known variable stored in the snapshot is written back to
the frame, even if the value stored on the frame had changed since the
snapshot was taken).</p>
<p>By making <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> return a view on the
underlying frame, these problems go away. <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> is always in
sync with the frame because it is a view of it, not a copy of it.</p>
</section>
<section id="making-the-locals-builtin-return-independent-snapshots">
<h3><a class="toc-backref" href="#making-the-locals-builtin-return-independent-snapshots" role="doc-backlink">Making the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin return independent snapshots</a></h3>
<p><a class="pep reference internal" href="../pep-0558/" title="PEP 558 Defined semantics for locals()">PEP 558</a> considered three potential options for standardising the behavior of the
<code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin in <a class="reference external" href="https://docs.python.org/3.13/glossary.html#term-optimized-scope" title="(in Python v3.13)"><span class="xref std std-term">optimized scopes</span></a>:</p>
<ul class="simple">
<li>retain the historical behaviour of having each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> on a given frame
update a single shared snapshot of the local variables</li>
<li>make <code class="docutils literal notranslate"><span class="pre">locals()</span></code> return write-through proxy instances (similar
to <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>)</li>
<li>make <code class="docutils literal notranslate"><span class="pre">locals()</span></code> return genuinely independent snapshots so that
attempts to change the values of local variables via <code class="docutils literal notranslate"><span class="pre">exec()</span></code>
would be <em>consistently</em> ignored rather than being accepted in some circumstances</li>
</ul>
<p>The last option was chosen as the one which could most easily be explained in the
language reference, and memorised by users:</p>
<ul class="simple">
<li>the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin gives an instantaneous snapshot of the local variables in
optimized scopes, and read/write access in other scopes; and</li>
<li><code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> gives read/write access to the local variables in all scopes,
including optimized scopes</li>
</ul>
<p>This approach allows the intent of a piece of code to be clearer than it would be if both
APIs granted full read/write access in optimized scopes, even when write access wasnt
needed or desired. For additional details on this design decision, refer to <a class="pep reference internal" href="../pep-0558/" title="PEP 558 Defined semantics for locals()">PEP 558</a>,
especially the <a class="reference internal" href="../pep-0558/#pep-558-motivation"><span class="std std-ref">Motivation</span></a> section and <a class="reference internal" href="../pep-0558/#pep-558-exec-eval-impact"><span class="std std-ref">Additional considerations for eval() and exec() in optimized scopes</span></a>.</p>
<p>This approach is not without its drawbacks, which are covered
in the Backwards Compatibility section below.</p>
</section>
</section>
<section id="specification">
<h2><a class="toc-backref" href="#specification" role="doc-backlink">Specification</a></h2>
<section id="python-api">
<h3><a class="toc-backref" href="#python-api" role="doc-backlink">Python API</a></h3>
<section id="the-frame-f-locals-attribute">
<span id="pep-667-f-locals-spec"></span><h4><a class="toc-backref" href="#the-frame-f-locals-attribute" role="doc-backlink">The <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute</a></h4>
<p>For module and class scopes (including <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code>
invocations), <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> is a direct
reference to the local variable namespace used in code execution.</p>
<p>For function scopes (and other <a class="reference external" href="https://docs.python.org/3.13/glossary.html#term-optimized-scope" title="(in Python v3.13)"><span class="xref std std-term">optimized scopes</span></a>)
it will be an instance of a new write-through proxy type that can directly modify
the optimized local variable storage array in the underlying frame, as well as the
contents of any cell references to non-local variables.</p>
<p>The view objects fully implement the <code class="docutils literal notranslate"><span class="pre">collections.abc.Mapping</span></code> interface,
and also implement the following mutable mapping operations:</p>
<ul class="simple">
<li>using assignment to add new key/value pairs</li>
<li>using assignment to update the value associated with a key</li>
<li>conditional assignment via the <code class="docutils literal notranslate"><span class="pre">setdefault()</span></code> method</li>
<li>bulk updates via the <code class="docutils literal notranslate"><span class="pre">update()</span></code> method</li>
</ul>
<p>Views of different frames compare unequal even if they have the same contents.</p>
<p>All writes to the <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> mapping will be immediately visible
in the underlying variables. All changes to the underlying variables
will be immediately visible in the mapping.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> object will be a full mapping, and can have arbitrary
key-value pairs added to it. New names added via the proxies
will be stored in a dedicated shared dictionary stored on the
underlying frame object (so all proxy instances for a given frame
will be able to access any names added this way).</p>
<p>Extra keys (which do not correspond to local variables on the underlying
frame) may be removed as usual with <code class="docutils literal notranslate"><span class="pre">del</span></code> statements or the <code class="docutils literal notranslate"><span class="pre">pop()</span></code>
method.</p>
<p>Using <code class="docutils literal notranslate"><span class="pre">del</span></code>, or the <code class="docutils literal notranslate"><span class="pre">pop()</span></code> method, to remove keys that correspond to local
variables on the underlying frame is NOT supported, and attempting to do so
will raise <code class="docutils literal notranslate"><span class="pre">ValueError</span></code>.
Local variables can only be set to <code class="docutils literal notranslate"><span class="pre">None</span></code> (or some other value) via the proxy,
they cannot be unbound completely.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">clear()</span></code> method is NOT implemented on the write-through proxies, as it
is unclear how it should handle the inability to delete entries corresponding
to local variables.</p>
<p>To maintain backwards compatibility, proxy APIs that need to produce a
new mapping (such as <code class="docutils literal notranslate"><span class="pre">copy()</span></code>) will produce regular builtin <code class="docutils literal notranslate"><span class="pre">dict</span></code>
instances, rather than write-through proxy instances.</p>
<p>To avoid introducing a circular reference between frame objects and the
write-through proxies, each access to <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> returns a <em>new</em>
write-through proxy instance.</p>
</section>
<section id="the-locals-builtin">
<h4><a class="toc-backref" href="#the-locals-builtin" role="doc-backlink">The <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin</a></h4>
<p><code class="docutils literal notranslate"><span class="pre">locals()</span></code> will be defined as:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">locals</span><span class="p">():</span>
<span class="n">frame</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">_getframe</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">f_locals</span> <span class="o">=</span> <span class="n">frame</span><span class="o">.</span><span class="n">f_locals</span>
<span class="k">if</span> <span class="n">frame</span><span class="o">.</span><span class="n">_is_optimized</span><span class="p">():</span> <span class="c1"># Not an actual frame method</span>
<span class="n">f_locals</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">f_locals</span><span class="p">)</span>
<span class="k">return</span> <span class="n">f_locals</span>
</pre></div>
</div>
<p>For module and class scopes (including <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code>
invocations), <code class="docutils literal notranslate"><span class="pre">locals()</span></code> continues to return a direct
reference to the local variable namespace used in code execution
(which is also the same value reported by <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>).</p>
<p>In <a class="reference external" href="https://docs.python.org/3.13/glossary.html#term-optimized-scope" title="(in Python v3.13)"><span class="xref std std-term">optimized scopes</span></a>,
each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> will produce an <em>independent</em>
snapshot of the local variables.</p>
</section>
<section id="the-eval-and-exec-builtins">
<h4><a class="toc-backref" href="#the-eval-and-exec-builtins" role="doc-backlink">The <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> builtins</a></h4>
<p>Because this PEP changes the behavior of <code class="docutils literal notranslate"><span class="pre">locals()</span></code>, the
behavior of <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> also changes.</p>
<p>Assuming a function <code class="docutils literal notranslate"><span class="pre">_eval()</span></code> which performs the job of
<code class="docutils literal notranslate"><span class="pre">eval()</span></code> with explicit namespace arguments, <code class="docutils literal notranslate"><span class="pre">eval()</span></code>
can be defined as follows:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">FrameProxyType</span> <span class="o">=</span> <span class="nb">type</span><span class="p">((</span><span class="k">lambda</span><span class="p">:</span> <span class="n">sys</span><span class="o">.</span><span class="n">_getframe</span><span class="p">()</span><span class="o">.</span><span class="n">f_locals</span><span class="p">)())</span>
<span class="k">def</span> <span class="nf">eval</span><span class="p">(</span><span class="n">expression</span><span class="p">,</span> <span class="o">/</span><span class="p">,</span> <span class="nb">globals</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="nb">locals</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">if</span> <span class="nb">globals</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="c1"># No globals -&gt; use calling frame&#39;s globals</span>
<span class="n">_calling_frame</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">_getframe</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="nb">globals</span> <span class="o">=</span> <span class="n">_calling_frame</span><span class="o">.</span><span class="n">f_globals</span>
<span class="k">if</span> <span class="nb">locals</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="c1"># No globals or locals -&gt; use calling frame&#39;s locals</span>
<span class="nb">locals</span> <span class="o">=</span> <span class="n">_calling_frame</span><span class="o">.</span><span class="n">f_locals</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="nb">locals</span><span class="p">,</span> <span class="n">FrameProxyType</span><span class="p">):</span>
<span class="c1"># Align with locals() builtin in optimized frame</span>
<span class="nb">locals</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="nb">locals</span><span class="p">)</span>
<span class="k">elif</span> <span class="nb">locals</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="c1"># Globals but no locals -&gt; use same namespace for both</span>
<span class="nb">locals</span> <span class="o">=</span> <span class="nb">globals</span>
<span class="k">return</span> <span class="n">_eval</span><span class="p">(</span><span class="n">expression</span><span class="p">,</span> <span class="nb">globals</span><span class="p">,</span> <span class="nb">locals</span><span class="p">)</span>
</pre></div>
</div>
<p>The specified argument handling for <code class="docutils literal notranslate"><span class="pre">exec()</span></code> is similarly updated.</p>
<p>(In Python 3.12 and earlier, it was not possible to provide <code class="docutils literal notranslate"><span class="pre">locals</span></code>
to <code class="docutils literal notranslate"><span class="pre">eval()</span></code> or <code class="docutils literal notranslate"><span class="pre">exec()</span></code> without also providing <code class="docutils literal notranslate"><span class="pre">globals</span></code> as these
were previously positional-only arguments. Independently of this
PEP, Python 3.13 updated these builtins to accept keyword arguments)</p>
</section>
</section>
<section id="c-api">
<h3><a class="toc-backref" href="#c-api" role="doc-backlink">C API</a></h3>
<section id="additions-to-the-pyeval-c-api">
<h4><a class="toc-backref" href="#additions-to-the-pyeval-c-api" role="doc-backlink">Additions to the <code class="docutils literal notranslate"><span class="pre">PyEval</span></code> C API</a></h4>
<p>Three new C-API functions will be added:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">PyObject</span> <span class="o">*</span><span class="n">PyEval_GetFrameLocals</span><span class="p">(</span><span class="n">void</span><span class="p">)</span>
<span class="n">PyObject</span> <span class="o">*</span><span class="n">PyEval_GetFrameGlobals</span><span class="p">(</span><span class="n">void</span><span class="p">)</span>
<span class="n">PyObject</span> <span class="o">*</span><span class="n">PyEval_GetFrameBuiltins</span><span class="p">(</span><span class="n">void</span><span class="p">)</span>
</pre></div>
</div>
<p><code class="docutils literal notranslate"><span class="pre">PyEval_GetFrameLocals()</span></code> is equivalent to: <code class="docutils literal notranslate"><span class="pre">locals()</span></code>.
<code class="docutils literal notranslate"><span class="pre">PyEval_GetFrameGlobals()</span></code> is equivalent to: <code class="docutils literal notranslate"><span class="pre">globals()</span></code>.</p>
<p>All of these functions will return a new reference.</p>
</section>
<section id="pyframe-getlocals-c-api">
<h4><a class="toc-backref" href="#pyframe-getlocals-c-api" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code> C API</a></h4>
<p>The existing <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals(f)</span></code> C API is equivalent to <code class="docutils literal notranslate"><span class="pre">f.f_locals</span></code>.
Its return value will be as described above for accessing <code class="docutils literal notranslate"><span class="pre">f.f_locals</span></code>.</p>
<p>This function returns a new reference, so it is able to accommodate the
creation of a new write-through proxy instance on each call in an
optimized scope.</p>
</section>
<section id="deprecated-c-apis">
<h4><a class="toc-backref" href="#deprecated-c-apis" role="doc-backlink">Deprecated C APIs</a></h4>
<p>The following C API functions will be deprecated, as they return borrowed references:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">PyEval_GetLocals</span><span class="p">()</span>
<span class="n">PyEval_GetGlobals</span><span class="p">()</span>
<span class="n">PyEval_GetBuiltins</span><span class="p">()</span>
</pre></div>
</div>
<p>The following functions (which return new references) should be used instead:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">PyEval_GetFrameLocals</span><span class="p">()</span>
<span class="n">PyEval_GetFrameGlobals</span><span class="p">()</span>
<span class="n">PyEval_GetFrameBuiltins</span><span class="p">()</span>
</pre></div>
</div>
<p>The following C API functions will become no-ops, and will be deprecated without
replacement:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">PyFrame_FastToLocalsWithError</span><span class="p">()</span>
<span class="n">PyFrame_FastToLocals</span><span class="p">()</span>
<span class="n">PyFrame_LocalsToFast</span><span class="p">()</span>
</pre></div>
</div>
<p>All of the deprecated functions will be marked as deprecated in the Python 3.13 documentation.</p>
<p>Of these functions, only <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> poses any significant maintenance burden.
Accordingly, calls to <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> will emit <code class="docutils literal notranslate"><span class="pre">DeprecationWarning</span></code> in Python
3.14, with a target removal date of Python 3.16 (two releases after Python 3.14).
Alternatives are recommended as described in <a class="reference internal" href="#pep-667-pyeval-getlocals-compatibility"><span class="std std-ref">PyEval_GetLocals compatibility</span></a>.</p>
</section>
</section>
</section>
<section id="summary-of-changes">
<h2><a class="toc-backref" href="#summary-of-changes" role="doc-backlink">Summary of Changes</a></h2>
<p>This section summarises how the specified behaviour in Python 3.13 and later
differs from the historical behaviour in Python 3.12 and earlier versions.</p>
<section id="python-api-changes">
<h3><a class="toc-backref" href="#python-api-changes" role="doc-backlink">Python API changes</a></h3>
<section id="frame-f-locals-changes">
<h4><a class="toc-backref" href="#frame-f-locals-changes" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> changes</a></h4>
<p>Consider the following example:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">l</span><span class="p">():</span>
<span class="s2">&quot;Get the locals of caller&quot;</span>
<span class="k">return</span> <span class="n">sys</span><span class="o">.</span><span class="n">_getframe</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="n">f_locals</span>
<span class="k">def</span> <span class="nf">test</span><span class="p">():</span>
<span class="k">if</span> <span class="mi">0</span><span class="p">:</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">1</span> <span class="c1"># Make &#39;y&#39; a local variable</span>
<span class="n">x</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">l</span><span class="p">()[</span><span class="s1">&#39;x&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">l</span><span class="p">()[</span><span class="s1">&#39;y&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">4</span>
<span class="n">l</span><span class="p">()[</span><span class="s1">&#39;z&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">5</span>
<span class="n">y</span>
<span class="nb">print</span><span class="p">(</span><span class="nb">locals</span><span class="p">(),</span> <span class="n">x</span><span class="p">)</span>
</pre></div>
</div>
<p>Given the changes in this PEP,
<code class="docutils literal notranslate"><span class="pre">test()</span></code> will print <code class="docutils literal notranslate"><span class="pre">{'x':</span> <span class="pre">2,</span> <span class="pre">'y':</span> <span class="pre">4,</span> <span class="pre">'z':</span> <span class="pre">5}</span> <span class="pre">2</span></code>.</p>
<p>In Python 3.12, this example will fail with an <code class="docutils literal notranslate"><span class="pre">UnboundLocalError</span></code>,
as the definition of <code class="docutils literal notranslate"><span class="pre">y</span></code> by <code class="docutils literal notranslate"><span class="pre">l()['y']</span> <span class="pre">=</span> <span class="pre">4</span></code> is lost.</p>
<p>If the second-to-last line were changed from <code class="docutils literal notranslate"><span class="pre">y</span></code> to <code class="docutils literal notranslate"><span class="pre">z</span></code>, this will still
raise <code class="docutils literal notranslate"><span class="pre">NameError</span></code>, as it does in Python 3.12.
Keys added to <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> that are not lexically local variables
remain visible in <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>,
but do not dynamically become local variables.</p>
</section>
<section id="locals-changes">
<span id="pep-667-locals-changes"></span><h4><a class="toc-backref" href="#locals-changes" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">locals()</span></code> changes</a></h4>
<p>Consider the following example:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="n">exec</span><span class="p">(</span><span class="s2">&quot;x = 1&quot;</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="nb">locals</span><span class="p">()</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;x&quot;</span><span class="p">))</span>
<span class="n">f</span><span class="p">()</span>
</pre></div>
</div>
<p>Given the changes in this PEP, this will <em>always</em> print <code class="docutils literal notranslate"><span class="pre">None</span></code>
(regardless of whether <code class="docutils literal notranslate"><span class="pre">x</span></code> is a defined local variable in the function),
as the explicit call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> produces a distinct snapshot from
the one implicitly used in the <code class="docutils literal notranslate"><span class="pre">exec()</span></code> call.</p>
<p>In Python 3.12, the exact example shown would print <code class="docutils literal notranslate"><span class="pre">1</span></code>, but seemingly
unrelated changes to the definition of the function involved could make
it print <code class="docutils literal notranslate"><span class="pre">None</span></code> instead (<a class="reference internal" href="../pep-0558/#pep-558-exec-eval-impact"><span class="std std-ref">Additional considerations for eval() and exec() in optimized scopes</span></a> in PEP 558
goes into more detail on that topic).</p>
</section>
<section id="eval-and-exec-changes">
<h4><a class="toc-backref" href="#eval-and-exec-changes" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> changes</a></h4>
<p>The primary change affecting <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> is shown
in the “<a class="reference internal" href="#pep-667-locals-changes"><span class="std std-ref">locals() changes</span></a>” example: repeatedly
accessing <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in an optimized scope will no longer
implicitly share a common underlying namespace.</p>
</section>
</section>
<section id="c-api-changes">
<h3><a class="toc-backref" href="#c-api-changes" role="doc-backlink">C API changes</a></h3>
<section id="pyframe-getlocals-change">
<h4><a class="toc-backref" href="#pyframe-getlocals-change" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code> change</a></h4>
<p><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code> can already return arbitrary mappings in Python 3.12,
as <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code> accept arbitrary mappings as their <code class="docutils literal notranslate"><span class="pre">locals</span></code> argument,
and metaclasses may return arbitrary mappings from their <code class="docutils literal notranslate"><span class="pre">__prepare__</span></code> methods.</p>
<p>Returning a frame locals proxy in optimized scopes just adds another case where
something other than a builtin dictionary will be returned.</p>
</section>
<section id="pyeval-getlocals-change">
<h4><a class="toc-backref" href="#pyeval-getlocals-change" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals</span></code> change</a></h4>
<p>The semantics of <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> are technically unchanged, but they do change in
practice as the dictionary cached on optimized frames is no longer shared with other
mechanisms for accessing the frame locals (<code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin, <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code>
function, frame <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> attributes).</p>
</section>
</section>
</section>
<section id="backwards-compatibility">
<h2><a class="toc-backref" href="#backwards-compatibility" role="doc-backlink">Backwards Compatibility</a></h2>
<section id="python-api-compatibility">
<h3><a class="toc-backref" href="#python-api-compatibility" role="doc-backlink">Python API compatibility</a></h3>
<p>The implementation used in versions up to and including Python 3.12 has many
corner cases and oddities. Code that works around those may need to be changed.
Code that uses <code class="docutils literal notranslate"><span class="pre">locals()</span></code> for simple templating, or print debugging,
will continue to work correctly. Debuggers and other tools that use
<code class="docutils literal notranslate"><span class="pre">f_locals</span></code> to modify local variables, will now work correctly,
even in the presence of threaded code, coroutines and generators.</p>
</section>
<section id="frame-f-locals-compatibility">
<h3><a class="toc-backref" href="#frame-f-locals-compatibility" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> compatibility</a></h3>
<p>Although <code class="docutils literal notranslate"><span class="pre">f.f_locals</span></code> behaves as if it were the namespace of the function,
there will be some observable differences.
For example, <code class="docutils literal notranslate"><span class="pre">f.f_locals</span> <span class="pre">is</span> <span class="pre">f.f_locals</span></code> will be <code class="docutils literal notranslate"><span class="pre">False</span></code> for optimized
frames, as each access to the attribute produces a new write-through proxy
instance.</p>
<p>However <code class="docutils literal notranslate"><span class="pre">f.f_locals</span> <span class="pre">==</span> <span class="pre">f.f_locals</span></code> will be <code class="docutils literal notranslate"><span class="pre">True</span></code>, and
all changes to the underlying variables, by any means, including the
addition of new variable names as mapping keys, will always be visible.</p>
<section id="locals-compatibility">
<h4><a class="toc-backref" href="#locals-compatibility" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">locals()</span></code> compatibility</a></h4>
<p><code class="docutils literal notranslate"><span class="pre">locals()</span> <span class="pre">is</span> <span class="pre">locals()</span></code> will be <code class="docutils literal notranslate"><span class="pre">False</span></code> for optimized frames, so
code like the following will raise <code class="docutils literal notranslate"><span class="pre">KeyError</span></code> instead of returning
<code class="docutils literal notranslate"><span class="pre">1</span></code>:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="nb">locals</span><span class="p">()[</span><span class="s2">&quot;x&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="nb">locals</span><span class="p">()[</span><span class="s2">&quot;x&quot;</span><span class="p">]</span>
</pre></div>
</div>
<p>To continue working, such code will need to explicitly store the namespace
to be modified in a local variable, rather than relying on the previous
implicit caching on the frame object:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="n">ns</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">ns</span><span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="n">ns</span><span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">]</span>
</pre></div>
</div>
<p>While this technically isnt a formal backwards compatibility break
(since the behaviour of writing back to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> was explicitly
documented as undefined), there is definitely some code that relies
on the existing behaviour. Accordingly, the updated behaviour will
be explicitly noted in the documentation as a change and it will be
covered in the Python 3.13 porting guide.</p>
<p>To work with a copy of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in optimized scopes on all
versions without making redundant copies on Python 3.13+, users
will need to define a version-dependent helper function that only
makes an explicit copy on Python versions prior to Python 3.13:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="n">sys</span><span class="o">.</span><span class="n">version_info</span> <span class="o">&gt;=</span> <span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">13</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">_ensure_func_snapshot</span><span class="p">(</span><span class="n">d</span><span class="p">):</span>
<span class="k">return</span> <span class="n">d</span> <span class="c1"># 3.13+ locals() already returns a snapshot</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">_ensure_func_snapshot</span><span class="p">(</span><span class="n">d</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">dict</span><span class="p">(</span><span class="n">d</span><span class="p">)</span> <span class="c1"># Create snapshot on older versions</span>
<span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="n">ns</span> <span class="o">=</span> <span class="n">_ensure_func_snapshot</span><span class="p">(</span><span class="nb">locals</span><span class="p">())</span>
<span class="n">ns</span><span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="n">ns</span>
</pre></div>
</div>
<p>In other scopes, <code class="docutils literal notranslate"><span class="pre">locals().copy()</span></code> can continue to be called
unconditionally without introducing any redundant copies.</p>
</section>
<section id="impact-on-exec-and-eval">
<h4><a class="toc-backref" href="#impact-on-exec-and-eval" role="doc-backlink">Impact on <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code></a></h4>
<p>Even though this PEP does not modify <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> directly,
the semantic change to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> impacts the behavior of <code class="docutils literal notranslate"><span class="pre">exec()</span></code>
and <code class="docutils literal notranslate"><span class="pre">eval()</span></code> as they default to running code in the calling namespace.</p>
<p>This poses a potential compatibility issue for some code, as with the
previous implementation that returns the same dict when <code class="docutils literal notranslate"><span class="pre">locals()</span></code> is called
multiple times in function scope, the following code usually worked due to
the implicitly shared local variable namespace:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="n">exec</span><span class="p">(</span><span class="s1">&#39;a = 0&#39;</span><span class="p">)</span> <span class="c1"># equivalent to exec(&#39;a = 0&#39;, globals(), locals())</span>
<span class="n">exec</span><span class="p">(</span><span class="s1">&#39;print(a)&#39;</span><span class="p">)</span> <span class="c1"># equivalent to exec(&#39;print(a)&#39;, globals(), locals())</span>
<span class="nb">print</span><span class="p">(</span><span class="nb">locals</span><span class="p">())</span> <span class="c1"># {&#39;a&#39;: 0}</span>
<span class="c1"># However, print(a) will not work here</span>
<span class="n">f</span><span class="p">()</span>
</pre></div>
</div>
<p>With the semantic changes to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in this PEP, the <code class="docutils literal notranslate"><span class="pre">exec('print(a)')'</span></code> call
will fail with <code class="docutils literal notranslate"><span class="pre">NameError</span></code>, and <code class="docutils literal notranslate"><span class="pre">print(locals())</span></code> will report an empty dictionary, as
each line will be using its own distinct snapshot of the local variables rather than
implicitly sharing a single cached snapshot stored on the frame object.</p>
<p>A shared namespace across <code class="docutils literal notranslate"><span class="pre">exec()</span></code> calls can still be obtained by using explicit
namespaces rather than relying on the previously implicitly shared frame namespace:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="n">ns</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">exec</span><span class="p">(</span><span class="s1">&#39;a = 0&#39;</span><span class="p">,</span> <span class="nb">locals</span><span class="o">=</span><span class="n">ns</span><span class="p">)</span>
<span class="n">exec</span><span class="p">(</span><span class="s1">&#39;print(a)&#39;</span><span class="p">,</span> <span class="nb">locals</span><span class="o">=</span><span class="n">ns</span><span class="p">)</span> <span class="c1"># 0</span>
<span class="n">f</span><span class="p">()</span>
</pre></div>
</div>
<p>You can even reliably change the variables in the local scope by explicitly using
<code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>, which was not possible before (even using <code class="docutils literal notranslate"><span class="pre">ctypes</span></code> to
invoke <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast</span></code> was subject to the state inconsistency problems
discussed elsewhere in this PEP):</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span>
<span class="n">a</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">exec</span><span class="p">(</span><span class="s1">&#39;a = 0&#39;</span><span class="p">,</span> <span class="nb">locals</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">_getframe</span><span class="p">()</span><span class="o">.</span><span class="n">f_locals</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="c1"># 0</span>
<span class="n">f</span><span class="p">()</span>
</pre></div>
</div>
<p>The behavior of <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code> for module and class scopes (including
nested invocations) is not changed, as the behaviour of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in those
scopes is not changing.</p>
</section>
<section id="impact-on-other-code-execution-apis-in-the-standard-library">
<h4><a class="toc-backref" href="#impact-on-other-code-execution-apis-in-the-standard-library" role="doc-backlink">Impact on other code execution APIs in the standard library</a></h4>
<p><code class="docutils literal notranslate"><span class="pre">pdb</span></code> and <code class="docutils literal notranslate"><span class="pre">bdb</span></code> use the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> API, and hence will be able to
reliably update local variables even in optimized frames. Implementing this
PEP will resolve several longstanding bugs in these modules relating to threads,
generators, coroutines, and other mechanisms that allow concurrent code execution
while the debugger is active.</p>
<p>Other code execution APIs in the standard library (such as the <code class="docutils literal notranslate"><span class="pre">code</span></code> module)
do not implicitly access <code class="docutils literal notranslate"><span class="pre">locals()</span></code> <em>or</em> <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>, but the behaviour
of explicitly passing these namespaces will change as described in the rest of
this PEP (passing <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in optimized scopes will no longer implicitly
share the code execution namespace across calls, passing <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>
in optimized scopes will allow reliable modification of local variables and
nonlocal cell references).</p>
</section>
</section>
<section id="c-api-compatibility">
<h3><a class="toc-backref" href="#c-api-compatibility" role="doc-backlink">C API compatibility</a></h3>
<section id="pyeval-getlocals-compatibility">
<span id="pep-667-pyeval-getlocals-compatibility"></span><h4><a class="toc-backref" href="#pyeval-getlocals-compatibility" role="doc-backlink"><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals</span></code> compatibility</a></h4>
<p><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> has never historically distinguished between whether it was
emulating <code class="docutils literal notranslate"><span class="pre">locals()</span></code> or <code class="docutils literal notranslate"><span class="pre">sys._getframe().f_locals</span></code> at the Python level, as they all
returned references to the same shared cache of the local variable bindings.</p>
<p>With this PEP, <code class="docutils literal notranslate"><span class="pre">locals()</span></code> changes to return independent snapshots on each call for
optimized frames, and <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> (along with <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code>) changes to
return new write-through proxy instances.</p>
<p>Because <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> returns a borrowed reference, it isnt possible to update
its semantics to align with either of those alternatives, leaving it as the only remaining
API that requires a shared cache dictionary stored on the frame object.</p>
<p>While this technically leaves the semantics of the function unchanged, it no longer allows
extra dict entries to be made visible to users of the other APIs, as those APIs are no longer
accessing the same underlying cache dictionary.</p>
<p>When <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> is being used as an equivalent to the Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code>
builtin, <code class="docutils literal notranslate"><span class="pre">PyEval_GetFrameLocals()</span></code> should be used instead.</p>
<p>This code:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nb">locals</span> <span class="o">=</span> <span class="n">PyEval_GetLocals</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">locals</span> <span class="o">==</span> <span class="n">NULL</span><span class="p">)</span> <span class="p">{</span>
<span class="n">goto</span> <span class="n">error_handler</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">Py_INCREF</span><span class="p">(</span><span class="nb">locals</span><span class="p">);</span>
</pre></div>
</div>
<p>should be replaced with:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">//</span> <span class="n">Equivalent</span> <span class="n">to</span> <span class="s2">&quot;locals()&quot;</span> <span class="ow">in</span> <span class="n">Python</span> <span class="n">code</span>
<span class="nb">locals</span> <span class="o">=</span> <span class="n">PyEval_GetFrameLocals</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">locals</span> <span class="o">==</span> <span class="n">NULL</span><span class="p">)</span> <span class="p">{</span>
<span class="n">goto</span> <span class="n">error_handler</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>When <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> is being used as an equivalent to calling
<code class="docutils literal notranslate"><span class="pre">sys._getframe().f_locals</span></code> in Python, it should be replaced by calling
<code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals()</span></code> on the result of <code class="docutils literal notranslate"><span class="pre">PyEval_GetFrame()</span></code>.</p>
<p>In these cases, the original code should be replaced with:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="o">//</span> <span class="n">Equivalent</span> <span class="n">to</span> <span class="s2">&quot;sys._getframe()&quot;</span> <span class="ow">in</span> <span class="n">Python</span> <span class="n">code</span>
<span class="n">frame</span> <span class="o">=</span> <span class="n">PyEval_GetFrame</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="n">frame</span> <span class="o">==</span> <span class="n">NULL</span><span class="p">)</span> <span class="p">{</span>
<span class="n">goto</span> <span class="n">error_handler</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">//</span> <span class="n">Equivalent</span> <span class="n">to</span> <span class="s2">&quot;frame.f_locals&quot;</span> <span class="ow">in</span> <span class="n">Python</span> <span class="n">code</span>
<span class="nb">locals</span> <span class="o">=</span> <span class="n">PyFrame_GetLocals</span><span class="p">(</span><span class="n">frame</span><span class="p">);</span>
<span class="n">frame</span> <span class="o">=</span> <span class="n">NULL</span><span class="p">;</span> <span class="o">//</span> <span class="n">Minimise</span> <span class="n">visibility</span> <span class="n">of</span> <span class="n">borrowed</span> <span class="n">reference</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">locals</span> <span class="o">==</span> <span class="n">NULL</span><span class="p">)</span> <span class="p">{</span>
<span class="n">goto</span> <span class="n">error_handler</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
</section>
</section>
<section id="impact-on-pep-709-inlined-comprehensions">
<h3><a class="toc-backref" href="#impact-on-pep-709-inlined-comprehensions" role="doc-backlink">Impact on PEP 709 inlined comprehensions</a></h3>
<p>For inlined comprehensions within a function, <code class="docutils literal notranslate"><span class="pre">locals()</span></code> currently behaves the
same inside or outside of the comprehension, and this will not change. The
behavior of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> inside functions will generally change as specified in
the rest of this PEP.</p>
<p>For inlined comprehensions at module or class scope, calling <code class="docutils literal notranslate"><span class="pre">locals()</span></code> within
the inlined comprehension returns a new dictionary for each call. This PEP will
make <code class="docutils literal notranslate"><span class="pre">locals()</span></code> within a function also always return a new dictionary for each
call, improving consistency; class or module scope inlined comprehensions will
appear to behave as if the inlined comprehension is still a distinct function.</p>
</section>
</section>
<section id="implementation">
<h2><a class="toc-backref" href="#implementation" role="doc-backlink">Implementation</a></h2>
<p>Each read of <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> will create a new proxy object that gives
the appearance of being the mapping of local (including cell and free)
variable names to the values of those local variables.</p>
<p>A possible implementation is sketched out below.
All attributes that start with an underscore are invisible and
cannot be accessed directly.
They serve only to illustrate the proposed design.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">NULL</span><span class="p">:</span> <span class="n">Object</span> <span class="c1"># NULL is a singleton representing the absence of a value.</span>
<span class="k">class</span> <span class="nc">CodeType</span><span class="p">:</span>
<span class="n">_name_to_offset_mapping_impl</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">|</span> <span class="n">NULL</span>
<span class="n">_cells</span><span class="p">:</span> <span class="nb">frozenset</span> <span class="c1"># Set of indexes of cell and free variables</span>
<span class="o">...</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">...</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_name_to_offset_mapping_impl</span> <span class="o">=</span> <span class="n">NULL</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_variable_names</span> <span class="o">=</span> <span class="n">deduplicate</span><span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">co_varnames</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">co_cellvars</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">co_freevars</span>
<span class="p">)</span>
<span class="o">...</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">_name_to_offset_mapping</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="s2">&quot;Mapping of names to offsets in local variable array.&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_name_to_offset_mapping_impl</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_name_to_offset_mapping_impl</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">name</span><span class="p">:</span> <span class="n">index</span> <span class="k">for</span> <span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_variable_names</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_name_to_offset_mapping_impl</span>
<span class="k">class</span> <span class="nc">FrameType</span><span class="p">:</span>
<span class="n">_locals</span> <span class="p">:</span> <span class="n">array</span><span class="p">[</span><span class="n">Object</span><span class="p">]</span> <span class="c1"># The values of the local variables, items may be NULL.</span>
<span class="n">_extra_locals</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">|</span> <span class="n">NULL</span> <span class="c1"># Dictionary for storing extra locals not in _locals.</span>
<span class="n">_locals_cache</span><span class="p">:</span> <span class="n">FrameLocalsProxy</span> <span class="o">|</span> <span class="n">NULL</span> <span class="c1"># required to support PyEval_GetLocals()</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">...</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_extra_locals</span> <span class="o">=</span> <span class="n">NULL</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_locals_cache</span> <span class="o">=</span> <span class="n">NULL</span>
<span class="o">...</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">f_locals</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="n">FrameLocalsProxy</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">FrameLocalsProxy</span><span class="p">:</span>
<span class="s2">&quot;Implements collections.MutableMapping.&quot;</span>
<span class="vm">__slots__</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&quot;_frame&quot;</span><span class="p">,</span> <span class="p">)</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">frame</span><span class="p">:</span><span class="n">FrameType</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_frame</span> <span class="o">=</span> <span class="n">frame</span>
<span class="k">def</span> <span class="fm">__getitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="n">f</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_frame</span>
<span class="n">co</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">f_code</span>
<span class="k">if</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">co</span><span class="o">.</span><span class="n">_name_to_offset_mapping</span><span class="p">:</span>
<span class="n">index</span> <span class="o">=</span> <span class="n">co</span><span class="o">.</span><span class="n">_name_to_offset_mapping</span><span class="p">[</span><span class="n">name</span><span class="p">]</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="k">if</span> <span class="n">val</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">KeyError</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">if</span> <span class="n">index</span> <span class="ow">in</span> <span class="n">co</span><span class="o">.</span><span class="n">_cells</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">val</span><span class="o">.</span><span class="n">cell_contents</span>
<span class="k">if</span> <span class="n">val</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">KeyError</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">return</span> <span class="n">val</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">f</span><span class="o">.</span><span class="n">_extra_locals</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">KeyError</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">return</span> <span class="n">f</span><span class="o">.</span><span class="n">_extra_locals</span><span class="p">[</span><span class="n">name</span><span class="p">]</span>
<span class="k">def</span> <span class="fm">__setitem__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
<span class="n">f</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_frame</span>
<span class="n">co</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">f_code</span>
<span class="k">if</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">co</span><span class="o">.</span><span class="n">_name_to_offset_mapping</span><span class="p">:</span>
<span class="n">index</span> <span class="o">=</span> <span class="n">co</span><span class="o">.</span><span class="n">_name_to_offset_mapping</span><span class="p">[</span><span class="n">name</span><span class="p">]</span>
<span class="n">kind</span> <span class="o">=</span> <span class="n">co</span><span class="o">.</span><span class="n">_local_kinds</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="k">if</span> <span class="n">index</span> <span class="ow">in</span> <span class="n">co</span><span class="o">.</span><span class="n">_cells</span>
<span class="n">cell</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="n">cell</span><span class="o">.</span><span class="n">cell_contents</span> <span class="o">=</span> <span class="n">val</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">f</span><span class="o">.</span><span class="n">_extra_locals</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="n">f</span><span class="o">.</span><span class="n">_extra_locals</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">f</span><span class="o">.</span><span class="n">_extra_locals</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</span>
<span class="k">def</span> <span class="fm">__iter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">f</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_frame</span>
<span class="n">co</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">f_code</span>
<span class="k">yield from</span> <span class="nb">iter</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">_extra_locals</span><span class="p">)</span>
<span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">name</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">co</span><span class="o">.</span><span class="n">_variable_names</span><span class="p">):</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="k">if</span> <span class="n">val</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="k">continue</span>
<span class="k">if</span> <span class="n">index</span> <span class="ow">in</span> <span class="n">co</span><span class="o">.</span><span class="n">_cells</span><span class="p">:</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">val</span><span class="o">.</span><span class="n">cell_contents</span>
<span class="k">if</span> <span class="n">val</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="k">continue</span>
<span class="k">yield</span> <span class="n">name</span>
<span class="k">def</span> <span class="fm">__contains__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">item</span><span class="p">):</span>
<span class="n">f</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_frame</span>
<span class="k">if</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">f</span><span class="o">.</span><span class="n">_extra_locals</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">True</span>
<span class="k">return</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">co</span><span class="o">.</span><span class="n">_variable_names</span>
<span class="k">def</span> <span class="fm">__len__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">f</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_frame</span>
<span class="n">co</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">f_code</span>
<span class="n">res</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">co</span><span class="o">.</span><span class="n">_variable_names</span><span class="p">):</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="k">if</span> <span class="n">val</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="k">continue</span>
<span class="k">if</span> <span class="n">index</span> <span class="ow">in</span> <span class="n">co</span><span class="o">.</span><span class="n">_cells</span><span class="p">:</span>
<span class="k">if</span> <span class="n">val</span><span class="o">.</span><span class="n">cell_contents</span> <span class="ow">is</span> <span class="n">NULL</span><span class="p">:</span>
<span class="k">continue</span>
<span class="n">res</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_extra_locals</span><span class="p">)</span> <span class="o">+</span> <span class="n">res</span>
</pre></div>
</div>
<section id="id1">
<h3><a class="toc-backref" href="#id1" role="doc-backlink">C API</a></h3>
<p><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> will be implemented roughly as follows:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">PyObject</span> <span class="o">*</span><span class="n">PyEval_GetLocals</span><span class="p">(</span><span class="n">void</span><span class="p">)</span> <span class="p">{</span>
<span class="n">PyFrameObject</span> <span class="o">*</span> <span class="o">=</span> <span class="o">...</span><span class="p">;</span> <span class="o">//</span> <span class="n">Get</span> <span class="n">the</span> <span class="n">current</span> <span class="n">frame</span><span class="o">.</span>
<span class="k">if</span> <span class="p">(</span><span class="n">frame</span><span class="o">-&gt;</span><span class="n">_locals_cache</span> <span class="o">==</span> <span class="n">NULL</span><span class="p">)</span> <span class="p">{</span>
<span class="n">frame</span><span class="o">-&gt;</span><span class="n">_locals_cache</span> <span class="o">=</span> <span class="n">PyEval_GetFrameLocals</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">PyDict_Update</span><span class="p">(</span><span class="n">frame</span><span class="o">-&gt;</span><span class="n">_locals_cache</span><span class="p">,</span> <span class="n">PyFrame_GetLocals</span><span class="p">(</span><span class="n">frame</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">frame</span><span class="o">-&gt;</span><span class="n">_locals_cache</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>As with all functions that return a borrowed reference, care must be taken to
ensure that the reference is not used beyond the lifetime of the object.</p>
</section>
</section>
<section id="implementation-notes">
<h2><a class="toc-backref" href="#implementation-notes" role="doc-backlink">Implementation Notes</a></h2>
<p>When accepted, the PEP text suggested that <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals</span></code> would start returning a
cached instance of the new write-through proxy, while the implementation sketch indicated
it would continue to return a dictionary snapshot cached on the frame instance. This
discrepancy was identified while implementing the PEP, and
<a class="reference external" href="https://github.com/python/steering-council/issues/245#issuecomment-2179005461">resolved by the Steering Council</a>
in favour of retaining the Python 3.12 behaviour of returning a dictionary snapshot
cached on the frame instance.
The PEP text has been updated accordingly.</p>
<p>During the discussions of the C API clarification, it also became apparent that the
rationale behind <code class="docutils literal notranslate"><span class="pre">locals()</span></code> being updated to return independent snapshots in
<a class="reference external" href="https://docs.python.org/3.13/glossary.html#term-optimized-scope" title="(in Python v3.13)"><span class="xref std std-term">optimized scopes</span></a> wasnt clear, as it had been inherited
from the original <a class="pep reference internal" href="../pep-0558/" title="PEP 558 Defined semantics for locals()">PEP 558</a> discussions rather than being independently covered in this
PEP. The PEP text has been updated to better cover this change, with additional updates
to the Specification and Backwards Compatibility sections to cover the impact on code
execution APIs that default to executing code in the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> namespace. Additional
motivation and rationale details have also been added to <a class="pep reference internal" href="../pep-0558/" title="PEP 558 Defined semantics for locals()">PEP 558</a>.</p>
<p>In 3.13.0, the write-through proxies did not allow deletion of even extra variables
with <code class="docutils literal notranslate"><span class="pre">del</span></code> and <code class="docutils literal notranslate"><span class="pre">pop()</span></code>. This was subsequently reported as a
<a class="reference external" href="https://github.com/python/cpython/issues/125590">compatibility regression</a>,
and <a class="reference external" href="https://github.com/python/cpython/pull/125616">resolved</a> as now described
in <a class="reference internal" href="#pep-667-f-locals-spec"><span class="std std-ref">The frame.f_locals attribute</span></a>.</p>
</section>
<section id="comparison-with-pep-558">
<h2><a class="toc-backref" href="#comparison-with-pep-558" role="doc-backlink">Comparison with PEP 558</a></h2>
<p>This PEP and <a class="pep reference internal" href="../pep-0558/" title="PEP 558 Defined semantics for locals()">PEP 558</a> shared a common goal:
to make the semantics of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> and <code class="docutils literal notranslate"><span class="pre">frame.f_locals()</span></code>
intelligible, and their operation reliable.</p>
<p>The key difference between this PEP and PEP 558 is that
PEP 558 attempted to store extra variables inside a full
internal dictionary copy of the local variables in an effort
to improve backwards compatibility with the legacy
<code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API, whereas this PEP does not (it stores
the extra local variables in a dedicated dictionary accessed
solely via the new frame proxy objects, and copies them to the
<code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> shared dict only when requested).</p>
<p>PEP 558 did not specify exactly when that internal copy was
updated, making the behavior of PEP 558 impossible to reason
about in several cases where this PEP remains well specified.</p>
<p>PEP 558 also proposed the introduction of some additional Python
scope introspection interfaces to the C API that would allow
extension modules to more easily determine whether the currently
active Python scope is optimized or not, and hence whether
the C APIs <code class="docutils literal notranslate"><span class="pre">locals()</span></code> equivalent returns a direct reference
to the frames local execution namespace or a shallow copy of
the frames local variables and nonlocal cell references.
Whether or not to add such introspection APIs is independent
of the proposed changes to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> and <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code>
and hence no such proposals have been included in this PEP.</p>
<p>PEP 558 was
<a class="pep reference internal" href="../pep-0558/#pep-withdrawal" title="PEP 558 Defined semantics for locals() § PEP Withdrawal">ultimately withdrawn</a>
in favour of this PEP.</p>
</section>
<section id="reference-implementation">
<h2><a class="toc-backref" href="#reference-implementation" role="doc-backlink">Reference Implementation</a></h2>
<p>The implementation is in development as a <a class="reference external" href="https://github.com/python/cpython/pull/115153">draft pull request on GitHub</a>.</p>
</section>
<section id="copyright">
<h2><a class="toc-backref" href="#copyright" role="doc-backlink">Copyright</a></h2>
<p>This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.</p>
</section>
</section>
<hr class="docutils" />
<p>Source: <a class="reference external" href="https://github.com/python/peps/blob/main/peps/pep-0667.rst">https://github.com/python/peps/blob/main/peps/pep-0667.rst</a></p>
<p>Last modified: <a class="reference external" href="https://github.com/python/peps/commits/main/peps/pep-0667.rst">2024-10-27 07:11:46 GMT</a></p>
</article>
<nav id="pep-sidebar">
<h2>Contents</h2>
<ul>
<li><a class="reference internal" href="#abstract">Abstract</a></li>
<li><a class="reference internal" href="#motivation">Motivation</a></li>
<li><a class="reference internal" href="#rationale">Rationale</a><ul>
<li><a class="reference internal" href="#making-the-frame-f-locals-attribute-a-write-through-proxy">Making the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute a write-through proxy</a></li>
<li><a class="reference internal" href="#making-the-locals-builtin-return-independent-snapshots">Making the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin return independent snapshots</a></li>
</ul>
</li>
<li><a class="reference internal" href="#specification">Specification</a><ul>
<li><a class="reference internal" href="#python-api">Python API</a><ul>
<li><a class="reference internal" href="#the-frame-f-locals-attribute">The <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute</a></li>
<li><a class="reference internal" href="#the-locals-builtin">The <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin</a></li>
<li><a class="reference internal" href="#the-eval-and-exec-builtins">The <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> builtins</a></li>
</ul>
</li>
<li><a class="reference internal" href="#c-api">C API</a><ul>
<li><a class="reference internal" href="#additions-to-the-pyeval-c-api">Additions to the <code class="docutils literal notranslate"><span class="pre">PyEval</span></code> C API</a></li>
<li><a class="reference internal" href="#pyframe-getlocals-c-api"><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code> C API</a></li>
<li><a class="reference internal" href="#deprecated-c-apis">Deprecated C APIs</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#summary-of-changes">Summary of Changes</a><ul>
<li><a class="reference internal" href="#python-api-changes">Python API changes</a><ul>
<li><a class="reference internal" href="#frame-f-locals-changes"><code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> changes</a></li>
<li><a class="reference internal" href="#locals-changes"><code class="docutils literal notranslate"><span class="pre">locals()</span></code> changes</a></li>
<li><a class="reference internal" href="#eval-and-exec-changes"><code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> changes</a></li>
</ul>
</li>
<li><a class="reference internal" href="#c-api-changes">C API changes</a><ul>
<li><a class="reference internal" href="#pyframe-getlocals-change"><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals</span></code> change</a></li>
<li><a class="reference internal" href="#pyeval-getlocals-change"><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals</span></code> change</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#backwards-compatibility">Backwards Compatibility</a><ul>
<li><a class="reference internal" href="#python-api-compatibility">Python API compatibility</a></li>
<li><a class="reference internal" href="#frame-f-locals-compatibility"><code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> compatibility</a><ul>
<li><a class="reference internal" href="#locals-compatibility"><code class="docutils literal notranslate"><span class="pre">locals()</span></code> compatibility</a></li>
<li><a class="reference internal" href="#impact-on-exec-and-eval">Impact on <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code></a></li>
<li><a class="reference internal" href="#impact-on-other-code-execution-apis-in-the-standard-library">Impact on other code execution APIs in the standard library</a></li>
</ul>
</li>
<li><a class="reference internal" href="#c-api-compatibility">C API compatibility</a><ul>
<li><a class="reference internal" href="#pyeval-getlocals-compatibility"><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals</span></code> compatibility</a></li>
</ul>
</li>
<li><a class="reference internal" href="#impact-on-pep-709-inlined-comprehensions">Impact on PEP 709 inlined comprehensions</a></li>
</ul>
</li>
<li><a class="reference internal" href="#implementation">Implementation</a><ul>
<li><a class="reference internal" href="#id1">C API</a></li>
</ul>
</li>
<li><a class="reference internal" href="#implementation-notes">Implementation Notes</a></li>
<li><a class="reference internal" href="#comparison-with-pep-558">Comparison with PEP 558</a></li>
<li><a class="reference internal" href="#reference-implementation">Reference Implementation</a></li>
<li><a class="reference internal" href="#copyright">Copyright</a></li>
</ul>
<br>
<a id="source" href="https://github.com/python/peps/blob/main/peps/pep-0667.rst">Page Source (GitHub)</a>
</nav>
</section>
<script src="../_static/colour_scheme.js"></script>
<script src="../_static/wrap_tables.js"></script>
<script src="../_static/sticky_banner.js"></script>
</body>
</html>