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:

  1. SETUP_ANNOTATIONS: Initializes __annotations__ in the current scope if it doesn't already exist.
  2. LOAD_NAME 0 (int): Fetches the type object int.
  3. 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 x triggers a NameError because it does not exist in globals() or builtins.