Jimbo
Jimbo

Reputation: 3284

instance variables declared in a python class outside methods with type hinting

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

Upvotes: 1

Views: 798

Answers (2)

Amadan
Amadan

Reputation: 198324

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)

Upvotes: 0

Grismar
Grismar

Reputation: 31319

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)

Upvotes: 1

Related Questions