CINXE.COM

PEP 558 – Defined semantics for locals() | peps.python.org

<!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 558 – Defined semantics for locals() | peps.python.org</title> <link rel="shortcut icon" href="../_static/py.png"> <link rel="canonical" href="https://peps.python.org/pep-0558/"> <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 558 – Defined semantics for locals() | peps.python.org'> <meta property="og:description" content="The semantics of the locals() builtin have historically been underspecified and hence implementation dependent."> <meta property="og:type" content="website"> <meta property="og:url" content="https://peps.python.org/pep-0558/"> <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="The semantics of the locals() builtin have historically been underspecified and hence implementation dependent."> <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 558</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 558 – Defined semantics for locals()</h1> <dl class="rfc2822 field-list simple"> <dt class="field-odd">Author<span class="colon">:</span></dt> <dd class="field-odd">Alyssa Coghlan &lt;ncoghlan&#32;&#97;t&#32;gmail.com&gt;</dd> <dt class="field-even">BDFL-Delegate<span class="colon">:</span></dt> <dd class="field-even">Nathaniel J. Smith</dd> <dt class="field-odd">Discussions-To<span class="colon">:</span></dt> <dd class="field-odd"><a class="reference external" href="https://mail.python.org/archives/list/python-dev&#64;python.org/">Python-Dev list</a></dd> <dt class="field-even">Status<span class="colon">:</span></dt> <dd class="field-even"><abbr title="Removed from consideration by sponsor or authors">Withdrawn</abbr></dd> <dt class="field-odd">Type<span class="colon">:</span></dt> <dd class="field-odd"><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-even">Created<span class="colon">:</span></dt> <dd class="field-even">08-Sep-2017</dd> <dt class="field-odd">Python-Version<span class="colon">:</span></dt> <dd class="field-odd">3.13</dd> <dt class="field-even">Post-History<span class="colon">:</span></dt> <dd class="field-even">08-Sep-2017, 22-May-2019, 30-May-2019, 30-Dec-2019, 18-Jul-2021, 26-Aug-2021</dd> </dl> <hr class="docutils" /> <section id="contents"> <details><summary>Table of Contents</summary><ul class="simple"> <li><a class="reference internal" href="#pep-withdrawal">PEP Withdrawal</a></li> <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="#proposal">Proposal</a><ul> <li><a class="reference internal" href="#new-locals-documentation">New <code class="docutils literal notranslate"><span class="pre">locals()</span></code> documentation</a></li> <li><a class="reference internal" href="#module-scope">Module scope</a></li> <li><a class="reference internal" href="#class-scope">Class scope</a></li> <li><a class="reference internal" href="#function-scope">Function scope</a></li> </ul> </li> <li><a class="reference internal" href="#cpython-implementation-changes">CPython Implementation Changes</a><ul> <li><a class="reference internal" href="#summary-of-proposed-implementation-specific-changes">Summary of proposed implementation-specific changes</a></li> <li><a class="reference internal" href="#providing-the-updated-python-level-semantics">Providing the updated Python level semantics</a></li> <li><a class="reference internal" href="#resolving-the-issues-with-tracing-mode-behaviour">Resolving the issues with tracing mode behaviour</a></li> <li><a class="reference internal" href="#fast-locals-proxy-implementation-details">Fast locals proxy implementation details</a></li> <li><a class="reference internal" href="#changes-to-the-stable-c-api-abi">Changes to the stable C API/ABI</a></li> <li><a class="reference internal" href="#changes-to-the-public-cpython-c-api">Changes to the public CPython C API</a></li> <li><a class="reference internal" href="#reducing-the-runtime-overhead-of-trace-hooks">Reducing the runtime overhead of trace hooks</a></li> </ul> </li> <li><a class="reference internal" href="#rationale-and-design-discussion">Rationale and Design Discussion</a><ul> <li><a class="reference internal" href="#changing-locals-to-return-independent-snapshots-at-function-scope">Changing <code class="docutils literal notranslate"><span class="pre">locals()</span></code> to return independent snapshots at function scope</a></li> <li><a class="reference internal" href="#keeping-locals-as-a-snapshot-at-function-scope">Keeping <code class="docutils literal notranslate"><span class="pre">locals()</span></code> as a snapshot at function scope</a></li> <li><a class="reference internal" href="#what-happens-with-the-default-args-for-eval-and-exec">What happens with the default args for <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code>?</a></li> <li><a class="reference internal" href="#additional-considerations-for-eval-and-exec-in-optimized-scopes">Additional considerations for <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> in optimized scopes</a></li> <li><a class="reference internal" href="#retaining-the-internal-frame-value-cache">Retaining the internal frame value cache</a></li> <li><a class="reference internal" href="#changing-the-frame-api-semantics-in-regular-operation">Changing the frame API semantics in regular operation</a></li> <li><a class="reference internal" href="#continuing-to-support-storing-additional-data-on-optimised-frames">Continuing to support storing additional data on optimised frames</a></li> <li><a class="reference internal" href="#historical-semantics-at-function-scope">Historical semantics at function scope</a></li> <li><a class="reference internal" href="#proposing-several-additions-to-the-stable-c-api-abi">Proposing several additions to the stable C API/ABI</a></li> <li><a class="reference internal" href="#comparison-with-pep-667">Comparison with PEP 667</a></li> </ul> </li> <li><a class="reference internal" href="#implementation">Implementation</a></li> <li><a class="reference internal" href="#acknowledgements">Acknowledgements</a></li> <li><a class="reference internal" href="#references">References</a></li> <li><a class="reference internal" href="#copyright">Copyright</a></li> </ul> </details></section> <section id="pep-withdrawal"> <h2><a class="toc-backref" href="#pep-withdrawal" role="doc-backlink">PEP Withdrawal</a></h2> <p>In December 2021, this PEP and <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> converged on a common definition of the proposed changes to the Python level semantics of the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin (as documented in the PEP text below), with the only remaining differences being in the proposed C API changes and various internal implementation details.</p> <p>Of those remaining differences, the most significant one was that <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> at the time still proposed an immediate backwards compatibility break for the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API as soon as the PEP was accepted and implemented.</p> <p><a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> has since been changed to propose a generous deprecation period for the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API, continuing to support it in parallel with the improved semantics offered by the new <code class="docutils literal notranslate"><span class="pre">PyEval_GetFrameLocals()</span></code> API.</p> <p>Any remaining C API design concerns relate to new informational APIs that can be added at a later date if they are deemed necessary, and any potential concerns about the exact performance characteristics of the frame locals view implementation are outweighed by the availability of a viable reference implementation.</p> <p>Accordingly, this PEP has been withdrawn in favour of proceeding with <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>.</p> <p>Note: while implementing <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> it became apparent that the rationale for and impact of <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> was not entirely clear in either PEP. The Motivation and Rationale sections in this PEP have been updated accordingly (since those aspects are equally applicable to the accepted <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>).</p> </section> <section id="abstract"> <h2><a class="toc-backref" href="#abstract" role="doc-backlink">Abstract</a></h2> <p>The semantics of the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin have historically been underspecified and hence implementation dependent.</p> <p>This PEP proposes formally standardising on the behaviour of the CPython 3.10 reference implementation for most execution scopes, with some adjustments to the behaviour at function scope to make it more predictable and independent of the presence or absence of tracing functions.</p> <p>In addition, it proposes that the following functions be added to the stable Python C API/ABI:</p> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="k">typedef</span><span class="w"> </span><span class="k">enum</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">PyLocals_UNDEFINED</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">-1</span><span class="p">,</span> <span class="w"> </span><span class="n">PyLocals_DIRECT_REFERENCE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span> <span class="w"> </span><span class="n">PyLocals_SHALLOW_COPY</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span> <span class="w"> </span><span class="n">_PyLocals_ENSURE_32BIT_ENUM</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2147483647</span> <span class="p">}</span><span class="w"> </span><span class="n">PyLocals_Kind</span><span class="p">;</span> <span class="n">PyLocals_Kind</span><span class="w"> </span><span class="nf">PyLocals_GetKind</span><span class="p">();</span> <span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyLocals_Get</span><span class="p">();</span> <span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyLocals_GetCopy</span><span class="p">();</span> </pre></div> </div> <p>It also proposes the addition of several supporting functions and type definitions to the CPython C API.</p> </section> <section id="motivation"> <span id="pep-558-motivation"></span><h2><a class="toc-backref" href="#motivation" role="doc-backlink">Motivation</a></h2> <p>While the precise semantics of the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin are nominally undefined, in practice, many Python programs depend on it behaving exactly as it behaves in CPython (at least when no tracing functions are installed).</p> <p>Other implementations such as PyPy are currently replicating that behaviour, up to and including replication of local variable mutation bugs that can arise when a trace hook is installed <a class="footnote-reference brackets" href="#id21" id="id1">[1]</a>.</p> <p>While this PEP considers CPython’s current behaviour when no trace hooks are installed to be largely acceptable, it considers the current behaviour when trace hooks are installed to be problematic, as it causes bugs like <a class="footnote-reference brackets" href="#id21" id="id2">[1]</a> <em>without</em> even reliably enabling the desired functionality of allowing debuggers like <code class="docutils literal notranslate"><span class="pre">pdb</span></code> to mutate local variables <a class="footnote-reference brackets" href="#id22" id="id3">[3]</a>.</p> <p>Review of the initial PEP and the draft implementation then identified an opportunity for simplification of both the documentation and implementation of the function level <code class="docutils literal notranslate"><span class="pre">locals()</span></code> behaviour by updating it to return an independent snapshot of the function locals and closure variables on each call, rather than continuing to return the semi-dynamic intermittently updated shared copy that it has historically returned in CPython.</p> <p>Specifically, the proposal in this PEP eliminates the historical behaviour where adding a new local variable can change the behaviour of code executed with <code class="docutils literal notranslate"><span class="pre">exec()</span></code> in function scopes, even if that code runs <em>before</em> the local variable is defined.</p> <p>For 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>prints <code class="docutils literal notranslate"><span class="pre">1</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">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">x</span> <span class="o">=</span> <span class="mi">0</span> <span class="n">f</span><span class="p">()</span> </pre></div> </div> <p>prints <code class="docutils literal notranslate"><span class="pre">None</span></code> (the default value from the <code class="docutils literal notranslate"><span class="pre">.get()</span></code> call).</p> <p>With this PEP both examples would print <code class="docutils literal notranslate"><span class="pre">None</span></code>, as the call to <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and the subsequent call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> would use independent dictionary snapshots of the local variables rather than using the same shared dictionary cached on the frame object.</p> </section> <section id="proposal"> <h2><a class="toc-backref" href="#proposal" role="doc-backlink">Proposal</a></h2> <p>The expected semantics of the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin change based on the current execution scope. For this purpose, the defined scopes of execution are:</p> <ul class="simple"> <li>module scope: top-level module code, as well as any other code executed using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with a single namespace</li> <li>class scope: code in the body of a <code class="docutils literal notranslate"><span class="pre">class</span></code> statement, as well as any other code executed using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with separate local and global namespaces</li> <li>function scope: code in the body of a <code class="docutils literal notranslate"><span class="pre">def</span></code> or <code class="docutils literal notranslate"><span class="pre">async</span> <span class="pre">def</span></code> statement, or any other construct that creates an optimized code block in CPython (e.g. comprehensions, lambda functions)</li> </ul> <p>This PEP proposes elevating most of the current behaviour of the CPython reference implementation to become part of the language specification, <em>except</em> that each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> at function scope will create a new dictionary object, rather than caching a common dict instance in the frame object that each invocation will update and return.</p> <p>This PEP also proposes to largely eliminate the concept of a separate “tracing” mode from the CPython reference implementation. In releases up to and including Python 3.10, the CPython interpreter behaves differently when a trace hook has been registered in one or more threads via an implementation dependent mechanism like <code class="docutils literal notranslate"><span class="pre">sys.settrace</span></code> (<a class="footnote-reference brackets" href="#id23" id="id4">[4]</a>) in CPython’s <code class="docutils literal notranslate"><span class="pre">sys</span></code> module or <code class="docutils literal notranslate"><span class="pre">PyEval_SetTrace</span></code> (<a class="footnote-reference brackets" href="#id24" id="id5">[5]</a>) in CPython’s C API. If this PEP is accepted, then the only remaining behavioural difference when a trace hook is installed is that some optimisations in the interpreter eval loop are disabled when the tracing logic needs to run after each opcode.</p> <p>This PEP proposes changes to CPython’s behaviour at function scope that make the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin semantics when a trace hook is registered identical to those used when no trace hook is registered, while also making the related frame API semantics clearer and easier for interactive debuggers to rely on.</p> <p>The proposed elimination of tracing mode affects the semantics of frame object references obtained through other means, such as via a traceback, or via the <code class="docutils literal notranslate"><span class="pre">sys._getframe()</span></code> API, as the write-through semantics needed for trace hook support are always provided by the <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> attribute on frame objects, rather than being runtime state dependent.</p> <section id="new-locals-documentation"> <h3><a class="toc-backref" href="#new-locals-documentation" role="doc-backlink">New <code class="docutils literal notranslate"><span class="pre">locals()</span></code> documentation</a></h3> <p>The heart of this proposal is to revise the documentation for the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin to read as follows:</p> <blockquote> <div>Return a mapping object representing the current local symbol table, with variable names as the keys, and their currently bound references as the values.<p>At module scope, as well as when using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with a single namespace, this function returns the same namespace as <code class="docutils literal notranslate"><span class="pre">globals()</span></code>.</p> <p>At class scope, it returns the namespace that will be passed to the metaclass constructor.</p> <p>When using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with separate local and global namespaces, it returns the local namespace passed in to the function call.</p> <p>In all of the above cases, each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in a given frame of execution will return the <em>same</em> mapping object. Changes made through the mapping object returned from <code class="docutils literal notranslate"><span class="pre">locals()</span></code> will be visible as bound, rebound, or deleted local variables, and binding, rebinding, or deleting local variables will immediately affect the contents of the returned mapping object.</p> <p>At function scope (including for generators and coroutines), each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> instead returns a fresh dictionary containing the current bindings of the function’s local variables and any nonlocal cell references. In this case, name binding changes made via the returned dict are <em>not</em> written back to the corresponding local variables or nonlocal cell references, and binding, rebinding, or deleting local variables and nonlocal cell references does <em>not</em> affect the contents of previously returned dictionaries.</p> </div></blockquote> <p>There would also be a <code class="docutils literal notranslate"><span class="pre">versionchanged</span></code> note for the release making this change:</p> <blockquote> <div>In prior versions, the semantics of mutating the mapping object returned from <code class="docutils literal notranslate"><span class="pre">locals()</span></code> were formally undefined. In CPython specifically, the mapping returned at function scope could be implicitly refreshed by other operations, such as calling <code class="docutils literal notranslate"><span class="pre">locals()</span></code> again, or the interpreter implicitly invoking a Python level trace function. Obtaining the legacy CPython behaviour now requires explicit calls to update the initially returned dictionary with the results of subsequent calls to <code class="docutils literal notranslate"><span class="pre">locals()</span></code>.</div></blockquote> <p>For reference, the current documentation of this builtin reads as follows:</p> <blockquote> <div>Update and return a dictionary representing the current local symbol table. Free variables are returned by locals() when it is called in function blocks, but not in class blocks.<p>Note: The contents of this dictionary should not be modified; changes may not affect the values of local and free variables used by the interpreter.</p> </div></blockquote> <p>(In other words: the status quo is that the semantics and behaviour of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> are formally implementation defined, whereas the proposed state after this PEP is that the only implementation defined behaviour will be that associated with whether or not the implementation emulates the CPython frame API, with the behaviour in all other cases being defined by the language and library references)</p> </section> <section id="module-scope"> <h3><a class="toc-backref" href="#module-scope" role="doc-backlink">Module scope</a></h3> <p>At module scope, as well as when using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with a single namespace, <code class="docutils literal notranslate"><span class="pre">locals()</span></code> must return the same object as <code class="docutils literal notranslate"><span class="pre">globals()</span></code>, which must be the actual execution namespace (available as <code class="docutils literal notranslate"><span class="pre">inspect.currentframe().f_locals</span></code> in implementations that provide access to frame objects).</p> <p>Variable assignments during subsequent code execution in the same scope must dynamically change the contents of the returned mapping, and changes to the returned mapping must change the values bound to local variable names in the execution environment.</p> <p>To capture this expectation as part of the language specification, the following paragraph will be added to the documentation for <code class="docutils literal notranslate"><span class="pre">locals()</span></code>:</p> <blockquote> <div>At module scope, as well as when using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with a single namespace, this function returns the same namespace as <code class="docutils literal notranslate"><span class="pre">globals()</span></code>.</div></blockquote> <p>This part of the proposal does not require any changes to the reference implementation - it is standardisation of the current behaviour.</p> </section> <section id="class-scope"> <h3><a class="toc-backref" href="#class-scope" role="doc-backlink">Class scope</a></h3> <p>At class scope, as well as when using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with separate global and local namespaces, <code class="docutils literal notranslate"><span class="pre">locals()</span></code> must return the specified local namespace (which may be supplied by the metaclass <code class="docutils literal notranslate"><span class="pre">__prepare__</span></code> method in the case of classes). As for module scope, this must be a direct reference to the actual execution namespace (available as <code class="docutils literal notranslate"><span class="pre">inspect.currentframe().f_locals</span></code> in implementations that provide access to frame objects).</p> <p>Variable assignments during subsequent code execution in the same scope must change the contents of the returned mapping, and changes to the returned mapping must change the values bound to local variable names in the execution environment.</p> <p>The mapping returned by <code class="docutils literal notranslate"><span class="pre">locals()</span></code> will <em>not</em> be used as the actual class namespace underlying the defined class (the class creation process will copy the contents to a fresh dictionary that is only accessible by going through the class machinery).</p> <p>For nested classes defined inside a function, any nonlocal cells referenced from the class scope are <em>not</em> included in the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> mapping.</p> <p>To capture this expectation as part of the language specification, the following two paragraphs will be added to the documentation for <code class="docutils literal notranslate"><span class="pre">locals()</span></code>:</p> <blockquote> <div>When using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code> with separate local and global namespaces, [this function] returns the given local namespace.<p>At class scope, it returns the namespace that will be passed to the metaclass constructor.</p> </div></blockquote> <p>This part of the proposal does not require any changes to the reference implementation - it is standardisation of the current behaviour.</p> </section> <section id="function-scope"> <h3><a class="toc-backref" href="#function-scope" role="doc-backlink">Function scope</a></h3> <p>At function scope, interpreter implementations are granted significant freedom to optimise local variable access, and hence are NOT required to permit arbitrary modification of local and nonlocal variable bindings through the mapping returned from <code class="docutils literal notranslate"><span class="pre">locals()</span></code>.</p> <p>Historically, this leniency has been described in the language specification with the words “The contents of this dictionary should not be modified; changes may not affect the values of local and free variables used by the interpreter.”</p> <p>This PEP proposes to change that text to instead say:</p> <blockquote> <div>At function scope (including for generators and coroutines), each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> instead returns a fresh dictionary containing the current bindings of the function’s local variables and any nonlocal cell references. In this case, name binding changes made via the returned dict are <em>not</em> written back to the corresponding local variables or nonlocal cell references, and binding, rebinding, or deleting local variables and nonlocal cell references does <em>not</em> affect the contents of previously returned dictionaries.</div></blockquote> <p>This part of the proposal <em>does</em> require changes to the CPython reference implementation, as CPython currently returns a shared mapping object that may be implicitly refreshed by additional calls to <code class="docutils literal notranslate"><span class="pre">locals()</span></code>, and the “write back” strategy currently used to support namespace changes from trace functions also doesn’t comply with it (and causes the quirky behavioural problems mentioned in the Motivation above).</p> </section> </section> <section id="cpython-implementation-changes"> <h2><a class="toc-backref" href="#cpython-implementation-changes" role="doc-backlink">CPython Implementation Changes</a></h2> <section id="summary-of-proposed-implementation-specific-changes"> <h3><a class="toc-backref" href="#summary-of-proposed-implementation-specific-changes" role="doc-backlink">Summary of proposed implementation-specific changes</a></h3> <ul class="simple"> <li>Changes are made as necessary to provide the updated Python level semantics</li> <li>Two new functions are added to the stable ABI to replicate the updated behaviour of the Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin:</li> </ul> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyLocals_Get</span><span class="p">();</span> <span class="n">PyLocals_Kind</span><span class="w"> </span><span class="nf">PyLocals_GetKind</span><span class="p">();</span> </pre></div> </div> <ul class="simple"> <li>One new function is added to the stable ABI to efficiently get a snapshot of the local namespace in the running frame:</li> </ul> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyLocals_GetCopy</span><span class="p">();</span> </pre></div> </div> <ul class="simple"> <li>Corresponding frame accessor functions for these new public APIs are added to the CPython frame C API</li> <li>On optimised frames, the Python level <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> API will return dynamically created read/write proxy objects that directly access the frame’s local and closure variable storage. To provide interoperability with the existing <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API, the proxy objects will continue to use the C level frame locals data storage field to hold a value cache that also allows for storage of arbitrary additional keys. Additional details on the expected behaviour of these fast locals proxy objects are covered below.</li> <li>No C API function is added to get access to a mutable mapping for the local namespace. Instead, <code class="docutils literal notranslate"><span class="pre">PyObject_GetAttrString(frame,</span> <span class="pre">&quot;f_locals&quot;)</span></code> is used, the same API as is used in Python code.</li> <li><code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> remains supported and does not emit a programmatic warning, but will be deprecated in the documentation in favour of the new APIs that don’t rely on returning a borrowed reference</li> <li><code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals()</span></code> and <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocalsWithError()</span></code> remain supported and do not emit a programmatic warning, but will be deprecated in the documentation in favour of the new APIs that don’t require direct access to the internal data storage layout of frame objects</li> <li><code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> always raises <code class="docutils literal notranslate"><span class="pre">RuntimeError()</span></code>, indicating that <code class="docutils literal notranslate"><span class="pre">PyObject_GetAttrString(frame,</span> <span class="pre">&quot;f_locals&quot;)</span></code> should be used to obtain a mutable read/write mapping for the local variables.</li> <li>The trace hook implementation will no longer call <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals()</span></code> implicitly. The version porting guide will recommend migrating to <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals()</span></code> for read-only access and <code class="docutils literal notranslate"><span class="pre">PyObject_GetAttrString(frame,</span> <span class="pre">&quot;f_locals&quot;)</span></code> for read/write access.</li> </ul> </section> <section id="providing-the-updated-python-level-semantics"> <h3><a class="toc-backref" href="#providing-the-updated-python-level-semantics" role="doc-backlink">Providing the updated Python level semantics</a></h3> <p>The implementation of the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin is modified to return a distinct copy of the local namespace for optimised frames, rather than a direct reference to the internal frame value cache updated by the <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals()</span></code> C API and returned by the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> C API.</p> </section> <section id="resolving-the-issues-with-tracing-mode-behaviour"> <h3><a class="toc-backref" href="#resolving-the-issues-with-tracing-mode-behaviour" role="doc-backlink">Resolving the issues with tracing mode behaviour</a></h3> <p>The current cause of CPython’s tracing mode quirks (both the side effects from simply installing a tracing function and the fact that writing values back to function locals only works for the specific function being traced) is the way that locals mutation support for trace hooks is currently implemented: the <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast</span></code> function.</p> <p>When a trace function is installed, CPython currently does the following for function frames (those where the code object uses “fast locals” semantics):</p> <ol class="arabic simple"> <li>Calls <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals</span></code> to update the frame value cache</li> <li>Calls the trace hook (with tracing of the hook itself disabled)</li> <li>Calls <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast</span></code> to capture any changes made to the frame value cache</li> </ol> <p>This approach is problematic for a few different reasons:</p> <ul class="simple"> <li>Even if the trace function doesn’t mutate the value cache, the final step resets any cell references back to the state they were in before the trace function was called (this is the root cause of the bug report in <a class="footnote-reference brackets" href="#id21" id="id6">[1]</a>)</li> <li>If the trace function <em>does</em> mutate the value cache, but then does something that causes the value cache to be refreshed from the frame, those changes are lost (this is one aspect of the bug report in <a class="footnote-reference brackets" href="#id22" id="id7">[3]</a>)</li> <li>If the trace function attempts to mutate the local variables of a frame other than the one being traced (e.g. <code class="docutils literal notranslate"><span class="pre">frame.f_back.f_locals</span></code>), those changes will almost certainly be lost (this is another aspect of the bug report in <a class="footnote-reference brackets" href="#id22" id="id8">[3]</a>)</li> <li>If a reference to the frame value cache (e.g. retrieved via <code class="docutils literal notranslate"><span class="pre">locals()</span></code>) is passed to another function, and <em>that</em> function mutates the value cache, then those changes <em>may</em> be written back to the execution frame <em>if</em> a trace hook is installed</li> </ul> <p>The proposed resolution to this problem is to take advantage of the fact that whereas functions typically access their <em>own</em> namespace using the language defined <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin, trace functions necessarily use the implementation dependent <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> interface, as a frame reference is what gets passed to hook implementations.</p> <p>Instead of being a direct reference to the internal frame value cache historically returned by the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin, the Python level <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> will be updated to instead return instances of a dedicated fast locals proxy type that writes and reads values directly to and from the fast locals array on the underlying frame. Each access of the attribute produces a new instance of the proxy (so creating proxy instances is intentionally a cheap operation).</p> <p>Despite the new proxy type becoming the preferred way to access local variables on optimised frames, the internal value cache stored on the frame is still retained for two key purposes:</p> <ul class="simple"> <li>maintaining backwards compatibility for and interoperability with the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> C API</li> <li>providing storage space for additional keys that don’t have slots in the fast locals array (e.g. the <code class="docutils literal notranslate"><span class="pre">__return__</span></code> and <code class="docutils literal notranslate"><span class="pre">__exception__</span></code> keys set by <code class="docutils literal notranslate"><span class="pre">pdb</span></code> when tracing code execution for debugging purposes)</li> </ul> <p>With the changes in this PEP, this internal frame value cache is no longer directly accessible from Python code (whereas historically it was both returned by the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin and available as the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute). Instead, the value cache is only accessible via the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> C API and by directly accessing the internal storage of a frame object.</p> <p>Fast locals proxy objects and the internal frame value cache returned by <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> offer the following behavioural guarantees:</p> <ul class="simple"> <li>changes made via a fast locals proxy will be immediately visible to the frame itself, to other fast locals proxy objects for the same frame, and in the internal value cache stored on the frame (it is this last point that provides <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> interoperability)</li> <li>changes made directly to the internal frame value cache will never be visible to the frame itself, and will only be reliably visible via fast locals proxies for the same frame if the change relates to extra variables that don’t have slots in the frame’s fast locals array</li> <li>changes made by executing code in the frame will be immediately visible to all fast locals proxy objects for that frame (both existing proxies and newly created ones). Visibility in the internal frame value cache cache returned by <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> is subject to the cache update guidelines discussed in the next section</li> </ul> <p>As a result of these points, only code using <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code>, <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code>, or <code class="docutils literal notranslate"><span class="pre">PyLocals_GetCopy()</span></code> will need to be concerned about the frame value cache potentially becoming stale. Code using the new frame fast locals proxy API (whether from Python or from C) will always see the live state of the frame.</p> </section> <section id="fast-locals-proxy-implementation-details"> <h3><a class="toc-backref" href="#fast-locals-proxy-implementation-details" role="doc-backlink">Fast locals proxy implementation details</a></h3> <p>Each fast locals proxy instance has a single internal attribute that is not exposed as part of the Python runtime API:</p> <ul class="simple"> <li><em>frame</em>: the underlying optimised frame that the proxy provides access to</li> </ul> <p>In addition, proxy instances use and update the following attributes stored on the underlying frame or code object:</p> <ul class="simple"> <li><em>_name_to_offset_mapping</em>: a hidden mapping from variable names to fast local storage offsets. This mapping is lazily initialized on the first frame read or write access through a fast locals proxy, rather than being eagerly populated as soon as the first fast locals proxy is created. Since the mapping is identical for all frames running a given code object, a single copy is stored on the code object, rather than each frame object populating its own mapping</li> <li><em>locals</em>: the internal frame value cache returned by the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> C API and updated by the <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals()</span></code> C API. This is the mapping that the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin returns in Python 3.10 and earlier.</li> </ul> <p><code class="docutils literal notranslate"><span class="pre">__getitem__</span></code> operations on the proxy will populate the <code class="docutils literal notranslate"><span class="pre">_name_to_offset_mapping</span></code> on the code object (if it is not already populated), and then either return the relevant value (if the key is found in either the <code class="docutils literal notranslate"><span class="pre">_name_to_offset_mapping</span></code> mapping or the internal frame value cache), or else raise <code class="docutils literal notranslate"><span class="pre">KeyError</span></code>. Variables that are defined on the frame but not currently bound also raise <code class="docutils literal notranslate"><span class="pre">KeyError</span></code> (just as they’re omitted from the result of <code class="docutils literal notranslate"><span class="pre">locals()</span></code>).</p> <p>As the frame storage is always accessed directly, the proxy will automatically pick up name binding and unbinding operations that take place as the function executes. The internal value cache is implicitly updated when individual variables are read from the frame state (including for containment checks, which need to check if the name is currently bound or unbound).</p> <p>Similarly, <code class="docutils literal notranslate"><span class="pre">__setitem__</span></code> and <code class="docutils literal notranslate"><span class="pre">__delitem__</span></code> operations on the proxy will directly affect the corresponding fast local or cell reference on the underlying frame, ensuring that changes are immediately visible to the running Python code, rather than needing to be written back to the runtime storage at some later time. Such changes are also immediately written to the internal frame value cache to make them visible to users of the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> C API.</p> <p>Keys that are not defined as local or closure variables on the underlying frame are still written to the internal value cache on optimised frames. This allows utilities like <code class="docutils literal notranslate"><span class="pre">pdb</span></code> (which writes <code class="docutils literal notranslate"><span class="pre">__return__</span></code> and <code class="docutils literal notranslate"><span class="pre">__exception__</span></code> values into the frame’s <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> mapping) to continue working as they always have. These additional keys that do not correspond to a local or closure variable on the frame will be left alone by future cache sync operations. Using the frame value cache to store these extra keys (rather than defining a new mapping that holds only the extra keys) provides full interoperability with the existing <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API (since users of either API will see extra keys added by users of either API, rather than users of the new fast locals proxy API only seeing keys added via that API).</p> <p>An additional benefit of storing only the variable value cache on the frame (rather than storing an instance of the proxy type), is that it avoids creating a reference cycle from the frame back to itself, so the frame will only be kept alive if another object retains a reference to a proxy instance.</p> <p>Note: calling the <code class="docutils literal notranslate"><span class="pre">proxy.clear()</span></code> method has a similarly broad impact as calling <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> on an empty frame value cache in earlier versions. Not only will the frame local variables be cleared, but also any cell variables accessible from the frame (whether those cells are owned by the frame itself or by an outer frame). This <em>can</em> clear a class’s <code class="docutils literal notranslate"><span class="pre">__class__</span></code> cell if called on the frame of a method that uses the zero-arg <code class="docutils literal notranslate"><span class="pre">super()</span></code> construct (or otherwise references <code class="docutils literal notranslate"><span class="pre">__class__</span></code>). This exceeds the scope of calling <code class="docutils literal notranslate"><span class="pre">frame.clear()</span></code>, as that only drop’s the frame’s references to cell variables, it doesn’t clear the cells themselves. This PEP could be a potential opportunity to narrow the scope of attempts to clear the frame variables directly by leaving cells belonging to outer frames alone, and only clearing local variables and cells belonging directly to the frame underlying the proxy (this issue affects <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> as well, as the question relates to the handling of cell variables, and is entirely independent of the internal frame value cache).</p> </section> <section id="changes-to-the-stable-c-api-abi"> <h3><a class="toc-backref" href="#changes-to-the-stable-c-api-abi" role="doc-backlink">Changes to the stable C API/ABI</a></h3> <p>Unlike Python code, extension module functions that call in to the Python C API can be called from any kind of Python scope. This means it isn’t obvious from the context whether <code class="docutils literal notranslate"><span class="pre">locals()</span></code> will return a snapshot or not, as it depends on the scope of the calling Python code, not the C code itself.</p> <p>This means it is desirable to offer C APIs that give predictable, scope independent, behaviour. However, it is also desirable to allow C code to exactly mimic the behaviour of Python code at the same scope.</p> <p>To enable mimicking the behaviour of Python code, the stable C ABI would gain the following new functions:</p> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyLocals_Get</span><span class="p">();</span> <span class="n">PyLocals_Kind</span><span class="w"> </span><span class="nf">PyLocals_GetKind</span><span class="p">();</span> </pre></div> </div> <p><code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> is directly equivalent to the Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin. It returns a new reference to the local namespace mapping for the active Python frame at module and class scope, and when using <code class="docutils literal notranslate"><span class="pre">exec()</span></code> or <code class="docutils literal notranslate"><span class="pre">eval()</span></code>. It returns a shallow copy of the active namespace at function/coroutine/generator scope.</p> <p><code class="docutils literal notranslate"><span class="pre">PyLocals_GetKind()</span></code> returns a value from the newly defined <code class="docutils literal notranslate"><span class="pre">PyLocals_Kind</span></code> enum, with the following options being available:</p> <ul class="simple"> <li><code class="docutils literal notranslate"><span class="pre">PyLocals_DIRECT_REFERENCE</span></code>: <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> returns a direct reference to the local namespace for the running frame.</li> <li><code class="docutils literal notranslate"><span class="pre">PyLocals_SHALLOW_COPY</span></code>: <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> returns a shallow copy of the local namespace for the running frame.</li> <li><code class="docutils literal notranslate"><span class="pre">PyLocals_UNDEFINED</span></code>: an error occurred (e.g. no active Python thread state). A Python exception will be set if this value is returned.</li> </ul> <p>Since the enum is used in the stable ABI, an additional 31-bit value is set to ensure that it is safe to cast arbitrary signed 32-bit signed integers to <code class="docutils literal notranslate"><span class="pre">PyLocals_Kind</span></code> values.</p> <p>This query API allows extension module code to determine the potential impact of mutating the mapping returned by <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> without needing access to the details of the running frame object. Python code gets equivalent information visually through lexical scoping (as covered in the new <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin documentation).</p> <p>To allow extension module code to behave consistently regardless of the active Python scope, the stable C ABI would gain the following new function:</p> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyLocals_GetCopy</span><span class="p">();</span> </pre></div> </div> <p><code class="docutils literal notranslate"><span class="pre">PyLocals_GetCopy()</span></code> returns a new dict instance populated from the current locals namespace. Roughly equivalent to <code class="docutils literal notranslate"><span class="pre">dict(locals())</span></code> in Python code, but avoids the double-copy in the case where <code class="docutils literal notranslate"><span class="pre">locals()</span></code> already returns a shallow copy. Akin to the following code, but doesn’t assume there will only ever be two kinds of locals result:</p> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="n">locals</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">PyLocals_Get</span><span class="p">();</span> <span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">PyLocals_GetKind</span><span class="p">()</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">PyLocals_DIRECT_REFERENCE</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">locals</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">PyDict_Copy</span><span class="p">(</span><span class="n">locals</span><span class="p">);</span> <span class="p">}</span> </pre></div> </div> <p>The existing <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API will retain its existing behaviour in CPython (mutable locals at class and module scope, shared dynamic snapshot otherwise). However, its documentation will be updated to note that the conditions under which the shared dynamic snapshot get updated have changed.</p> <p>The <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> documentation will also be updated to recommend replacing usage of this API with whichever of the new APIs is most appropriate for the use case:</p> <ul class="simple"> <li>Use <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> (optionally combined with <code class="docutils literal notranslate"><span class="pre">PyDictProxy_New()</span></code>) for read-only access to the current locals namespace. This form of usage will need to be aware that the copy may go stale in optimised frames.</li> <li>Use <code class="docutils literal notranslate"><span class="pre">PyLocals_GetCopy()</span></code> for a regular mutable dict that contains a copy of the current locals namespace, but has no ongoing connection to the active frame.</li> <li>Use <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> to exactly match the semantics of the Python level <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin.</li> <li>Query <code class="docutils literal notranslate"><span class="pre">PyLocals_GetKind()</span></code> explicitly to implement custom handling (e.g. raising a meaningful exception) for scopes where <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> would return a shallow copy rather than granting read/write access to the locals namespace.</li> <li>Use implementation specific APIs (e.g. <code class="docutils literal notranslate"><span class="pre">PyObject_GetAttrString(frame,</span> <span class="pre">&quot;f_locals&quot;)</span></code>) if read/write access to the frame is required and <code class="docutils literal notranslate"><span class="pre">PyLocals_GetKind()</span></code> returns something other than <code class="docutils literal notranslate"><span class="pre">PyLocals_DIRECT_REFERENCE</span></code>.</li> </ul> </section> <section id="changes-to-the-public-cpython-c-api"> <h3><a class="toc-backref" href="#changes-to-the-public-cpython-c-api" role="doc-backlink">Changes to the public CPython C API</a></h3> <p>The existing <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API returns a borrowed reference, which means it cannot be updated to return the new shallow copies at function scope. Instead, it will continue to return a borrowed reference to an internal dynamic snapshot stored on the frame object. This shared mapping will behave similarly to the existing shared mapping in Python 3.10 and earlier, but the exact conditions under which it gets refreshed will be different. Specifically, it will be updated only in the following circumstance:</p> <ul class="simple"> <li>any call to <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code>, <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code>, <code class="docutils literal notranslate"><span class="pre">PyLocals_GetCopy()</span></code>, or the Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin while the frame is running</li> <li>any call to <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals()</span></code>, <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocalsCopy()</span></code>, <code class="docutils literal notranslate"><span class="pre">_PyFrame_BorrowLocals()</span></code>, <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals()</span></code>, or <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocalsWithError()</span></code> for the frame</li> <li>any operation on a fast locals proxy object that updates the shared mapping as part of its implementation. In the initial reference implementation, those operations are those that are intrinsically <code class="docutils literal notranslate"><span class="pre">O(n)</span></code> operations (<code class="docutils literal notranslate"><span class="pre">len(flp)</span></code>, mapping comparison, <code class="docutils literal notranslate"><span class="pre">flp.copy()</span></code> and rendering as a string), as well as those that refresh the cache entries for individual keys.</li> </ul> <p>Requesting a fast locals proxy will <em>not</em> implicitly update the shared dynamic snapshot, and the CPython trace hook handling will no longer implicitly update it either.</p> <p>(Note: even though <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> is part of the stable C API/ABI, the specifics of when the namespace it returns gets refreshed are still an interpreter implementation detail)</p> <p>The additions to the public CPython C API are the frame level enhancements needed to support the stable C API/ABI updates:</p> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="n">PyLocals_Kind</span><span class="w"> </span><span class="nf">PyFrame_GetLocalsKind</span><span class="p">(</span><span class="n">frame</span><span class="p">);</span> <span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyFrame_GetLocals</span><span class="p">(</span><span class="n">frame</span><span class="p">);</span> <span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">PyFrame_GetLocalsCopy</span><span class="p">(</span><span class="n">frame</span><span class="p">);</span> <span class="n">PyObject</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nf">_PyFrame_BorrowLocals</span><span class="p">(</span><span class="n">frame</span><span class="p">);</span> </pre></div> </div> <p><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocalsKind(frame)</span></code> is the underlying API for <code class="docutils literal notranslate"><span class="pre">PyLocals_GetKind()</span></code>.</p> <p><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocals(frame)</span></code> is the underlying API for <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code>.</p> <p><code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocalsCopy(frame)</span></code> is the underlying API for <code class="docutils literal notranslate"><span class="pre">PyLocals_GetCopy()</span></code>.</p> <p><code class="docutils literal notranslate"><span class="pre">_PyFrame_BorrowLocals(frame)</span></code> is the underlying API for <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code>. The underscore prefix is intended to discourage use and to indicate that code using it is unlikely to be portable across implementations. However, it is documented and visible to the linker in order to avoid having to access the internals of the frame struct from the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> implementation.</p> <p>The <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> function will be changed to always emit <code class="docutils literal notranslate"><span class="pre">RuntimeError</span></code>, explaining that it is no longer a supported operation, and affected code should be updated to use <code class="docutils literal notranslate"><span class="pre">PyObject_GetAttrString(frame,</span> <span class="pre">&quot;f_locals&quot;)</span></code> to obtain a read/write proxy instead.</p> <p>In addition to the above documented interfaces, the draft reference implementation also exposes the following undocumented interfaces:</p> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="n">PyTypeObject</span><span class="w"> </span><span class="n">_PyFastLocalsProxy_Type</span><span class="p">;</span> <span class="cp">#define _PyFastLocalsProxy_CheckExact(self) Py_IS_TYPE(op, &amp;_PyFastLocalsProxy_Type)</span> </pre></div> </div> <p>This type is what the reference implementation actually returns from <code class="docutils literal notranslate"><span class="pre">PyObject_GetAttrString(frame,</span> <span class="pre">&quot;f_locals&quot;)</span></code> for optimized frames (i.e. when <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocalsKind()</span></code> returns <code class="docutils literal notranslate"><span class="pre">PyLocals_SHALLOW_COPY</span></code>).</p> </section> <section id="reducing-the-runtime-overhead-of-trace-hooks"> <h3><a class="toc-backref" href="#reducing-the-runtime-overhead-of-trace-hooks" role="doc-backlink">Reducing the runtime overhead of trace hooks</a></h3> <p>As noted in <a class="footnote-reference brackets" href="#id28" id="id9">[9]</a>, the implicit call to <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals()</span></code> in the Python trace hook support isn’t free, and could be rendered unnecessary if the frame proxy read values directly from the frame instead of getting them from the mapping.</p> <p>As the new frame locals proxy type doesn’t require separate data refresh steps, this PEP incorporates Victor Stinner’s proposal to no longer implicitly call <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocalsWithError()</span></code> before calling trace hooks implemented in Python.</p> <p>Code using the new fast locals proxy objects will have the dynamic locals snapshot implicitly refreshed when accessing methods that need it, while code using the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API will implicitly refresh it when making that call.</p> <p>The PEP necessarily also drops the implicit call to <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> when returning from a trace hook, as that API now always raises an exception.</p> </section> </section> <section id="rationale-and-design-discussion"> <h2><a class="toc-backref" href="#rationale-and-design-discussion" role="doc-backlink">Rationale and Design Discussion</a></h2> <section id="changing-locals-to-return-independent-snapshots-at-function-scope"> <h3><a class="toc-backref" href="#changing-locals-to-return-independent-snapshots-at-function-scope" role="doc-backlink">Changing <code class="docutils literal notranslate"><span class="pre">locals()</span></code> to return independent snapshots at function scope</a></h3> <p>The <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin is a required part of the language, and in the reference implementation it has historically returned a mutable mapping with the following characteristics:</p> <ul class="simple"> <li>each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> returns the <em>same</em> mapping object</li> <li>for namespaces where <code class="docutils literal notranslate"><span class="pre">locals()</span></code> returns a reference to something other than the actual local execution namespace, each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> updates the mapping object with the current state of the local variables and any referenced nonlocal cells</li> <li>changes to the returned mapping <em>usually</em> aren’t written back to the local variable bindings or the nonlocal cell references, but write backs can be triggered by doing one of the following:<ul> <li>installing a Python level trace hook (write backs then happen whenever the trace hook is called)</li> <li>running a function level wildcard import (requires bytecode injection in Py3)</li> <li>running an <code class="docutils literal notranslate"><span class="pre">exec</span></code> statement in the function’s scope (Py2 only, since <code class="docutils literal notranslate"><span class="pre">exec</span></code> became an ordinary builtin in Python 3)</li> </ul> </li> </ul> <p>Originally this PEP proposed to retain the first two of these properties, while changing the third in order to address the outright behaviour bugs that it can cause.</p> <p>In <a class="footnote-reference brackets" href="#id26" id="id10">[7]</a> Nathaniel Smith made a persuasive case that we could make the behaviour of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> at function scope substantially less confusing by retaining only the second property and having each call to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> at function scope return an <em>independent</em> snapshot of the local variables and closure references rather than updating an implicitly shared snapshot.</p> <p>As this revised design also made the implementation markedly easier to follow, the PEP was updated to propose this change in behaviour, rather than retaining the historical shared snapshot.</p> </section> <section id="keeping-locals-as-a-snapshot-at-function-scope"> <h3><a class="toc-backref" href="#keeping-locals-as-a-snapshot-at-function-scope" role="doc-backlink">Keeping <code class="docutils literal notranslate"><span class="pre">locals()</span></code> as a snapshot at function scope</a></h3> <p>As discussed in <a class="footnote-reference brackets" href="#id26" id="id11">[7]</a>, it would theoretically be possible to change the semantics of the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin to return the write-through proxy at function scope, rather than switching it to return independent snapshots.</p> <p>This PEP doesn’t (and won’t) propose this as it’s a backwards incompatible change in practice, even though code that relies on the current behaviour is technically operating in an undefined area of the language specification.</p> <p>Consider the following code snippet:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">example</span><span class="p">():</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">1</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">2</span> <span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> </pre></div> </div> <p>Even with a trace hook installed, that function will consistently print <code class="docutils literal notranslate"><span class="pre">1</span></code> on the current reference interpreter implementation:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="n">example</span><span class="p">()</span> <span class="go">1</span> <span class="gp">&gt;&gt;&gt; </span><span class="kn">import</span> <span class="nn">sys</span> <span class="gp">&gt;&gt;&gt; </span><span class="k">def</span> <span class="nf">basic_hook</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">):</span> <span class="gp">... </span> <span class="k">return</span> <span class="n">basic_hook</span> <span class="gp">...</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">sys</span><span class="o">.</span><span class="n">settrace</span><span class="p">(</span><span class="n">basic_hook</span><span class="p">)</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">example</span><span class="p">()</span> <span class="go">1</span> </pre></div> </div> <p>Similarly, <code class="docutils literal notranslate"><span class="pre">locals()</span></code> can be passed to the <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code> builtins at function scope (either explicitly or implicitly) without risking unexpected rebinding of local variables or closure references.</p> <p>Provoking the reference interpreter into incorrectly mutating the local variable state requires a more complex setup where a nested function closes over a variable being rebound in the outer function, and due to the use of either threads, generators, or coroutines, it’s possible for a trace function to start running for the nested function before the rebinding operation in the outer function, but finish running after the rebinding operation has taken place (in which case the rebinding will be reverted, which is the bug reported in <a class="footnote-reference brackets" href="#id21" id="id12">[1]</a>).</p> <p>In addition to preserving the de facto semantics which have been in place since <a class="pep reference internal" href="../pep-0227/" title="PEP 227 – Statically Nested Scopes">PEP 227</a> introduced nested scopes in Python 2.1, the other benefit of restricting the write-through proxy support to the implementation-defined frame object API is that it means that only interpreter implementations which emulate the full frame API need to offer the write-through capability at all, and that JIT-compiled implementations only need to enable it when a frame introspection API is invoked, or a trace hook is installed, not whenever <code class="docutils literal notranslate"><span class="pre">locals()</span></code> is accessed at function scope.</p> <p>Returning snapshots from <code class="docutils literal notranslate"><span class="pre">locals()</span></code> at function scope also means that static analysis for function level code will be more reliable, as only access to the frame machinery will allow rebinding of local and nonlocal variable references in a way that is hidden from static analysis.</p> </section> <section id="what-happens-with-the-default-args-for-eval-and-exec"> <h3><a class="toc-backref" href="#what-happens-with-the-default-args-for-eval-and-exec" role="doc-backlink">What happens with the default args for <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code>?</a></h3> <p>These are formally defined as inheriting <code class="docutils literal notranslate"><span class="pre">globals()</span></code> and <code class="docutils literal notranslate"><span class="pre">locals()</span></code> from the calling scope by default.</p> <p>There isn’t any need for the PEP to change these defaults, so it doesn’t, and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code> will start running in a shallow copy of the local namespace when that is what <code class="docutils literal notranslate"><span class="pre">locals()</span></code> returns.</p> <p>This behaviour will have potential performance implications, especially for functions with large numbers of local variables (e.g. if these functions are called in a loop, calling <code class="docutils literal notranslate"><span class="pre">globals()</span></code> and <code class="docutils literal notranslate"><span class="pre">locals()</span></code> once before the loop and then passing the namespace into the function explicitly will give the same semantics and performance characteristics as the status quo, whereas relying on the implicit default would create a new shallow copy of the local namespace on each iteration).</p> <p>(Note: the reference implementation draft PR has updated the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> and <code class="docutils literal notranslate"><span class="pre">vars()</span></code>, <code class="docutils literal notranslate"><span class="pre">eval()</span></code>, and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> builtins to use <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code>. The <code class="docutils literal notranslate"><span class="pre">dir()</span></code> builtin still uses <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code>, since it’s only using it to make a list from the keys).</p> </section> <section id="additional-considerations-for-eval-and-exec-in-optimized-scopes"> <span id="pep-558-exec-eval-impact"></span><h3><a class="toc-backref" href="#additional-considerations-for-eval-and-exec-in-optimized-scopes" role="doc-backlink">Additional considerations for <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> in optimized scopes</a></h3> <p>Note: while implementing <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>, it was noted that neither that PEP nor this one clearly explained the impact the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> changes would have on code execution APIs like <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code>. This section was added to this PEP’s rationale to better describe the impact and explain the intended benefits of the change.</p> <p>When <code class="docutils literal notranslate"><span class="pre">exec()</span></code> was converted from a statement to a builtin function in Python 3.0 (part of the core language changes in <a class="pep reference internal" href="../pep-3100/" title="PEP 3100 – Miscellaneous Python 3.0 Plans">PEP 3100</a>), the associated implicit call to <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> was removed, so it typically appears as if attempts to write to local variables with <code class="docutils literal notranslate"><span class="pre">exec()</span></code> in optimized frames are ignored:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span> <span class="gp">... </span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span> <span class="gp">... </span> <span class="n">exec</span><span class="p">(</span><span class="s2">&quot;x = 1&quot;</span><span class="p">)</span> <span class="gp">... </span> <span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="gp">... </span> <span class="nb">print</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="gp">...</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">f</span><span class="p">()</span> <span class="go">0</span> <span class="go">0</span> </pre></div> </div> <p>In truth, the writes aren’t being ignored, they just aren’t being copied from the dictionary cache back to the optimized local variable array. The changes to the dictionary are then overwritten the next time the dictionary cache is refreshed from the array:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span> <span class="gp">... </span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span> <span class="gp">... </span> <span class="n">locals_cache</span> <span class="o">=</span> <span class="nb">locals</span><span class="p">()</span> <span class="gp">... </span> <span class="n">exec</span><span class="p">(</span><span class="s2">&quot;x = 1&quot;</span><span class="p">)</span> <span class="gp">... </span> <span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="gp">... </span> <span class="nb">print</span><span class="p">(</span><span class="n">locals_cache</span><span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">])</span> <span class="gp">... </span> <span class="nb">print</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="gp">...</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">f</span><span class="p">()</span> <span class="go">0</span> <span class="go">1</span> <span class="go">0</span> </pre></div> </div> <p id="pep-558-ctypes-example">The behaviour becomes even stranger if a tracing function or another piece of code invokes <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> before the cache is next refreshed. In those cases the change <em>is</em> written back to the optimized local variable array:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="n">_getframe</span> <span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">ctypes</span> <span class="kn">import</span> <span class="n">pythonapi</span><span class="p">,</span> <span class="n">py_object</span><span class="p">,</span> <span class="n">c_int</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">_locals_to_fast</span> <span class="o">=</span> <span class="n">pythonapi</span><span class="o">.</span><span class="n">PyFrame_LocalsToFast</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">_locals_to_fast</span><span class="o">.</span><span class="n">argtypes</span> <span class="o">=</span> <span class="p">[</span><span class="n">py_object</span><span class="p">,</span> <span class="n">c_int</span><span class="p">]</span> <span class="gp">&gt;&gt;&gt; </span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span> <span class="gp">... </span> <span class="n">_frame</span> <span class="o">=</span> <span class="n">_getframe</span><span class="p">()</span> <span class="gp">... </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="gp">... </span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span> <span class="gp">... </span> <span class="n">exec</span><span class="p">(</span><span class="s2">&quot;x = 1&quot;</span><span class="p">)</span> <span class="gp">... </span> <span class="n">_locals_to_fast</span><span class="p">(</span><span class="n">_frame</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="gp">... </span> <span class="nb">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="gp">... </span> <span class="nb">print</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="gp">... </span> <span class="nb">print</span><span class="p">(</span><span class="n">_f_locals</span><span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">])</span> <span class="gp">...</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">f</span><span class="p">()</span> <span class="go">1</span> <span class="go">1</span> <span class="go">1</span> </pre></div> </div> <p>This situation was more common in Python 3.10 and earlier versions, as merely installing a tracing function was enough to trigger implicit calls to <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> after every line of Python code. However, it can still happen in Python 3.11+ depending on exactly which tracing functions are active (e.g. interactive debuggers intentionally do this so that changes made at the debugging prompt are visible when code execution resumes).</p> <p>All of the above comments in relation to <code class="docutils literal notranslate"><span class="pre">exec()</span></code> apply to <em>any</em> attempt to mutate the result of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in optimized scopes, and are the main reason that the <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin docs contain this caveat:</p> <blockquote> <div>Note: The contents of this dictionary should not be modified; changes may not affect the values of local and free variables used by the interpreter.</div></blockquote> <p>While the exact wording in the library reference is not entirely explicit, both <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code> have long used the results of calling <code class="docutils literal notranslate"><span class="pre">globals()</span></code> and <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in the calling Python frame as their default execution namespace.</p> <p>This was historically also equivalent to using the calling frame’s <code class="docutils literal notranslate"><span class="pre">frame.f_globals</span></code> and <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attributes, but this PEP maps the default namespace arguments for <code class="docutils literal notranslate"><span class="pre">exec()</span></code> and <code class="docutils literal notranslate"><span class="pre">eval()</span></code> to <code class="docutils literal notranslate"><span class="pre">globals()</span></code> and <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in the calling frame in order to preserve the property of defaulting to ignoring attempted writes to the local namespace in optimized scopes.</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 <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in an optimised scope returning the same shared dict for each call, it was possible to store extra “fake locals” in that dict. While these aren’t real locals known by the compiler (so they can’t be printed with code like <code class="docutils literal notranslate"><span class="pre">print(a)</span></code>), they can still be accessed via <code class="docutils literal notranslate"><span class="pre">locals()</span></code> and shared between multiple <code class="docutils literal notranslate"><span class="pre">exec()</span></code> calls in the same function scope. Furthermore, because they’re <em>not</em> real locals, they don’t get implicitly updated or removed when the shared cache is refreshed from the local variable storage array.</p> <p>When the code in <code class="docutils literal notranslate"><span class="pre">exec()</span></code> tries to write to an existing local variable, the runtime behaviour gets harder to predict:</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="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;: None}</span> <span class="n">f</span><span class="p">()</span> </pre></div> </div> <p><code class="docutils literal notranslate"><span class="pre">print(a)</span></code> will print <code class="docutils literal notranslate"><span class="pre">None</span></code> because the implicit <code class="docutils literal notranslate"><span class="pre">locals()</span></code> call in <code class="docutils literal notranslate"><span class="pre">exec()</span></code> refreshes the cached dict with the actual values on the frame. This means that, unlike the “fake” locals created by writing back to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> (including via previous calls to <code class="docutils literal notranslate"><span class="pre">exec()</span></code>), the real locals known by the compiler can’t easily be modified by <code class="docutils literal notranslate"><span class="pre">exec()</span></code> (it can be done, but it requires both retrieving the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute to enable writes back to the frame, and then invoking <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code>, as <a class="reference internal" href="#pep-558-ctypes-example"><span class="std std-ref">shown</span></a> using <code class="docutils literal notranslate"><span class="pre">ctypes</span></code> above).</p> <p>As noted in the <a class="reference internal" href="#pep-558-motivation"><span class="std std-ref">Motivation</span></a> section, this confusing side effect happens even if the local variable is only defined <em>after</em> the <code class="docutils literal notranslate"><span class="pre">exec()</span></code> calls:</p> <div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="k">def</span> <span class="nf">f</span><span class="p">():</span> <span class="gp">... </span> <span class="n">exec</span><span class="p">(</span><span class="s2">&quot;a = 0&quot;</span><span class="p">)</span> <span class="gp">... </span> <span class="n">exec</span><span class="p">(</span><span class="s2">&quot;print(&#39;a&#39; in locals())&quot;</span><span class="p">)</span> <span class="c1"># Printing &#39;a&#39; directly won&#39;t work</span> <span class="gp">... </span> <span class="nb">print</span><span class="p">(</span><span class="nb">locals</span><span class="p">())</span> <span class="gp">... </span> <span class="n">a</span> <span class="o">=</span> <span class="kc">None</span> <span class="gp">... </span> <span class="nb">print</span><span class="p">(</span><span class="nb">locals</span><span class="p">())</span> <span class="gp">...</span> <span class="gp">&gt;&gt;&gt; </span><span class="n">f</span><span class="p">()</span> <span class="go">False</span> <span class="go">{}</span> <span class="go">{&#39;a&#39;: None}</span> </pre></div> </div> <p>Because <code class="docutils literal notranslate"><span class="pre">a</span></code> is a real local variable that is not currently bound to a value, it gets explicitly removed from the dictionary returned by <code class="docutils literal notranslate"><span class="pre">locals()</span></code> whenever <code class="docutils literal notranslate"><span class="pre">locals()</span></code> is called prior to the <code class="docutils literal notranslate"><span class="pre">a</span> <span class="pre">=</span> <span class="pre">None</span></code> line. This removal is intentional, as it allows the contents of <code class="docutils literal notranslate"><span class="pre">locals()</span></code> to be updated correctly in optimized scopes when <code class="docutils literal notranslate"><span class="pre">del</span></code> statements are used to delete previously bound local variables.</p> <p>As noted in the <code class="docutils literal notranslate"><span class="pre">ctypes</span></code> <a class="reference internal" href="#pep-558-ctypes-example"><span class="std std-ref">example</span></a>, the above behavioural description may be invalidated if the CPython <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> API gets invoked while the frame is still running. In that case, the changes to <code class="docutils literal notranslate"><span class="pre">a</span></code> <em>might</em> become visible to the running code, depending on exactly when that API is called (and whether the frame has been primed for locals modification by accessing the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> attribute).</p> <p>As described above, two options were considered to replace this confusing behaviour:</p> <ul class="simple"> <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 without any of the caveats noted above.</li> </ul> <p>The PEP chooses the second option for the following reasons:</p> <ul class="simple"> <li>returning independent snapshots in optimized scopes preserves the Python 3.0 change to <code class="docutils literal notranslate"><span class="pre">exec()</span></code> that resulted in attempts to mutate local variables via <code class="docutils literal notranslate"><span class="pre">exec()</span></code> being ignored in most cases</li> <li>the distinction between “<code class="docutils literal notranslate"><span class="pre">locals()</span></code> gives an instantaneous snapshot of the local variables in optimized scopes, and read/write access in other scopes” and “<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” 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 wasn’t needed or desired</li> <li>in addition to improving clarity for human readers, ensuring that name rebinding in optimized scopes remains lexically visible in the code (as long as the frame introspection APIs are not accessed) allows compilers and interpreters to apply related performance optimizations more consistently</li> <li>only Python implementations that support the optional frame introspection APIs will need to provide the new write-through proxy support for optimized frames</li> </ul> <p>With the semantic changes to <code class="docutils literal notranslate"><span class="pre">locals()</span></code> in this PEP, it becomes much easier to explain 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>: in optimized scopes, they will <em>never</em> implicitly affect local variables; in other scopes, they will <em>always</em> implicitly affect local variables. In optimized scopes, any implicit assignment to the local variables will be discarded when the code execution API returns, since a fresh copy of the local variables is used on each invocation.</p> </section> <section id="retaining-the-internal-frame-value-cache"> <h3><a class="toc-backref" href="#retaining-the-internal-frame-value-cache" role="doc-backlink">Retaining the internal frame value cache</a></h3> <p>Retaining the internal frame value cache results in some visible quirks when frame proxy instances are kept around and re-used after name binding and unbinding operations have been executed on the frame.</p> <p>The primary reason for retaining the frame value cache is to maintain backwards compatibility with the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API. That API returns a borrowed reference, so it must refer to persistent state stored on the frame object. Storing a fast locals proxy object on the frame creates a problematic reference cycle, so the cleanest option is to instead continue to return a frame value cache, just as this function has done since optimised frames were first introduced.</p> <p>With the frame value cache being kept around anyway, it then further made sense to rely on it to simplify the fast locals proxy mapping implementation.</p> <p>Note: the fact <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> <em>doesn’t</em> use the internal frame value cache as part of the write-through proxy implementation is the key Python level difference between the two PEPs.</p> </section> <section id="changing-the-frame-api-semantics-in-regular-operation"> <h3><a class="toc-backref" href="#changing-the-frame-api-semantics-in-regular-operation" role="doc-backlink">Changing the frame API semantics in regular operation</a></h3> <p>Note: when this PEP was first written, it predated the Python 3.11 change to drop the implicit writeback of the frame local variables whenever a tracing function was installed, so making that change was included as part of the proposal.</p> <p>Earlier versions of this PEP proposed having the semantics of the frame <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> attribute depend on whether or not a tracing hook was currently installed - only providing the write-through proxy behaviour when a tracing hook was active, and otherwise behaving the same as the historical <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin.</p> <p>That was adopted as the original design proposal for a couple of key reasons, one pragmatic and one more philosophical:</p> <ul class="simple"> <li>Object allocations and method wrappers aren’t free, and tracing functions aren’t the only operations that access frame locals from outside the function. Restricting the changes to tracing mode meant that the additional memory and execution time overhead of these changes would be as close to zero in regular operation as we can possibly make them.</li> <li>“Don’t change what isn’t broken”: the current tracing mode problems are caused by a requirement that’s specific to tracing mode (support for external rebinding of function local variable references), so it made sense to also restrict any related fixes to tracing mode</li> </ul> <p>However, actually attempting to implement and document that dynamic approach highlighted the fact that it makes for a really subtle runtime state dependent behaviour distinction in how <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> works, and creates several new edge cases around how <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> behaves as trace functions are added and removed.</p> <p>Accordingly, the design was switched to the current one, where <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> is always a write-through proxy, and <code class="docutils literal notranslate"><span class="pre">locals()</span></code> is always a snapshot, which is both simpler to implement and easier to explain.</p> <p>Regardless of how the CPython reference implementation chooses to handle this, optimising compilers and interpreters also remain free to impose additional restrictions on debuggers, such as making local variable mutation through frame objects an opt-in behaviour that may disable some optimisations (just as the emulation of CPython’s frame API is already an opt-in flag in some Python implementations).</p> </section> <section id="continuing-to-support-storing-additional-data-on-optimised-frames"> <h3><a class="toc-backref" href="#continuing-to-support-storing-additional-data-on-optimised-frames" role="doc-backlink">Continuing to support storing additional data on optimised frames</a></h3> <p>One of the draft iterations of this PEP proposed removing the ability to store additional data on optimised frames by writing to <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> keys that didn’t correspond to local or closure variable names on the underlying frame.</p> <p>While this idea offered some attractive simplification of the fast locals proxy implementation, <code class="docutils literal notranslate"><span class="pre">pdb</span></code> stores <code class="docutils literal notranslate"><span class="pre">__return__</span></code> and <code class="docutils literal notranslate"><span class="pre">__exception__</span></code> values on arbitrary frames, so the standard library test suite fails if that functionality no longer works.</p> <p>Accordingly, the ability to store arbitrary keys was retained, at the expense of certain operations on proxy objects being slower than could otherwise be (since they can’t assume that only names defined on the code object will be accessible through the proxy).</p> <p>It is expected that the exact details of the interaction between the fast locals proxy and the <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> value cache on the underlying frame will evolve over time as opportunities for improvement are identified.</p> </section> <section id="historical-semantics-at-function-scope"> <h3><a class="toc-backref" href="#historical-semantics-at-function-scope" role="doc-backlink">Historical semantics at function scope</a></h3> <p>The current semantics of mutating <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 CPython are rather quirky due to historical implementation details:</p> <ul class="simple"> <li>actual execution uses the fast locals array for local variable bindings and cell references for nonlocal variables</li> <li>there’s a <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals</span></code> operation that populates the frame’s <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> attribute based on the current state of the fast locals array and any referenced cells. This exists for three reasons:<ul> <li>allowing trace functions to read the state of local variables</li> <li>allowing traceback processors to read the state of local variables</li> <li>allowing <code class="docutils literal notranslate"><span class="pre">locals()</span></code> to read the state of local variables</li> </ul> </li> <li>a direct reference to <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> is returned from <code class="docutils literal notranslate"><span class="pre">locals()</span></code>, so if you hand out multiple concurrent references, then all those references will be to the exact same dictionary</li> <li>the two common calls to the reverse operation, <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast</span></code>, were removed in the migration to Python 3: <code class="docutils literal notranslate"><span class="pre">exec</span></code> is no longer a statement (and hence can no longer affect function local namespaces), and the compiler now disallows the use of <code class="docutils literal notranslate"><span class="pre">from</span> <span class="pre">module</span> <span class="pre">import</span> <span class="pre">*</span></code> operations at function scope</li> <li>however, two obscure calling paths remain: <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast</span></code> is called as part of returning from a trace function (which allows debuggers to make changes to the local variable state), and you can also still inject the <code class="docutils literal notranslate"><span class="pre">IMPORT_STAR</span></code> opcode when creating a function directly from a code object rather than via the compiler</li> </ul> <p>This proposal deliberately <em>doesn’t</em> formalise these semantics as is, since they only make sense in terms of the historical evolution of the language and the reference implementation, rather than being deliberately designed.</p> </section> <section id="proposing-several-additions-to-the-stable-c-api-abi"> <h3><a class="toc-backref" href="#proposing-several-additions-to-the-stable-c-api-abi" role="doc-backlink">Proposing several additions to the stable C API/ABI</a></h3> <p>Historically, the CPython C API (and subsequently, the stable ABI) has exposed only a single API function related to the Python <code class="docutils literal notranslate"><span class="pre">locals</span></code> builtin: <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code>. However, as it returns a borrowed reference, it is not possible to adapt that interface directly to supporting the new <code class="docutils literal notranslate"><span class="pre">locals()</span></code> semantics proposed in this PEP.</p> <p>An earlier iteration of this PEP proposed a minimalist adaptation to the new semantics: one C API function that behaved like the Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code> builtin, and another that behaved like the <code class="docutils literal notranslate"><span class="pre">frame.f_locals</span></code> descriptor (creating and returning the write-through proxy if necessary).</p> <p>The feedback <a class="footnote-reference brackets" href="#id27" id="id13">[8]</a> on that version of the C API was that it was too heavily based on how the Python level semantics were implemented, and didn’t account for the behaviours that authors of C extensions were likely to <em>need</em>.</p> <p>The broader API now being proposed came from grouping the potential reasons for wanting to access the Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code> namespace from an extension module into the following cases:</p> <ul class="simple"> <li>needing to exactly replicate the semantics of the Python level <code class="docutils literal notranslate"><span class="pre">locals()</span></code> operation. This is the <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> API.</li> <li>needing to behave differently depending on whether writes to the result of <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> will be visible to Python code or not. This is handled by the <code class="docutils literal notranslate"><span class="pre">PyLocals_GetKind()</span></code> query API.</li> <li>always wanting a mutable namespace that has been pre-populated from the current Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code> namespace, but <em>not</em> wanting any changes to be visible to Python code. This is the <code class="docutils literal notranslate"><span class="pre">PyLocals_GetCopy()</span></code> API.</li> <li>always wanting a read-only view of the current locals namespace, without incurring the runtime overhead of making a full copy each time. This isn’t readily offered for optimised frames due to the need to check whether names are currently bound or not, so no specific API is being added to cover it.</li> </ul> <p>Historically, these kinds of checks and operations would only have been possible if a Python implementation emulated the full CPython frame API. With the proposed API, extension modules can instead ask more clearly for the semantics that they actually need, giving Python implementations more flexibility in how they provide those capabilities.</p> </section> <section id="comparison-with-pep-667"> <h3><a class="toc-backref" href="#comparison-with-pep-667" role="doc-backlink">Comparison with PEP 667</a></h3> <p>NOTE: the comparison below is against PEP 667 as it was in December 2021. It does not reflect the state of PEP 667 as of April 2024 (when this PEP was withdrawn in favour of proceeding with PEP 667).</p> <p><a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> offers a partially competing proposal for this PEP that suggests it would be reasonable to eliminate the internal frame value cache on optimised frames entirely.</p> <p>These changes were originally offered as amendments to <a class="pep reference internal" href="../pep-0558/" title="PEP 558 – Defined semantics for locals()">PEP 558</a>, and the PEP author rejected them for three main reasons:</p> <ul class="simple"> <li>the initial claim that <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> was unfixable because it returns a borrowed reference was simply false, as it is still working in the <a class="pep reference internal" href="../pep-0558/" title="PEP 558 – Defined semantics for locals()">PEP 558</a> reference implementation. All that is required to keep it working is to retain the internal frame value cache and design the fast locals proxy in such a way that it is reasonably straightforward to keep the cache up to date with changes in the frame state without incurring significant runtime overhead when the cache isn’t needed. Given that this claim is false, the proposal to require that all code using the <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API be rewritten to use a new API with different refcounting semantics fails <a class="pep reference internal" href="../pep-0387/" title="PEP 387 – Backwards Compatibility Policy">PEP 387</a>’s requirement that API compatibility breaks should have a large benefit to breakage ratio (since there’s no significant benefit gained from dropping the cache, no code breakage can be justified). The only genuinely unfixable public API is <code class="docutils literal notranslate"><span class="pre">PyFrame_LocalsToFast()</span></code> (which is why both PEPs propose breaking that).</li> <li>without some form of internal value cache, the API performance characteristics of the fast locals proxy mapping become quite unintuitive. <code class="docutils literal notranslate"><span class="pre">len(proxy)</span></code>, for example, becomes consistently O(n) in the number of variables defined on the frame, as the proxy has to iterate over the entire fast locals array to see which names are currently bound to values before it can determine the answer. By contrast, maintaining an internal frame value cache potentially allows proxies to largely be treated as normal dictionaries from an algorithmic complexity point of view, with allowances only needing to be made for the initial implicit O(n) cache refresh that runs the first time an operation that relies on the cache being up to date is executed.</li> <li>the claim that a cache-free implementation would be simpler is highly suspect, as <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> includes only a pure Python sketch of a subset of a mutable mapping implementation, rather than a full-fledged C implementation of a new mapping type integrated with the underlying data storage for optimised frames. <a class="pep reference internal" href="../pep-0558/" title="PEP 558 – Defined semantics for locals()">PEP 558</a>’s fast locals proxy implementation delegates heavily to the frame value cache for the operations needed to fully implement the mutable mapping API, allowing it to re-use the existing dict implementations of the following operations:<ul> <li><code class="docutils literal notranslate"><span class="pre">__len__</span></code></li> <li><code class="docutils literal notranslate"><span class="pre">__str__</span></code></li> <li><code class="docutils literal notranslate"><span class="pre">__or__</span></code> (dict union)</li> <li><code class="docutils literal notranslate"><span class="pre">__iter__</span></code> (allowing the <code class="docutils literal notranslate"><span class="pre">dict_keyiterator</span></code> type to be reused)</li> <li><code class="docutils literal notranslate"><span class="pre">__reversed__</span></code> (allowing the <code class="docutils literal notranslate"><span class="pre">dict_reversekeyiterator</span></code> type to be reused)</li> <li><code class="docutils literal notranslate"><span class="pre">keys()</span></code> (allowing the <code class="docutils literal notranslate"><span class="pre">dict_keys</span></code> type to be reused)</li> <li><code class="docutils literal notranslate"><span class="pre">values()</span></code> (allowing the <code class="docutils literal notranslate"><span class="pre">dict_values</span></code> type to be reused)</li> <li><code class="docutils literal notranslate"><span class="pre">items()</span></code> (allowing the <code class="docutils literal notranslate"><span class="pre">dict_items</span></code> type to be reused)</li> <li><code class="docutils literal notranslate"><span class="pre">copy()</span></code></li> <li><code class="docutils literal notranslate"><span class="pre">popitem()</span></code></li> <li>value comparison operations</li> </ul> </li> </ul> <p>Of the three reasons, the first is the most important (since we need compelling reasons to break API backwards compatibility, and we don’t have them).</p> <p>However, after reviewing <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>’s proposed Python level semantics, the author of this PEP eventually agreed that they <em>would</em> be simpler for users of the Python <code class="docutils literal notranslate"><span class="pre">locals()</span></code> API, so this distinction between the two PEPs has been eliminated: regardless of which PEP and implementation is accepted, the fast locals proxy object <em>always</em> provides a consistent view of the current state of the local variables, even if this results in some operations becoming O(n) that would be O(1) on a regular dictionary (specifically, <code class="docutils literal notranslate"><span class="pre">len(proxy)</span></code> becomes O(n), since it needs to check which names are currently bound, and proxy mapping comparisons avoid relying on the length check optimisation that allows differences in the number of stored keys to be detected quickly for regular mappings).</p> <p>Due to the adoption of these non-standard performance characteristics in the proxy implementation, the <code class="docutils literal notranslate"><span class="pre">PyLocals_GetView()</span></code> and <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocalsView()</span></code> C APIs were also removed from the proposal in this PEP.</p> <p>This leaves the only remaining points of distinction between the two PEPs as specifically related to the C API:</p> <ul class="simple"> <li><a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> still proposes completely unnecessary C API breakage (the programmatic deprecation and eventual removal of <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code>, <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocalsWithError()</span></code>, and <code class="docutils literal notranslate"><span class="pre">PyFrame_FastToLocals()</span></code>) without justification, when it is entirely possible to keep these working indefinitely (and interoperably) given a suitably designed fast locals proxy implementation</li> <li>the fast locals proxy handling of additional variables is defined in this PEP in a way that is fully interoperable with the existing <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API. In the proxy implementation proposed in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>, users of the new frame API will not see changes made to additional variables by users of the old API, and changes made to additional variables via the old API will be overwritten on subsequent calls to <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code>.</li> <li>the <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> API in this PEP is called <code class="docutils literal notranslate"><span class="pre">PyEval_Locals()</span></code> in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>. This function name is a bit strange as it lacks a verb, making it look more like a type name than a data access API.</li> <li>this PEP adds <code class="docutils literal notranslate"><span class="pre">PyLocals_GetCopy()</span></code> and <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocalsCopy()</span></code> APIs to allow extension modules to easily avoid incurring a double copy operation in frames where <code class="docutils literal notranslate"><span class="pre">PyLocals_Get()</span></code> already makes a copy</li> <li>this PEP adds <code class="docutils literal notranslate"><span class="pre">PyLocals_Kind</span></code>, <code class="docutils literal notranslate"><span class="pre">PyLocals_GetKind()</span></code>, and <code class="docutils literal notranslate"><span class="pre">PyFrame_GetLocalsKind()</span></code> to allow extension modules to identify when code is running at function scope without having to inspect non-portable frame and code object APIs (without the proposed query API, the existing equivalent to the new <code class="docutils literal notranslate"><span class="pre">PyLocals_GetKind()</span> <span class="pre">==</span> <span class="pre">PyLocals_SHALLOW_COPY</span></code> check is to include the CPython internal frame API headers and check if <code class="docutils literal notranslate"><span class="pre">_PyFrame_GetCode(PyEval_GetFrame())-&gt;co_flags</span> <span class="pre">&amp;</span> <span class="pre">CO_OPTIMIZED</span></code> is set)</li> </ul> <p>The Python pseudo-code below is based on the implementation sketch presented in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> as of the time of writing (2021-10-24). The differences that provide the improved interoperability between the new fast locals proxy API and the existing <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> API are noted in comments.</p> <p>As in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>, all attributes that start with an underscore are invisible and cannot be accessed directly. They serve only to illustrate the proposed design.</p> <p>For simplicity (and as in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>), the handling of module and class level frames is omitted (they’re much simpler, as <code class="docutils literal notranslate"><span class="pre">_locals</span></code> <em>is</em> the execution namespace, so no translation is required).</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="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="k">def</span> <span class="nf">_is_cell</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">offset</span><span class="p">):</span> <span class="o">...</span> <span class="c1"># How the interpreter identifies cells is an implementation detail</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">_fast_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">_locals</span><span class="p">:</span> <span class="nb">dict</span> <span class="o">|</span> <span class="n">NULL</span> <span class="c1"># Dictionary returned by 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">_locals</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">FastLocalsProxy</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="k">class</span> <span class="nc">FastLocalsProxy</span><span class="p">:</span> <span class="vm">__slots__</span> <span class="s2">&quot;_frame&quot;</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="nf">_set_locals_entry</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">val</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">f</span><span class="o">.</span><span class="n">_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">_locals</span> <span class="o">=</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">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</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">_fast_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">co</span><span class="o">.</span><span class="n">_is_cell</span><span class="p">(</span><span class="n">offset</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">raise</span> <span class="ne">KeyError</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="c1"># PyEval_GetLocals() interop: implicit frame cache refresh</span> <span class="bp">self</span><span class="o">.</span><span class="n">_set_locals_entry</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">val</span><span class="p">)</span> <span class="k">return</span> <span class="n">val</span> <span class="c1"># PyEval_GetLocals() interop: frame cache may contain additional names</span> <span class="k">if</span> <span class="n">f</span><span class="o">.</span><span class="n">_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">_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">co</span><span class="o">.</span><span class="n">_is_cell</span><span class="p">(</span><span class="n">offset</span><span class="p">)</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">_fast_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="c1"># PyEval_GetLocals() interop: implicit frame cache update</span> <span class="c1"># even for names that are part of the fast locals array</span> <span class="bp">self</span><span class="o">.</span><span class="n">_set_locals_entry</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">val</span><span class="p">)</span> <span class="k">def</span> <span class="fm">__delitem__</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">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">co</span><span class="o">.</span><span class="n">_is_cell</span><span class="p">(</span><span class="n">offset</span><span class="p">)</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">NULL</span> <span class="k">else</span><span class="p">:</span> <span class="n">f</span><span class="o">.</span><span class="n">_fast_locals</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">=</span> <span class="n">NULL</span> <span class="c1"># PyEval_GetLocals() interop: implicit frame cache update</span> <span class="c1"># even for names that are part of the fast locals array</span> <span class="k">if</span> <span class="n">f</span><span class="o">.</span><span class="n">_locals</span> <span class="ow">is</span> <span class="ow">not</span> <span class="n">NULL</span><span class="p">:</span> <span class="k">del</span> <span class="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="p">[</span><span class="n">name</span><span class="p">]</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">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">_fast_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">co</span><span class="o">.</span><span class="n">_is_cell</span><span class="p">(</span><span class="n">offset</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">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="p">:</span> <span class="c1"># Yield any extra names not defined on the frame</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="k">continue</span> <span class="k">yield</span> <span class="n">name</span> <span class="k">def</span> <span class="nf">popitem</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">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">:</span> <span class="n">val</span> <span class="o">=</span> <span class="bp">self</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="c1"># PyEval_GetLocals() interop: implicit frame cache update</span> <span class="c1"># even for names that are part of the fast locals array</span> <span class="k">del</span> <span class="n">name</span> <span class="k">return</span> <span class="n">name</span><span class="p">,</span> <span class="n">val</span> <span class="k">def</span> <span class="nf">_sync_frame_cache</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># This method underpins PyEval_GetLocals, PyFrame_FastToLocals</span> <span class="c1"># PyFrame_GetLocals, PyLocals_Get, mapping comparison, etc</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">if</span> <span class="n">f</span><span class="o">.</span><span class="n">_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">_locals</span> <span class="o">=</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">_fast_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="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span> <span class="k">continue</span> <span class="k">if</span> <span class="n">co</span><span class="o">.</span><span class="n">_is_cell</span><span class="p">(</span><span class="n">offset</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="n">f</span><span class="o">.</span><span class="n">_locals</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span> <span class="k">continue</span> <span class="n">f</span><span class="o">.</span><span class="n">_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">__len__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">_sync_frame_cache</span><span class="p">()</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">_locals</span><span class="p">)</span> </pre></div> </div> <p>Note: the simplest way to convert the earlier iterations of the <a class="pep reference internal" href="../pep-0558/" title="PEP 558 – Defined semantics for locals()">PEP 558</a> reference implementation into a preliminary implementation of the now proposed semantics is to remove the <code class="docutils literal notranslate"><span class="pre">frame_cache_updated</span></code> checks in affected operations, and instead always sync the frame cache in those methods. Adopting that approach changes the algorithmic complexity of the following operations as shown (where <code class="docutils literal notranslate"><span class="pre">n</span></code> is the number of local and cell variables defined on the frame):</p> <blockquote> <div><ul class="simple"> <li><code class="docutils literal notranslate"><span class="pre">__len__</span></code>: O(1) -&gt; O(n)</li> <li>value comparison operations: no longer benefit from O(1) length check shortcut</li> <li><code class="docutils literal notranslate"><span class="pre">__iter__</span></code>: O(1) -&gt; O(n)</li> <li><code class="docutils literal notranslate"><span class="pre">__reversed__</span></code>: O(1) -&gt; O(n)</li> <li><code class="docutils literal notranslate"><span class="pre">keys()</span></code>: O(1) -&gt; O(n)</li> <li><code class="docutils literal notranslate"><span class="pre">values()</span></code>: O(1) -&gt; O(n)</li> <li><code class="docutils literal notranslate"><span class="pre">items()</span></code>: O(1) -&gt; O(n)</li> <li><code class="docutils literal notranslate"><span class="pre">popitem()</span></code>: O(1) -&gt; O(n)</li> </ul> </div></blockquote> <p>The length check and value comparison operations have relatively limited opportunities for improvement: without allowing usage of a potentially stale cache, the only way to know how many variables are currently bound is to iterate over all of them and check, and if the implementation is going to be spending that many cycles on an operation anyway, it may as well spend it updating the frame value cache and then consuming the result. These operations are O(n) in both this PEP and in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>. Customised implementations could be provided that <em>are</em> faster than updating the frame cache, but it’s far from clear that the extra code complexity needed to speed these operations up would be worthwhile when it only offers a linear performance improvement rather than an algorithmic complexity improvement.</p> <p>The O(1) nature of the other operations can be restored by adding implementation code that doesn’t rely on the value cache being up to date.</p> <p>Keeping the iterator/iterable retrieval methods as O(1) will involve writing custom replacements for the corresponding builtin dict helper types, just as proposed in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>. As illustrated above, the implementations would be similar to the pseudo-code presented in <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a>, but not identical (due to the improved <code class="docutils literal notranslate"><span class="pre">PyEval_GetLocals()</span></code> interoperability offered by this PEP affecting the way it stores extra variables).</p> <p><code class="docutils literal notranslate"><span class="pre">popitem()</span></code> can be improved from “always O(n)” to “O(n) worst case” by creating a custom implementation that relies on the improved iteration APIs.</p> <p>To ensure stale frame information is never presented in the Python fast locals proxy API, these changes in the reference implementation will need to be implemented before merging.</p> <p>The current implementation at time of writing (2021-10-24) also still stores a copy of the fast refs mapping on each frame rather than storing a single instance on the underlying code object (as it still stores cell references directly, rather than check for cells on each fast locals array access). Fixing this would also be required before merging.</p> </section> </section> <section id="implementation"> <h2><a class="toc-backref" href="#implementation" role="doc-backlink">Implementation</a></h2> <p>The reference implementation update is in development as a draft pull request on GitHub (<a class="footnote-reference brackets" href="#id25" id="id14">[6]</a>).</p> </section> <section id="acknowledgements"> <h2><a class="toc-backref" href="#acknowledgements" role="doc-backlink">Acknowledgements</a></h2> <p>Thanks to Nathaniel J. Smith for proposing the write-through proxy idea in <a class="footnote-reference brackets" href="#id21" id="id15">[1]</a> and pointing out some critical design flaws in earlier iterations of the PEP that attempted to avoid introducing such a proxy.</p> <p>Thanks to Steve Dower and Petr Viktorin for asking that more attention be paid to the developer experience of the proposed C API additions <a class="footnote-reference brackets" href="#id27" id="id16">[8]</a> <a class="footnote-reference brackets" href="#id32" id="id17">[13]</a>.</p> <p>Thanks to Larry Hastings for the suggestion on how to use enums in the stable ABI while ensuring that they safely support typecasting from arbitrary integers.</p> <p>Thanks to Mark Shannon for pushing for further simplification of the C level API and semantics, as well as significant clarification of the PEP text (and for restarting discussion on the PEP in early 2021 after a further year of inactivity) <a class="footnote-reference brackets" href="#id29" id="id18">[10]</a> <a class="footnote-reference brackets" href="#id30" id="id19">[11]</a> <a class="footnote-reference brackets" href="#id31" id="id20">[12]</a>. Mark’s comments that were ultimately published as <a class="pep reference internal" href="../pep-0667/" title="PEP 667 – Consistent views of namespaces">PEP 667</a> also directly resulted in several implementation efficiency improvements that avoid incurring the cost of redundant O(n) mapping refresh operations when the relevant mappings aren’t used, as well as the change to ensure that the state reported through the Python level <code class="docutils literal notranslate"><span class="pre">f_locals</span></code> API is never stale.</p> </section> <section id="references"> <h2><a class="toc-backref" href="#references" role="doc-backlink">References</a></h2> <aside class="footnote-list brackets"> <aside class="footnote brackets" id="id21" role="doc-footnote"> <dt class="label" id="id21">[1]<em> (<a href='#id1'>1</a>, <a href='#id2'>2</a>, <a href='#id6'>3</a>, <a href='#id12'>4</a>, <a href='#id15'>5</a>) </em></dt> <dd><a class="reference external" href="https://github.com/python/cpython/issues/74929">Broken local variable assignment given threads + trace hook + closure</a></aside> <aside class="footnote brackets" id="id22" role="doc-footnote"> <dt class="label" id="id22">[3]<em> (<a href='#id3'>1</a>, <a href='#id7'>2</a>, <a href='#id8'>3</a>) </em></dt> <dd><a class="reference external" href="https://github.com/python/cpython/issues/5384)">Updating function local variables from pdb is unreliable</a></aside> <aside class="footnote brackets" id="id23" role="doc-footnote"> <dt class="label" id="id23">[<a href="#id4">4</a>]</dt> <dd><a class="reference external" href="https://docs.python.org/dev/library/sys.html#sys.settrace">CPython’s Python API for installing trace hooks</a></aside> <aside class="footnote brackets" id="id24" role="doc-footnote"> <dt class="label" id="id24">[<a href="#id5">5</a>]</dt> <dd><a class="reference external" href="https://docs.python.org/3/c-api/init.html#c.PyEval_SetTrace">CPython’s C API for installing trace hooks</a></aside> <aside class="footnote brackets" id="id25" role="doc-footnote"> <dt class="label" id="id25">[<a href="#id14">6</a>]</dt> <dd><a class="reference external" href="https://github.com/python/cpython/pull/3640/files">PEP 558 reference implementation</a></aside> <aside class="footnote brackets" id="id26" role="doc-footnote"> <dt class="label" id="id26">[7]<em> (<a href='#id10'>1</a>, <a href='#id11'>2</a>) </em></dt> <dd><a class="reference external" href="https://mail.python.org/pipermail/python-dev/2019-May/157738.html">Nathaniel’s review of possible function level semantics for locals()</a></aside> <aside class="footnote brackets" id="id27" role="doc-footnote"> <dt class="label" id="id27">[8]<em> (<a href='#id13'>1</a>, <a href='#id16'>2</a>) </em></dt> <dd><a class="reference external" href="https://discuss.python.org/t/pep-558-defined-semantics-for-locals/2936/3">Discussion of more intentionally designed C API enhancements</a></aside> <aside class="footnote brackets" id="id28" role="doc-footnote"> <dt class="label" id="id28">[<a href="#id9">9</a>]</dt> <dd><a class="reference external" href="https://github.com/python/cpython/issues/86363">Disable automatic update of frame locals during tracing</a></aside> <aside class="footnote brackets" id="id29" role="doc-footnote"> <dt class="label" id="id29">[<a href="#id18">10</a>]</dt> <dd><a class="reference external" href="https://mail.python.org/archives/list/python-dev&#64;python.org/thread/TUQOEWQSCQZPUDV2UFFKQ3C3I4WGFPAJ/">python-dev thread: Resurrecting PEP 558 (Defined semantics for locals())</a></aside> <aside class="footnote brackets" id="id30" role="doc-footnote"> <dt class="label" id="id30">[<a href="#id19">11</a>]</dt> <dd><a class="reference external" href="https://mail.python.org/archives/list/python-dev&#64;python.org/thread/A3UN4DGBCOB45STE6AQBITJFW6UZE43O/">python-dev thread: Comments on PEP 558</a></aside> <aside class="footnote brackets" id="id31" role="doc-footnote"> <dt class="label" id="id31">[<a href="#id20">12</a>]</dt> <dd><a class="reference external" href="https://mail.python.org/archives/list/python-dev&#64;python.org/thread/7TKPMD5LHCBXGFUIMKDAUZELRH6EX76S/">python-dev thread: More comments on PEP 558</a></aside> <aside class="footnote brackets" id="id32" role="doc-footnote"> <dt class="label" id="id32">[<a href="#id17">13</a>]</dt> <dd><a class="reference external" href="https://mail.python.org/archives/list/python-dev&#64;python.org/message/BTQUBHIVE766RPIWLORC5ZYRCRC4CEBL/">Petr Viktorin’s suggestion to use an enum for PyLocals_Get’s behaviour</a></aside> </aside> </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-0558.rst">https://github.com/python/peps/blob/main/peps/pep-0558.rst</a></p> <p>Last modified: <a class="reference external" href="https://github.com/python/peps/commits/main/peps/pep-0558.rst">2024-08-05 03:54:25 GMT</a></p> </article> <nav id="pep-sidebar"> <h2>Contents</h2> <ul> <li><a class="reference internal" href="#pep-withdrawal">PEP Withdrawal</a></li> <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="#proposal">Proposal</a><ul> <li><a class="reference internal" href="#new-locals-documentation">New <code class="docutils literal notranslate"><span class="pre">locals()</span></code> documentation</a></li> <li><a class="reference internal" href="#module-scope">Module scope</a></li> <li><a class="reference internal" href="#class-scope">Class scope</a></li> <li><a class="reference internal" href="#function-scope">Function scope</a></li> </ul> </li> <li><a class="reference internal" href="#cpython-implementation-changes">CPython Implementation Changes</a><ul> <li><a class="reference internal" href="#summary-of-proposed-implementation-specific-changes">Summary of proposed implementation-specific changes</a></li> <li><a class="reference internal" href="#providing-the-updated-python-level-semantics">Providing the updated Python level semantics</a></li> <li><a class="reference internal" href="#resolving-the-issues-with-tracing-mode-behaviour">Resolving the issues with tracing mode behaviour</a></li> <li><a class="reference internal" href="#fast-locals-proxy-implementation-details">Fast locals proxy implementation details</a></li> <li><a class="reference internal" href="#changes-to-the-stable-c-api-abi">Changes to the stable C API/ABI</a></li> <li><a class="reference internal" href="#changes-to-the-public-cpython-c-api">Changes to the public CPython C API</a></li> <li><a class="reference internal" href="#reducing-the-runtime-overhead-of-trace-hooks">Reducing the runtime overhead of trace hooks</a></li> </ul> </li> <li><a class="reference internal" href="#rationale-and-design-discussion">Rationale and Design Discussion</a><ul> <li><a class="reference internal" href="#changing-locals-to-return-independent-snapshots-at-function-scope">Changing <code class="docutils literal notranslate"><span class="pre">locals()</span></code> to return independent snapshots at function scope</a></li> <li><a class="reference internal" href="#keeping-locals-as-a-snapshot-at-function-scope">Keeping <code class="docutils literal notranslate"><span class="pre">locals()</span></code> as a snapshot at function scope</a></li> <li><a class="reference internal" href="#what-happens-with-the-default-args-for-eval-and-exec">What happens with the default args for <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code>?</a></li> <li><a class="reference internal" href="#additional-considerations-for-eval-and-exec-in-optimized-scopes">Additional considerations for <code class="docutils literal notranslate"><span class="pre">eval()</span></code> and <code class="docutils literal notranslate"><span class="pre">exec()</span></code> in optimized scopes</a></li> <li><a class="reference internal" href="#retaining-the-internal-frame-value-cache">Retaining the internal frame value cache</a></li> <li><a class="reference internal" href="#changing-the-frame-api-semantics-in-regular-operation">Changing the frame API semantics in regular operation</a></li> <li><a class="reference internal" href="#continuing-to-support-storing-additional-data-on-optimised-frames">Continuing to support storing additional data on optimised frames</a></li> <li><a class="reference internal" href="#historical-semantics-at-function-scope">Historical semantics at function scope</a></li> <li><a class="reference internal" href="#proposing-several-additions-to-the-stable-c-api-abi">Proposing several additions to the stable C API/ABI</a></li> <li><a class="reference internal" href="#comparison-with-pep-667">Comparison with PEP 667</a></li> </ul> </li> <li><a class="reference internal" href="#implementation">Implementation</a></li> <li><a class="reference internal" href="#acknowledgements">Acknowledgements</a></li> <li><a class="reference internal" href="#references">References</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-0558.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>

Pages: 1 2 3 4 5 6 7 8 9 10