Immutable objects in Python
Hugh Jeremy
Posted on January 10, 2019
Python is a beautifully flexible beast. Class properties are variable, which means we can change them from anything to anything at any time.
class Flexible:
piece = "hello world"
instance = Flexible()
print(instance.piece) # prints “hello world”
instance.piece = 42
print(instance.piece) # prints “42”
Sometimes, we might want to trade some flexibility for safety. Humans are fallible, forgetful, and fickle beasts. Programmers are also humans. We make mistakes more often than we would like to admit.
Fortunately, Python gives us the tools protect ourselves against ourselves. Where we want to, we can trade flexibility for safety. You might wish to protect yourself by creating immutable objects : Instances of a class that can’t be modified once they are created.
In this article, we will seek immutability of properties. That is, we will stop ourselves from being able to change the.piece
property of a Flexible
class.
By making our class properties immutable, we eliminate the need to reason about object state. This reduces our cognitive load, and thus the potential for error.
Note that the immutability in this context is different to the immutability discussed by himank in his earlier Dev.to post. There, himank talks about immutability from the perspective of memory, an equally valuable but different angle on the broad topic of immutability in general.
Our objective is to achieve immutability from the perspective of the programmer - To explicitly catch cases were we accidentally attempt to mutate a property that we should not. From the perspective of the machine, the property is still perfectly mutable. We aren’t trying to change the way the property behaves in memory, we are trying to protect ourselves from our own stupidity.
To create an immutable property, we will utilise the inbuilt Python property
class. property
allows us to define get and set behaviour for a property.
class Flexible:
piece = property(lambda s: "hello world"w)
instance = Flexible()
print(instance.piece) # prints “hello world”
Instance.piece = ‘mutated’ # throws AttributeError
The property
class takes four parameters. The two we will focus on here are fget
and fset
. In the above example, lambda s: “hello world”
was our fget
, allowing us to print(instance.piece)
. The absence of fset
caused the AttributeError when we attempted to set the value of instance.piece
to ’mutated’
.
An AttributeError might be a solid enough reminder to yourself that you’ve accidentally done something dumb. However, you might be working on a project with multiple programmers. Perhaps an AttributeError is not a clear enough warning to others that a property should not change.
For example, a colleague might interpret that AttributeError as a sign that you simply forgot to implement fset
. They might merrily edit your class, adding fset
, unknowingly opening a Pandora’s Box of state-related bugs.
To give our colleagues as much information as possible, let’s make the immutability explicit. We can do so by subclassing property
.
class Immutable(property):
_MESSAGE = "Object state must not be mutated"
def __init__(self, get) -> None:
super(
fget,
self._set_error
)
def self._set_error(self, _1, _2) -> None:
raise RuntimeError(self._MESSAGE)
Now, when we attempt to change a property, we get a clear and unambiguous error.
class Flexible:
piece = Immutable(lambda s: "Can't touch this")
instance = Flexible()
instance.piece = "try me" # Raises RuntimeError with clear description
Of course, a lambda
serving a constant is not going to satisfy many requirements. You can supply the fget
parameter something more useful. For example, suppose a class maintains some internal state, readable by the whole program. It is crucial to the safe operation of the program that nothing outside the class modifies that state.
class Flexible:
_internal_state = 42
some_state = Immutable(lambda s: s._internal_state)
In this case, the rest of the program can safely access the value of _internal_state
via the some_state
property. We provide a strong hint to our colleagues that _internal_state
is off limits by using the leading underscore: A convention for hinting that a variable be treated as "private". The value returned by some_state
can be changed internally by the class, but it is very hard for a programmer to accidentally modify the state externally.
Other languages might achieve this behaviour in other ways, especially through the use of the private
keyword. For example, in Swift:
class Flexible {
public private(set) var some_state = 42
}
Unlike Swift and others, Python will not explicitly stop someone from modifying the Flexible
state. For example, a colleague could easily execute
instance._internal_state = "where is your god now?"
That flexibility is a great strength of Python. The point is not to stop anyone doing anything. The point is to provide helpful hints, checks, and clues to stop ourselves from making silly mistakes.
Originally published at hughjeremy.com
Posted on January 10, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.