Why Variable Annotations Raise NameError in Python REPL: How __annotations__ Works
If you've ever typed a type annotation directly into the Python REPL without assigning a value, you might have run into a puzzling behavior:
>>> x: int
>>> x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
The annotation statement executes without an error, but attempting to inspect the variable immediately raises a NameError. So, what exactly is the Python interpreter doing under the hood? Is the line silently ignored, or does it serve a purpose?
The Short Answer: Annotations Don't Create Values
In Python, type hints are purely metadata. A standalone variable annotation like x: int registers the type hint into the namespace's __annotations__ dictionary, but it does not assign a value or bind the name x to any object in the namespace.
Because x remains unbound, evaluating x fails with a NameError.
What the Interpreter Actually Executes
Variable annotations were officially introduced in PEP 526. The specification dictates how the compiler handles annotations depending on scope:
- In module or class scope: The annotation is parsed, evaluated, and stored in the special dictionary attribute
__annotations__. - Inside a function scope: Type annotations without assignment are completely ignored by the bytecode compiler at runtime.
Since the Python REPL runs at the top-level module scope, your statement creates or updates the module-level __annotations__ dictionary:
>>> x: int
>>> __annotations__
{'x': <class 'int'>}
As you can see, the interpreter didn't ignore x: int. It actively evaluated the right-hand side (int) and mapped it to the string key 'x' inside __annotations__.
Under the Hood: Dissecting the Bytecode
To see what the Python Virtual Machine (PVM) is doing, you can inspect the generated bytecode using the standard library's dis module:
>>> import dis
>>> dis.dis("x: int")
0 0 RESUME 0
1 2 SETUP_ANNOTATIONS
4 LOAD_NAME 0 (int)
6 LOAD_NAME 1 (__annotations__)
8 STORE_SUBSCR
10 RETURN_CONST 0 (None)
Notice what operations take place:
SETUP_ANNOTATIONS: Initializes__annotations__in the current scope if it doesn't already exist.LOAD_NAME 0 (int): Fetches the type objectint.STORE_SUBSCR: Performs the equivalent of__annotations__['x'] = int.
Crucially, there is no STORE_NAME (x) operation. The identifier x only exists as a dictionary key inside __annotations__, not as a variable in the global symbol table (globals()).
Contrast with Functions and Assignments
1. Variable Annotation with Assignment
If you provide an initial value, Python both updates the annotation dictionary and binds the name:
>>> y: str = "hello"
>>> y
'hello'
>>> __annotations__['y']
<class 'str'>
2. Annotations Inside Functions
Inside functions, unassigned annotations are completely discarded at runtime to save memory and execution overhead:
>>> def test():
... z: float
... print(locals())
...
>>> test()
{}
Neither z nor any local __annotations__ dictionary is created inside test(). Only static type checkers (like Mypy or Pyright) analyze local type hints during static code analysis.
Summary
When you type x: int in the REPL:
- Python executes bytecode that stores the annotation in
__annotations__['x']. - No value is bound to the variable name
x. - Calling
xtriggers aNameErrorbecause it does not exist inglobals()orbuiltins.