9

As far as I understand var is a class variable here:

class MyClass:
    var = 'hello'

    def __init__(self):
        print(self.var)

And thats an instance variable:

class MyClass:

    def __init__(self, var):
        self.var = var
        print(self.var)

I had the problem, that I was looking for a method to make type hinting possible for instance variables. I can of course typehint the parameter with def __init__(self, var: str): but that would not effect the instance variable itself.

Then I noticed in some descriptions (like here) that they used the term instance variable for a var like this:

class MyClass:
    var : str = 'hello'

    def __init__(self, var : str = None):
        self.var = var if var
        print(self.var)

That would be the solution indeed, but is that still an instance variable? Because it is defined in the class body, it would be a class variable in my understanding. If you would use a list for var, all alterations to this list-var would be shared over the instances.

But in this case there would be no problem, because the string is replaced and would not be shared for other instances. However, it seems wrong to me if you call it an instance variable and I don't know if I should use it like this just to have the type hinting working.

James
  • 32,991
  • 4
  • 47
  • 70
Asara
  • 2,791
  • 3
  • 26
  • 55
  • Overriding a class variable with an instance variable is an anti-pattern. You should avoid using it. What is wrong with `def __init__(self, var: str):`? It indicates to the user that `var` should be a string. – James Nov 28 '17 at 13:17
  • That's a `SyntaxError` according to my inerpreter. – Stop harming Monica Nov 28 '17 at 13:24
  • @Goyo This was implemented in Python 3.5 – APorter1031 Nov 28 '17 at 13:24
  • I am using [python 3.6](https://repl.it/repls/PalatableEuphoricBufflehead) – Stop harming Monica Nov 28 '17 at 13:26
  • @James the problem is, that the local `var` within the `__init__` method would be typehinted, but not the instance variable itself. When I use `self.var` in another method, interpreter and IDE wouldn't know that this should be a string – Asara Nov 28 '17 at 13:30
  • @Asara It won't be a class variable because there is a syntax error and the code won't run. Depending on how you fix the error different things can happen. – Stop harming Monica Nov 28 '17 at 14:17

1 Answers1

11

That would be the solution indeed, but is that still an instance variable? Because it is defined in the class body, it would be a class variable in my understanding. [...snip...] However, it seems wrong to me if you call it an instance variable and I don't know if I should use it like this just to have the type hinting working.

For what it's worth, I also share the same discomfort. It seems like we're conceptually mixing two concepts there just for the sake of having cleaner type annotations.

However, I've asked Guido one or two times about this, and it seems like he does indeed prefers treating those class attributes as if they were instance attributes.

In any case, to answer your core question, if we do this:

class Test:
    field1: int
    field2: str = 'foo'

Then...

  1. PEP 484 and 526 compliant type checkers will treat this class as if:
    1. It has an instance attribute named field1
    2. It has an instance attribute named field2 that has a default value of 'foo' (as per PEP 526).
  2. At runtime, ignoring type hints, Python will:
    1. Add a class annotation named field1 to Test, but not a class attribute. (Class annotations are not automatically turned into class attributes.)
    2. Add both a class annotation named field2 to Test as well as a class attribute named field2 containing the value 'foo'.

So, it can get a bit muddled.

But regardless, this then begs the question: how do we indicate to a type checker that we want some field to genuinely be a class attribute?

Well, it turns out PEP 484 was amended semi-recently to contain the ClassVar type annotation, which does exactly that.

So, if we wanted to add a new class attribute, we could do this:

from typing import ClassVar

class Test:
    field1: int
    field2: str = 'foo'
    field3: ClassVar[int] = 3

So now, field3 should be treated as a class attribute with a default value of '3'.

(Note: ClassVar was added to typing for Python 3.5.3 -- if you're using the older version of typing bundled with Python 3.5, you can get a "backport" of the type by installing the typing_extensions third part module via pip and importing ClassVar from there instead.)

I think whether you decide to embrace this approach or not use it is a personal preference.

On one hand, Guido's opinion, pretty much by definition, defines what's "Pythonic" or not, so from that stance, there's no issue adopting this new idiom. Furthermore, the language itself is slowly but surely shifting to adopt this new idiom -- see the very recently accepted PEP 557, for example, which ends up following this same idiom of treating class attributes/class annotations as instance attributes.

On the other hand, it's difficult to shake off the nagging worry that this subtle difference will lead to issues down the line. In that case, you could stick with the standard approach of just setting all your fields inside __init__. This approach also has the benefit of keeping your code compatible with Python 2 and 3.x - 3.5.

A middle ground might be to just simply never use class attributes, in any way, shape, or form, and just stick to using class annotations. This is slightly restrictive, since we can no longer give our instance variables default values, but we can now avoid conflating class attributes with instance attributes entirely. (As previously stated, and as pointed out in the comments, class annotations are not added as class attributes.)

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • 1
    `field1: int` doesn't create a class attribute. Annotations don't create attributes; you need to actually assign a value to create an attribute. – user2357112 Dec 05 '17 at 05:19
  • Also, variable annotations don't exist pre-3.6, so you can't actually do anything with `ClassVar` pre-3.6 even if you install a backport. – user2357112 Dec 05 '17 at 05:20
  • @user2357112 ok, sure, but doing `field1: int = 3` does create a class attribute, and yet according to [PEP 526](https://www.python.org/dev/peps/pep-0526/#class-and-instance-variable-annotations), that should be treated as an instance field with a default argument -- my overarching point is that there's some weird intermingling precisely because of things like that. Also, I believe doing `foo = 3 # type: ClassVar[int]` is legal, so there is some value in having that backport. (And in fact, ClassVar was actually added to typing in Python 3.5.3, not Python 3.6.) – Michael0x2a Dec 05 '17 at 05:33
  • 3
    Definitely agreed on the weirdness of the intermingling. – user2357112 Dec 05 '17 at 05:39
  • @user2357112 In any case, I clarified the class attribute vs class annotation thing to address your first objection. – Michael0x2a Dec 05 '17 at 05:45