Skip to content

Commit b5700d4

Browse files
authored
Rewrite why annotation scope is needed (#4)
* Rewrite why annotation scope is needed * Minor copyediting
1 parent 6848c8e commit b5700d4

File tree

1 file changed

+57
-34
lines changed

1 file changed

+57
-34
lines changed

peps/pep-0750.rst

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -414,53 +414,69 @@ except that all expressions are always implicitly wrapped with a ``lambda``:
414414
lexical scope, including local and global variables. Any valid Python expression
415415
can be used, including function and method calls.
416416

417-
However, this PEP further proposes to extend the usual lexical scoping of a
418-
lambda-wrapped expression in the interpolation, as seen with class definitions,
419-
to that supported by `annotation scopes
420-
<https://docs.python.org/3/reference/executionmodel.html#annotation-scopes>`_,
421-
so as to minimize developer surprise.
417+
However, there's one additional nuance to consider, `function scope
418+
<https://docs.python.org/3/reference/executionmodel.html#resolution-of-names>`_
419+
versus `annotation scope
420+
<https://docs.python.org/3/reference/executionmodel.html#annotation-scopes>`_.
421+
Consider this somewhat contrived example to configure captions:
422422

423-
Let's look at why using annotation scope is important with a somewhat contrived
424-
example where a developer sets up how figures are rendered in a caption, using a
425-
``html`` tag:
423+
.. code-block:: python
424+
425+
class CaptionConfig:
426+
tag = 'b'
427+
figure = f'<{tag}>Figure</{tag}>'
428+
429+
Let's now attempt to rewrite the above example to use tag strings:
426430

427431
.. code-block:: python
428432
429-
class SomeConfig:
433+
class CaptionConfig:
430434
tag = 'b'
431435
figure = html'<{tag}>Figure</{tag}>'
432436
433-
Unless the ``html`` function fully evaluates all interpolations before the class
434-
is constructed, a subsequent interpolation will fail with ``NameError: name
435-
'tag' is not defined``. This means that the name ``tag`` is no longer available
436-
in the usual lexical scope for lambdas. Contrast that scoping with what is seen
437-
in using a f-string:
437+
Unfortunately, this rewrite doesn't work if using the usual lambda wrapping to
438+
implement interpolations, namely ``lambda: tag``. When the interpolations are
439+
evaluated by the tag function, it will result in ``NameError: name 'tag' is not
440+
defined``. The root cause of this name error is that ``lambda: tag`` uses function scope,
441+
and it's therefore not able to use the class definition where ``tag`` is
442+
defined.
443+
444+
Desugaring how the tag string could be evaluated will result in the same
445+
``NameError`` even using f-strings; the lambda wrapping here also uses function
446+
scoping:
438447

439448
.. code-block:: python
440449
441-
class SomeConfig:
450+
class CaptionConfig:
442451
tag = 'b'
443-
figure = f'<{tag}>Figure</{tag}>'
452+
figure = f'<{(lambda: tag)()}>Figure</{(lambda: tag)()}>'
444453
445-
The class variable ``figure`` here does evaluate correctly with respect to
446-
lexical scope, if at the risk of an HTML injection attack. The reason it
447-
evaluates correctly is that the evaluation is always immediate. First the
448-
expression for ``tag`` is evaluated; then for ``figure``; and finally both
449-
settings are passed into the dynamic construction of the class ``SomeConfig``.
454+
For tag strings, getting such a ``NameError`` would be surprising. It would also
455+
be a rough edge in using tag strings in this specific case of working with class
456+
variables. After all, tag strings are supposed to support a superset of the
457+
capabilities of f-strings.
450458

451-
Because tag strings are supposed to work like f-strings, but with more
452-
capabilities, it's necessary to support annotation scope for the lambda-wrapped
453-
expressions in interpolations.
459+
The solution is to use annotation scope for tag string interpolations. While the
460+
name "annotation scope" suggests it's only about annotations, it solves this
461+
problem by lexically resolving names in the class definition, such as ``tag``,
462+
unlike function scope.
454463

455-
But why is annotation scope used, given that this PEP is not at all about
456-
annotations?
464+
.. note::
465+
466+
The use of annotation scope means it's not possible to fully desugar
467+
interpolations into Python code. Instead it's as if one is writing
468+
``interpolation_lambda: tag``, not ``lambda: tag``, where a hypothetical
469+
``interpolation_lambda`` keyword variant uses annotation scope instead of
470+
the standard function scope.
471+
472+
This is more or less how the reference implementation implements this
473+
concept (but without creating a new keyword of course).
457474

458-
*Annotation* scope (as of :pep:`649` and :pep:`695`) provides the necessary
459-
scoping semantics for names used in class definitions, thereby avoiding the
460-
``NameError`` above. In addition, the implementation of these two PEPs provide a
461-
somewhat similar deferred execution model for annotations. However, this PEP and
462-
its reference implementation only use the support for annotation scope; it's up
463-
to the tag function to evaluate any interpolations.
475+
This PEP and its reference implementation therefore use the support for
476+
annotation scope. Note that this usage is a separable part from the
477+
implementation of :pep:`649` and :pep:`695` which provides a somewhat similar
478+
deferred execution model for annotations. Instead it's up to the tag function to
479+
evaluate any interpolations.
464480

465481
With annotation scope in place, lambda-wrapped expressions in interpolations
466482
then provide the usual lexical scoping seen with f-strings. So there's no need
@@ -520,7 +536,7 @@ the following, at the cost of losing some type specificity:
520536
def mytag(*args: str | tuple) -> Any:
521537
...
522538
523-
A user might write a tag string as a split string:
539+
A user might write a tag string as follows:
524540

525541
.. code-block:: python
526542
@@ -568,7 +584,14 @@ This is equivalent to:
568584

569585
.. code-block:: python
570586
571-
mytag(DecodedConcrete(r'Hi, '), InterpolationConcrete(lambda: name, 'name', 's', 'format_spec'), DecodedConcrete(r'!'))
587+
mytag(DecodedConcrete(r'Hi, '), InterpolationConcrete(lambda: name, 'name',
588+
's', 'format_spec'), DecodedConcrete(r'!'))
589+
590+
.. note::
591+
592+
To keep it simple, this and subsequent desugaring omits an important scoping
593+
aspect in how names in interpolation expressions are resolved, specifically
594+
when defining classes. See `Interpolation Expression Evaluation`_.
572595

573596
No Empty Decoded String
574597
-----------------------

0 commit comments

Comments
 (0)