Skip to content

Guidelines for Class Attribute vs Instance Attribute usage #235

@MikeHart85

Description

@MikeHart85

Class attributes can be a bit dangerous in Python. They have behaviour that appears very similar to instance attributes most of the time, but work very differently under the hood and can suddenly produce radically different results under some conditions (which may be triggered far away from what is affected).

To illustrate:
class Foobar(object):
    _value = 'Foo'
    @property
    def value(self):
        return self._value  # Doesn't really exist until setter is called!
    @value.setter
    def value(self, v):
        self._value = v

>>> foo = Foobar()
>>> foo.value  # Returns Foobar._value (foo._value doesn't exist yet!)
'Foo'
>>> foo.value = 'Foo'  # Defines foo._value for the first time
>>> foo.value  # Returns foo._value now
'Foo'  # Looks the same, but isn't
>>> bar = Foobar()
>>> bar.value  # Foobar._value
'Foo'
>>> Foobar._value = 'Bar'
>>> foo.value  # foo._value
'Foo'
>>> bar.value  # Foobar._value still
'Bar'
>>> bar.value = bar.value  # Side-effects can't be predicted by looking at property code alone (!!!)
>>> Foobar._value = 'Baz'
>>> bar.value  # Now really bar._value
'Bar'

Even worse:
class Foobar(object):
    values = []
    def add(self, value):
        self.values.append(value)
    def get(self):
        return self.values

>>> foo = Foobar()
>>> foo.add('hello')
>>> foo.get()
['hello']
>>> bar = Foobar()
>>> bar.add('world')
>>> bar.get()  # ?!
['hello', 'world']
>>> foo.get()  # !!
['hello', 'world']
>>> foo.values = ['goodbye', 'cruel']
>>> foo.get()
['goodbye', 'cruel']
>>> foo.add('world')
>>> foo.get()
['goodbye', 'cruel', 'world']
>>> bar.get()  # ?!?!
['hello', 'world']

This is probably not intended behaviour. Again, this is caused by self.values not actually existing (and defaulting to Foobar.values). Here with the added twist that, since the value of values is mutable, changes to an instance affect all other instances. That is, until I "assign" something to foo.values (really, create foo.values for the first time and point it at a different list). Now foo has its own values list, while bar still falls back on whatever Foobar.values points to.


In C++ terms, Python class attributes appear to behave like static, public data members that automagically become non-static as soon as you assign something to them via this->attribute = ... or instance_variable.attribute = .... But only for that one instance. And not if you modify them any other way apart from assignment (such as calling member functions). And not if you assign to ClassName::attribute instead.

This issue is to do the following:

  • Come up with general rule(s) for when using class attributes is appropriate / permitted. For example:
    • Are there any plausible scenarios where you or another developer may be tempted to write self.attribute = value or myinstance.attribute = value?
      If yes, attribute must be an instance attribute.
      If no, attribute may be a class attribute.
      ... or ...
    • "Only use a class attribute where you would use a class constant in C++."
  • Document rule(s) in our contribution guidelines somewhere.
  • Go through codebase to make sure it complies with new rule(s).

SimulatedJulabo, in particular, has a lot of class attributes that are used like instance attributes.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions