Search code examples
pythondesign-patternsinterfaceimmutabilitystatic-variables

Python static immutable properties


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?

Solution

  • 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.