November 21, 2016·4 min read·engineeringpythontypinginterpreter

Python 3.6's final beta puts variable annotations into the language

Python 3.6.0b4 gives maintainers a final planned beta for checking a language that now accepts variable annotations without enforcing their types.

Python's type comments ask tools to interpret information that the language itself treats as commentary. An empty list can carry a comment saying that it contains integers, but an ordinary syntax tree does not represent that comment as a type annotation. A variable that has no value yet creates a second problem. Giving it a placeholder value just to attach a comment can misrepresent the program's intended state. PEP 526 addresses those problems by adding variable annotations to Python's grammar.

The Python 3.6.0b4 release announcement dates today's release to November 21, 2016. It identifies this as the final planned beta and lists variable annotations as provisional. That makes the release a useful point to examine the distinction between syntax accepted by the interpreter and guarantees supplied by a separate tool. A program can now describe the intended type of a variable in executable source without acquiring runtime enforcement of that type.

I value Python's protocol-based object model, but its dynamic behavior makes the boundary between a declaration of intent and a checked property important. Variable annotations improve the way a program expresses intent. Their usefulness depends on understanding where the interpreter stops interpreting that intent.

An annotation does not initialize a variable

PEP 526 builds on the type comments introduced by PEP 484. Its rationale includes readability in editors and the ability to inspect annotations through ordinary language tools. It also discusses variables assigned in different branches. Their shared annotation can appear before the branches, so the reader does not have to wonder whether a comment attached to one assignment applies to the other.

The proposal allows an annotation with an assignment or an annotation on its own. These two statements have different effects on the variable's value.

from typing import List

pending: List[int] = []
attempts: int

The first statement binds an empty list to pending. The second records an annotation at module scope but does not bind an integer, None, or another placeholder to attempts. Reading attempts before assigning it raises NameError. This preserves the distinction between an absent value and a value deliberately set to None. Code that wants an initial value must still provide one.

The syntax also permits an assignment whose value contradicts its annotation. Writing attempts: int = "three" is valid at runtime. A type checker can report the mismatch, but the interpreter does not reject the assignment on that basis. The PEP explicitly excludes runtime type checking from its goals and says that annotations are not intended to become mandatory.

That choice affects how annotations should appear in an interface. A function receiving data from another component still needs whatever runtime validation its contract requires. An annotation describes an expectation to a reader or checker. It does not establish that an arbitrary caller has met that expectation. I prefer this separation to an ambiguous promise that some annotations happen to validate data while others merely describe it.

Scope changes the runtime behavior

Annotations at module and class scope evaluate their annotation expressions. For simple names, Python stores the resulting information in the relevant __annotations__ mapping. An undefined name used as an annotation there can therefore raise an error when that statement executes. Importing a module with annotations can involve evaluating names that a comment-based equivalent would never evaluate.

Local variable annotations have different behavior. Python does not evaluate or store their annotation expressions on each call. The PEP explains that creating and populating a mapping for every invocation would cost more than the local information justified. A local annotation still affects the compiler's classification of the name as local to that function.

def read_attempts():
    attempts: int
    return attempts

This function raises UnboundLocalError when called. The annotation establishes a local name, but no assignment supplies its value. Treating an annotation as an executable initialization would hide this distinction and make the example harder to explain. The proposal deliberately rejects automatic initialization to None or a new undefined-value singleton.

Class attributes need more information

A class body can initialize both a shared class variable and a default that instances will override. Their similar spelling does not communicate that difference to a checker. PEP 526 adds ClassVar in the typing module to describe a variable intended to belong to the class. A checker can then flag an assignment through an instance that would create a shadowing instance attribute.

from typing import ClassVar, Dict

class Worker:
    name: str
    totals: ClassVar[Dict[str, int]] = {}

    def __init__(self, name: str):
        self.name = name

Here the annotation for name can sit beside the other attribute descriptions even though initialization happens in the constructor. The ClassVar annotation communicates that totals is shared. It does not change Python's attribute assignment rules or prevent an instance from shadowing that name at runtime. The extra information supports diagnostics without introducing a second object model.

The proposal also limits where annotations can appear. It excludes annotated tuple unpacking because the relationship between multiple targets and one annotation would be ambiguous. Variables used as loop targets can receive annotations ahead of the loop. These restrictions keep the meaning of an annotation attached to an individual assignment target.

What the beta is asking maintainers to establish

The release announcement asks third-party maintainers to exercise Python 3.6 during the beta phase and report problems. It describes the release as a preview unsuitable for production and gives December 5 as the planned first release candidate date. Features can still change before that phase, even though the beta began with the feature set complete.

For variable annotations, compatibility includes tools that read source as well as applications that execute it. The change gives parsers a language construct to represent, while runtime consumers can use the annotation retrieval facilities described in the PEP. Libraries supporting older interpreters must account for the new grammar when deciding where to use it. The syntax belongs to Python 3.6, even though the information it expresses continues the existing type-hinting model.