PEP 649: Behavior of the REPL

PEP 649 states:

The semantics established in this PEP also hold true when executing code in Python’s interactive REPL shell, except for module annotations in the interactive module (__main__) itself. Since that module is never “finished”, there’s no specific point where we can compile the __annotate__ function.
For the sake of simplicity, in this case we forego delayed evaluation. Module-level annotations in the REPL shell will continue to work exactly as they do with “stock semantics”, evaluating immediately and setting the result directly inside the __annotations__ dict.

I implemented this behavior:

>>> x: int = 3
>>> __annotations__
{'x': <class 'int'>}
>>> class X:
...     y: doesntexist
...     
>>> X.__annotations__
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    X.__annotations__
  File "<python-input-2>", line 2, in <annotations of X>
    y: doesntexist
       ^^^^^^^^^^^
NameError: name 'doesntexist' is not defined

But I’m not sure it’s a good idea. The interactive console would be the only place in the language that eagerly evaluates annotations, and it seems confusing to have core language behavior that differs based on whether the “interactive” compilation mode was used.

If there are multiple statements in a line, the new 3.13 REPL actually compiles all but the final statement in exec (i.e., module) mode, so my current code applies PEP 649 semantics to them.

>>> x: x = 3; pass
>>> __annotate__(1)
{'x': 3}

There is surely some way to fix that and make all of the REPL code get compiled as “interactive”, but it shows how subtle this behavior change is.

Another implication of the proposed behavior is that pasting code into the REPL line by line will sometimes not work:

x: X | None = None
class X: pass

With PEP 649 applied, this code will work fine when placed in a module. But if you paste it into the console line by line, the first line will throw an error.

Here’s an alternative proposal: treat the interactive console like any other module-level code, and make annotations lazily evaluated. This makes the language more consistent and avoids subtle behavior changes between modules and the REPL.

13 Likes

I’m curious how you will address the problem noted in the PEP quote. When do you actually compile the __annotate__ function for the module-level code in the REPL?

A new one would get created for every statement that is evaluated. This would mean that earlier annotations get lost when you add a new one.

Ok, that’s what I suspected.

I think this is likely fine; I don’t really know under what circumstances someone would care about the contents of __annotations__ reflecting their full REPL session history. But it does still mean that the REPL is a bit odd in terms of __annotations__ behavior.

In practice, given that we have to choose, I think “forward references working the same as everywhere else” is much more valuable in the REPL than a useful “module” level __annotations__ is.

3 Likes