1

I'm trying to catch up on Python variable annotations. According to PEP-0526 we now have something like:

class BasicStarship:
    captain: str = 'Picard'               # instance variable with default
    damage: int                           # instance variable without default
    stats: ClassVar[Dict[str, int]] = {}  # class variable 

It's been a rough weekend, and my Python is a bit rusty, but I thought variables declared without assignment to self were class variables. Here are some interesting examples:

class BasicStarship:
        captain: str = 'Picard'               # instance variable with default

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

wtf = BasicStarship('Jim')
BasicStarship.captain = 'nope'

print(wtf.captain) #=> Jim

The above code works as I would expect. Below however confuses me a bit.

class BasicStarship:
        captain: str = 'Picard'               # instance variable with default

wtf = BasicStarship()
BasicStarship.captain = 'nope'

print(wtf.captain) #=> 'nope'

I would have expected 'Picard' instead of 'nope' in the second example. I feel like I am missing some rules about class variables versus instance variables. To some extent I would have thought doing BasicStarship.captain would have resulted in a class error since the captain is an instance variable (in the first example, not sure in the second example). Have you always been able to define instance variables after the class declaration (outside of methods)? Is there some interplay between class and instance variables that would make this clearer?

Code run with Python 3.6.3

Jimbo
  • 2,886
  • 2
  • 29
  • 45
  • Does this answer your question? [What is the python attribute get and set order?](https://stackoverflow.com/questions/30961069/what-is-the-python-attribute-get-and-set-order) – Mike Scotty Jan 06 '20 at 03:23
  • Until a value is assigned to that attribute of the instance then the class attribute **is** the instance attribute. What happens when you then doe `wtf.captain = 'foo'`? – wwii Jan 06 '20 at 03:32
  • @wwii it would for the first time assign the `captain` instance attribute, previously a reference to it would just be a reference to the underlying class attribute - the documentation is not very clear. – Grismar Jan 06 '20 at 03:34
  • Read through the Custom classes and Class instances sections of [3.2 The standard type heiarchy](https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy). - attribute lookup and assignment. – wwii Jan 06 '20 at 03:40
  • People suggest closing the question because "what happens" is answered elsewhere, however I think the question primarily pertains to the confusing comment in the documentation? Perhaps that's best addressed by leaving a message with the maintainers of the documentation, because I agree it's confusing. – Grismar Jan 06 '20 at 03:50
  • @Grismar I don't know how though. The text is explicit as to what is happening: "attribute is _intended to be used_ as a class variable and should not be set on instances of that class" and "`ClassVar` does not change Python runtime behavior". The comments in the code are only there to connect the code examples with the text; I don't think it would be a good idea to stuff that much text into the code example itself. – Amadan Jan 06 '20 at 03:56
  • Does this answer your question? [How to declare a static attribute in Python?](https://stackoverflow.com/questions/27481116/how-to-declare-a-static-attribute-in-python) – ventaquil Jan 06 '20 at 03:57
  • 1
    @ventaquil No, OP knows (even though they are unsure of themselves) how to declare a "static attribute". The problem is a comment in documentation on `typing.ClassVar` that seems to suggest that `captain: str = 'Picard'` defines an _instance_ variable, causing confusion. – Amadan Jan 06 '20 at 04:01
  • The documentation, at least that comment, is a little misleading. It is assigning a class variables there, at `captain: str = 'Picard'` Nothing has fundamentally changed in python with the addition of type annotations. – juanpa.arrivillaga Jan 06 '20 at 04:05

2 Answers2

1

I share some of your confusion about the documentation, since it seems that captain in your example is a class attribute instead of an instance attribute.

Consider this:

class BasicStarship:
    captain = 'Picard'

    def __init__(self, captain=None):
        if captain:
            self.captain = captain


wtf1 = BasicStarship()
wtf2 = BasicStarship('Me!')

BasicStarship.captain = 'Riker'

print(wtf1.captain)
print(wtf2.captain)

As you would expect (based on your question), this prints:

Riker
Me!

However, this is interesting:

print(wtf1.__dict__)
print(wtf2.__dict__)
print(BasicStarship.__dict__)

Results in:

{}
{'captain': 'Me!'}
{'__module__': '__main__', 'captain': 'Riker', '__init__': <etc.> }

So, wtf1 does not have an attribute called captain and therefore uses the class attribute called captain (explains why this changes when you change it). wtf2 does have an attribute called captain, so that overrides the class attribute. BasicStarship shows the class attribute.

This all makes sense and is similar to the example given in the actual documentation, the only confusing part is the description as instance variable with default, as it seems more correct as class variable with default.

(on Python 3.7.5, same otherwise)

Grismar
  • 27,561
  • 4
  • 31
  • 54
0

It is the normal interplay of class variables and instance variables. It has nothing to do with typing.

class Quux:
    foo = 1
    bar = 2

    def __init__(self):
        self.bar = 3
        self.baz = 4

quux = Quux()
Quux.foo    # => 1   - class variable
Quux.bar    # => 2   - class variable
quux.foo    # => 1   - class variable, because no instance variable
quux.bar    # => 3   - instance variable shadowing the class variable
quux.baz    # => 4   - instance variable

Your error is the wording here:

captain: str = 'Picard'               # instance variable with default

captain is not an instance variable here. This defines a class variable, which acts as a default when a corresponding instance variable is not set. But the class variable and the instance variable are two separate things.

Note that typing is only ever evaluated by static type checkers, never by Python interpreter itself. Thus, there cannot be any runtime semantic difference between these two:

class CheckedQuux:
    foo: Dict[str, int] = {}
    bar: ClassVar[Dict[str, int]] = {}

At runtime, they are both assignments of an empty dictionary to a class variable. If the annotation made it so that one of them defined an instance variable and the other a class variable, it would violate the rule that typing can't have runtime effect. The comment under ClassVar is misleading, though the text gives hints about what is meant: the annotation serves to indicate the intent of how the variable will be used. The type checker is to assume that foo above will be accessed from the instance (checked_quux.foo) and will presumably be shadowed over by an instance variable, while bar should not be accessed on the instance, but only class (CheckedQuux.bar) and should raise a type check error if we tried to access it on an instance (checked_quux.bar)

Amadan
  • 191,408
  • 23
  • 240
  • 301
  • 1
    I think the OP is not missing the distinction, they are confused by the documentation stating that `foo = 1` is an `instance variable`. (understandably so, in my opinion) – Grismar Jan 06 '20 at 03:34