Skip to content

Commit

Permalink
Merge pull request #2 from ilevkivskyi/master
Browse files Browse the repository at this point in the history
Additions to ClassVar and runtime effects
  • Loading branch information
gvanrossum authored Aug 25, 2016
2 parents 2063be8 + e14bf09 commit c793e4e
Showing 1 changed file with 89 additions and 56 deletions.
145 changes: 89 additions & 56 deletions pep-0526.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Content-Type: text/x-rst
Created: 09-Aug-2016
Python-Version: 3.6


Notice for Reviewers
====================

Expand All @@ -18,6 +19,7 @@ textual nits please use https://github.com/python/peps/ssues and
"at-mention" at least one of the authors. For discussion about
contents, please refer to https://github.com/python/typing/issues/258.


Abstract
========

Expand All @@ -43,7 +45,8 @@ and attributes, instead of expressing them through comments::
captain: str # Note: no initial value!

class Starship:
stats: ClassAttr[Dict[str, int]] = {}
stats: ClassVar[Dict[str, int]] = {}


Rationale
=========
Expand Down Expand Up @@ -165,16 +168,17 @@ or ``__new__``. The proposed syntax is as follows::
class BasicStarship:
captain: str = 'Picard' # instance variable with default
damage: int # instance variable without default
stats: ClassAttr[Dict[str, int]] = {} # class variable
stats: ClassVar[Dict[str, int]] = {} # class variable

Here ``ClassAttr`` is a special class in typing module that indicates to
Here ``ClassVar`` is a special class in typing module that indicates to
static type checker that this attribute should not be set on class instances.
This could be illustrated with a more detailed example. In this class::

class Starship:
captain = 'Picard'
stats = {}
def __init__(self, captain=None):
def __init__(self, damage, captain=None):
self.damage = damage
if captain:
self.captain = captain # Else keep the default
def hit(self):
Expand All @@ -187,11 +191,36 @@ checker -- both get initialized in the class, but ``captain`` serves only
as a convenient default value for the instance variable, while ``stats``
is truly a class variable -- it is intended to be shared by all instances.

Since both variables happen to be initialized at the class level, I it is
Since both variables happen to be initialized at the class level, it is
useful to distinguish them by marking class variables as annotated with
types wrapped in ``ClassAttr[...]``. In such way type checker will prevent
accidental assignmets to attributes with a same name on class instances.
types wrapped in ``ClassVar[...]``. In such way type checker will prevent
accidental assignments to attributes with a same name on class instances.
For example, annotating the discussed class::

class Starship:
captain: str = 'Picard'
damage: int
stats: ClassVar[Dict[str, int]] = {}
def __init__(self, damage: int, captain: str = None):
self.damage = damage
if captain:
self.captain = captain # Else keep the default
def hit(self):
Starship.stats['hits'] = Starship.stats.get('hits', 0) + 1

enterprise_d = Starship(3000)
enterprise_d.stats = {} # Flagged as error by a type checker
Starship.stats = {} # This is OK

As a matter of convenience, instance attributes can be annotated in
``__init__`` or other methods, rather than in class::

from typing import Generic, TypeVar
T = TypeVar(’T’)

class Box(Generic[T]):
def __init__(self, content):
self.content: T = content

Where annotations aren't allowed
********************************
Expand All @@ -214,62 +243,75 @@ unpacking::
with myfunc() as f:
...

Changes to standard library
===========================

- Special covariant type ``ClassAttr[T_co]`` is added to the ``typing``
Changes to standard library and documentation
=============================================

- Special covariant type ``ClassVar[T_co]`` is added to the ``typing``
module. It accepts only a single argument that should be a valid type,
and is used to annotate class attributes that should no be set on class
and is used to annotate class variables that should no be set on class
instances. This restriction is ensured by static checkers,
but not at runtime.

- Function ``getannotations`` is added to the ``inspect`` module that is used
to retrieve annotations at runtime from modules, classes, and functions.
Annotations are returned as a dictionary mapping from variable, arguments,
or attributes to theit type hints. For classes it returns
or attributes to their type hints. For classes it returns
``collections.ChainMap`` constructed from annotations in method
resolution order of that class.

- Recommended guidelines for using annotations will be added to the
documentation, containing a pedagogical recapitulation of specifications
described in this PEP and in PEP 484. As well, a helper script for
translating type comments into type annotations will be published
separately from the standard library.


Runtime effects of type annotations
===================================

In order to capture variable types that are usable at runtime, we store the
types in ``__annotations__`` as dictionaries at various levels. At each level
(for example, global), the types dictionary would be stored in the
``__annotations__`` dictionary for that given level. Here is an example for
both global and class level types::
Variable annotations that are found at a module or class level are
evaluated and stored in ``__annotations__`` attribute of that module or
class as a dictionary mapping from names to evaluated annotations.
Here is an example::

# print global type annotations
from typing import Dict
class Player:
...
players: Dict[str, Player]
print(__annotations__)

# print class type annotations
class Starship:
hitpoints: ClassAttr[int] = 50
stats: ClassAttr[Dict[str, int]] = {}
shield: int = 100
captain: str # no initial value
print(Starship.__annotations__)
print(__annotations__)
# prints: {'players': typing.Dict[str, __main__.Player]}

A note about locals -- the value of having annotations available locally does not
offset the cost of having to create and populate the annotations dictionary on
every function call.
The recommended way of getting annotations at runtime is by using
``inspect.getannotations`` function; as with all dunder attributes,
any undocummented use of ``__annotations__`` is subject to breakage
without warning::

These annotations would be printed out from the previous program as follows::
from typing import Dict, ClassVar
from inspect import getannotations
class Starship:
hitpoints: int = 50
stats: ClassVar[Dict[str, int]] = {}
shield: int = 100
captain: str
def __init__(self, captain: str) -> None:
...

{'players': Dict[str, Player]}
assert getannotations(Starship) == {'hitpoints': int,
'stats': ClassVar[Dict[str, int]],
'shield': int,
'captain': str}

{'hitpoints': ClassAttr[int],
'stats': ClassAttr[Dict[str, int]],
'shield': int,
'captain': str
}
assert getannotations(Starship.__init__) == {'captain': str,
'return': None}

Mypy supports allowing ``# type`` on assignments to instance variables and
other things. In case you prefer annotating instance variables in
``__init__`` or ``__new__``, you can also annotate variable types for
instance variables in methods. Despite this, ``__annotations__`` will
not be updated for that class.
Note that if annotations are not found statically, then the
``__annotations__`` dictionary is not created at all. Also the
value of having annotations available locally does not offset
the cost of having to create and populate the annotations dictionary
on every function call. Therefore annotations at function level are
not evaluated and not stored.

Other uses of annotations
*************************
Expand All @@ -292,7 +334,7 @@ but with this PEP we explicitly recommend type hinting as the
preferred use of annotations.


Rejected proposals and thigs left out for now
Rejected proposals and things left out for now
=============================================

- **Introduce a new keyword:**
Expand All @@ -304,7 +346,7 @@ Rejected proposals and thigs left out for now

- **Allow type annotations for tuple unpacking:**
This cause an ambiguity: PEP 484 says that everywhere, where a type
is expectedconfusion but missing ``Any`` is assumed. Therefore it is
is expected but missing ``Any`` is assumed. Therefore it is
not clear what meaning should be assigned to this statement::

x, y: T
Expand Down Expand Up @@ -332,18 +374,9 @@ Rejected proposals and thigs left out for now
This was rejected because in ``for`` it makes hard to spot the actual
iterable, and in ``with`` it will confuse the CPython's LL(1) parser.

- **Allow annotations of complex expressions:**
This creates a problem with evaluation of the left hand side:
On one hand, if that expression is going to be evaluated,
then it leads to inconsistency: ``f().a: int``
calls ``f()``, but, e.g., ``d[f()]: int`` cannot call ``f()``
since it can't call ``d.__setitem__()`` without an actual item.
On other hand, if the left hand side is not evaluated
(so in the first and second example ``f()`` should not be called),
then some bugs will not be caught at runtime.
Finally, the main intention of annotations is to annotate variables
and attributes in modules and classes, and two forms ``x: int`` and
``self.x: int`` are enough for this purpose.
- **Evaluate local annotations at function definition time:**
This has been rejected by Guido because the placement of the annotation
strongly suggests that it's in the same scope as the surrounding code.

- **Store variable annotations also in function scope:**
The value of having the annotations available locally is just not enough
Expand All @@ -370,7 +403,7 @@ Rejected proposals and thigs left out for now
declarations together in the class makes it easier to find them,
and helps a first-time reader of the code.

- **Forget about** ``ClassAttr`` **altogether:**
- **Forget about** ``ClassVar`` **altogether:**
This was proposed since mypy seems to be getting along fine without a way
to distinguish between class and instance variables. But a type checker
can do useful things with the extra information, for example flag
Expand Down

0 comments on commit c793e4e

Please sign in to comment.