2

What's the correct way to implement static immutable properties in Python?

Minimal example:

A program module 'Family' has a class Parent, defined below:

class Parent():
    def __init__(self, type):
        self.type = type

The parent can be of either of the two string types: 'mother' or 'father'. An external user of this class should be able to set the type when constructing, and later query parent.type to get either of these two values. Furthermore, other parts of the Family module utilise the Parent class and rely on either of these two values being returned. Therefore, the requirements of the type are as follows:

  • be available both externally to users and internally to the Family module
  • be always either 'mother' or 'father', therefore be based on immutable variables

A naive approach could encourage setting type by passing a string:

parent = Parent('mother')

But this allows for accidental misspellings (eg. Parent('omther')). To prevent this, I utilise the following approach:

class Parent():
    TYPE_MOTHER = 'mother'
    TYPE_FATHER = 'father'
    TYPES = [TYPE_MOTHER, TYPE_FATHER]

    def __init__(self, type):
        assert type in self.TYPES, ('Invalid type "%s"' % type)
        self.type = type

parent = Parent(Parent.TYPE_MOTHER)

However, nothing would stop the user from changing these static variables as they like, eg:

parent = Parent('mother')
Parent.TYPE_MOTHER = 'burrito'
print(parent.type == Parent.TYPE_MOTHER)
#> False

To solve this, I considered using @staticmethod and @property annotations:

class Parent():
    @property
    @staticmethod
    def TYPE_MOTHER():
        return 'mother'

    @property
    @staticmethod
    def TYPE_FATHER():
        return 'father'

This wouldn't prevent the user from passing a string to constructor (eg. Parent('mother')) but at least it would prevent them from screwing up the Family module by changing what the parent types can be.


Problems I have with this method:

  • It's ugly, 4 lines of code instead of 1
  • It feels hacky (possibly due to my experience with language-supported private static variables in other languages)
  • My IDE's linter don't like it due to no 'self' argument (even if it's provided)

Questions for you:

  • Is this method Pythonic?
  • Is there a nicer way to do it?
  • Can you suggest a pattern that will achieve such immutability, whilst also enforcing user to use only the variables that I want them to use, preventing them from passing primitive string to the constructor?
Voy
  • 5,286
  • 1
  • 49
  • 59

3 Answers3

1

I've found one tidy answer to my question - Enum class (doc).

from enum import Enum
class Parent():
    class TYPES(Enum):
        MOTHER = 'mother'
        FATHER = 'father'

    def __init__(self, type:TYPES):
        assert type in self.TYPES, ('Invalid type "%s"' % type)
        self.type = type

parent = Parent(Parent.TYPES.MOTHER)

In which case user could still overwrite Parent.TYPES. I can't think of another way of preventing it than using __setattr__ and catching the malicious write. If you can think of anything here share your thoughts.

Such pattern provides following benefits:

  • No way of overwriting the individual properties MOTHER and FATHER due to Enum's policy
  • No way of overwriting the TYPES due to __setattr__
  • No way to use primitive string instead of Parent.TYPES.MOTHER (or .FATHER)
  • Compatible with assert type in self.TYPES or assert isinstance(type, self.TYPES)

So far, I can see two differences in usage:

  • If I'd ever want to get the actual value - ie. 'mother' or 'father', then I'd need to use the .value property of the Enum. So instead of parent.type (which would return "TYPES.MOTHER") I need to useparent.type.value which correctly returns 'mother'.
  • To print the contents of TYPES I need to use list(self.TYPES)

I think with Enum I could also drop the ALL_CAPS standard, given that I don't need to signify that types is static.

Voy
  • 5,286
  • 1
  • 49
  • 59
  • I wouldn't worry too much about users "malicious" behaviour. Accidentally passing a wrong value, okay, but why would the users break their own code? – tobias_k Nov 28 '19 at 17:26
  • I admit 'malicious' was an overstatement; 'erroneous' would probably be more appropriate. I would rather not have a user going _'hey, I know where I can store that local `types` variable... `parent.TYPES = ['tall', 'short']`'_, silently breaking the class. Isn't this the whole idea of immutability - const, final etc. - to prevent such behaviour? I think spending a lot of time with JavaScript made me very cautious of such users, ie. I've seen this happen multiple times. – Voy Nov 28 '19 at 17:33
0

Isn't this what classes are for. Just have Mother and Father classes to inherit from:

class Parent():
    def __init__(self, type):
        self.type = type

class Mother(Parent):
    def __init__(self):
        super().__init__('mother')

class Father(Parent):
    def __init__(self):
        super().__init__('father')

parent = Mother()
quamrana
  • 37,849
  • 12
  • 53
  • 71
  • An interesting approach. Though would you say this is still viable if there are 20 types? – Voy Nov 28 '19 at 13:01
  • Or if there are multiple variations of such type properties of the class (admittedly not included in the original question)? eg.`parent.type` being either 'mother' or 'father', but Parent could also have `parent.character` being 'good' , 'neutral', or 'bad', `parent.vehicle` being 'car', 'motorbike', 'none'. etc. Wouldn't you then need to permute each of these types to cover each variation? – Voy Nov 28 '19 at 13:15
  • I think what you need is some sort of Builder Pattern like: `parent = parentBuilder().makeMother().withCharacterGood().withVehicleMotorBike().get()` – quamrana Nov 28 '19 at 16:31
  • Yes that could work. How would you say that compares to the solution I proposed? – Voy Nov 29 '19 at 11:16
  • With the solution I propose, then your users would only ever see method chaining as per my example. You can still have strings and enums under the covers as you propose, but method chaining could hide this and let you vary the exact details whenever you want. (Imagine the horror your users would go through if you renamed `TYPE_BIKE` to `TYPE_MOTORBIKE` etc) – quamrana Nov 29 '19 at 14:16
0

Overall the approach is fine, just a bit long. If you do not want to implement inheritance, the shortest way to go about it is by making the TYPES list a protected class variable:

class Parent():
    def __init__(self, type):
        self._TYPES = ['mother', 'father']
        assert type in self._TYPES, ('Invalid type "%s"' % type)
        self.type = type
nickyfot
  • 1,932
  • 17
  • 25
  • Thanks, yes, that would put the convention between the user and the misuse, given that Parent._TYPES could (though shouldn't) still be modified. On the other hand it promotes the unwanted string-passing when constructing the class, given that in such case 'mother' and 'father' are by convention unavailable externally – Voy Nov 28 '19 at 13:04
  • 1
    True! Potentially then making the TYPES list a property with only a getter which at least shorts out the duplication issue. – nickyfot Nov 28 '19 at 13:51