Home > Software engineering >  Try to understand "Class and instance variable annotations" in PEP 526
Try to understand "Class and instance variable annotations" in PEP 526

Time:12-04

It seems to me that I misunderstand the PEP 526 in the context of annotating class and instance variables.

Based on the example in the PEP document here the object bar should be an instance variable with default value 7:

class Foo:
    bar: int = 7
    
    def __init__(self, bar):
        self.bar = bar

But testing that with Python it seems different to me. bar is an class and instance variable.

Python 3.9.10 (tags/v3.9.10:f2f3f53, Jan 17 2022, 15:14:21) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> class Foo:
...   bar: int = 7
...   def __init__(self, bar):
...     self.bar = bar
...
>>> Foo.bar
7
>>> f = Foo(3)
>>> f.bar
3
>>> f.__class__.bar
7
>>>

Maybe the PEP is not only clear about it? IMHO the PEP is violated and the goal not reached.

CodePudding user response:

There is no conflict, and the PEP does explain what is going on.

First of all: An instance variable is just a name set on an instance (usually via self.[name] = ...), and class variables are just names set on a class (usually by assigning to a name within the class ... statement body. They are not mutually exclusive.

What you may be missing is how Python looks up names on instances. When you have an instance foo and you want to get attribute bar, then foo.bar is actually handled by code defined for the class, as it needs to make a few checks for more advanced use-cases (called descriptors), but in the case of regular variables it’ll return the value from an instance variable before falling back to the class variable. This is how default values work; if there is no bar set on the instance then the value from the class is used.

In other words: a default value for an instance variable set on the class only comes into play when you don’t set an a value on the instance:

>>> class Foo:
...     bar: int = 7
...     def __init__(self, bar: int | None):
...         if bar is not None:
...             self.bar = bar
...
>>> f1 = Foo()  # default value for bar
>>> f2 = Foo(3)  # explicit value set
>>> Foo.bar  # default on class
7
>>> f1.bar   # is returned here
7
>>> "bar" in vars(f1)  # as there is no instance variable
False
>>> f2.bar  # gets the instance value
3
>>> vars(f2)["bar"]  # from the instance namespace
3

The section you link to talks about why there is a need to annotate class variables for type checkers. The example they give has two names, one intended to be a default value for an instance attribute and the other intended to be only set on the class. Without that type annotation the type checker can’t distinguish between those two cases because they look exactly the same otherwise:

class Starship:
    captain = 'Picard'
    stats = {}

The stats dict is meant to be shared between instances, so self.stats = ... would shadow the class variable and break the intended use:

stats is intended to be a class variable (keeping track of many different per-game statistics), while captain is an instance variable with a default value set in the class. This difference might not be seen by a type 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.

Using ClassVar[...] lets a type checker know that creating an instance attribute should be an error, while assigning to self.captain is not:

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 ClassVar[...]. In this way a type checker may flag accidental assignments to attributes with the same name on instances.

Note that the purpose of the PEP is to define how type annotations work for variables, including class and instance variables, not define how Python variables work at runtime! Type annotations are designed to be machine readable documentation for static analysis tools, and are there to help find programmer errors. Don’t try to read this as a description of how the Python runtime works.

  • Related