Python Immutability for Collection-Typed Attributes

I came across a situation recently where I wanted to preserve the immutability of lists of data that were being returned from a Python class. I held an implicit assumption that nothing in the code would change the content of the lists. But nothing enforced this assumption in the code, and the logic in the code was getting deep, enough that a list being passed through multiple levels of methods could be inadvertently changed, creating a difficult to diagnose bug.

A previous post discussed maintaining immutability of a Java class by maintaining the immutability of object instances returned from methods of the class. Let’s explore how this can be done in Python.

Here’s a simple python class that returns collections of data, demonstrating two ways of making the data immutable:

  • returning a copy of a list (using the slice operator to make a copy)
  • returning a tuple (inherently immutable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ImmutableDemo(object):
    def __init__(self):
        self._the_list = [0, 1, 2, 3, 4]
        self._the_tuple = (0, 1, 2, 3, 4)
 
    def get_list(self):
        """Return a copy of the list (using slice operator)"""
        return self._the_list[:] 
    def get_tuple(self):
        """Return a tuple - is inherently immutable"""
        return self._the_tuple 
    def print_state(self):
        """Print the states of the list and the tuple"""
        print 'ImmutableDemo object:'
        print_obj('_the_list', self._the_list)
        print_obj('_the_tuple', self._the_tuple)
 
def print_obj(namestr, obj):
    """Print a string representation and the hexadecimal id of an object instance"""
    print namestr + ': ' + str(obj) + ', id=' + hex(id(obj))

Here’s a main program that exercises the ImmutableDemo class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if __name__ == '__main__':
    demo = ImmutableDemo()
    demo.print_state()
 
    print
    a_list = demo.get_list()
    print_obj('a_list', a_list)
 
    # The list returned from the demo object should be different
    # than its internal copy of the list
    assert id(demo._the_list) != id(a_list) 
    a_tuple = demo.get_tuple()
    print_obj('a_tuple', a_tuple)
 
    # Tuple returned from demo object is the same as the internal one
    assert id(demo._the_tuple) == id(a_tuple) 
    print
    print "Remove an element from the list..."
    a_list.remove(3)
    # tuple has only count() and index() methods, no modifiers
 
    print
    demo.print_state()
    print
    print_obj('a_list', a_list)
 
    print
    print 'Methods of list:'
    print [attr for attr in dir(list) if not attr.startswith('__')]
 
    print 'Methods of tuple:'
    print [attr for attr in dir(tuple) if not attr.startswith('__')]

Output from a run of the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ImmutableDemo object:
_the_list: [0, 1, 2, 3, 4], id=0x10e2d98c0_the_tuple: (0, 1, 2, 3, 4), id=0x10e1c6d70
 
a_list: [0, 1, 2, 3, 4], id=0x10e2c0b48a_tuple: (0, 1, 2, 3, 4), id=0x10e1c6d70
 
Remove an element from the list...
 
ImmutableDemo object:
_the_list: [0, 1, 2, 3, 4], id=0x10e2d98c0_the_tuple: (0, 1, 2, 3, 4), id=0x10e1c6d70
 
a_list: [0, 1, 2, 4], id=0x10e2c0b48 
Methods of list:
['append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']Methods of tuple:
['count', 'index']

Printing the unique id() of the list and tuple objects, both internal and external to the ImmutableDemo class, shows that the list returned from the class is a different instance (lines 2 and 5 of the output listing), while the tuple is the same instance internally and externally. So when an element is removed from the list that’s returned form the get_list() method, the copy of the list internal to the ImmutableDemo object doesn’t change.

The tuple class in Python doesn’t have any methods that could change the content of a tuple instance (line 19 of the output listing), so tuples are inherently immutable.

TL; DR

When returning a list type from a method of a Python class, and instances of the class should be kept immutable, consider returning a copy of the list using the slice operator (‘my_list[:]’) rather than returning the internal copy of the list. This will reduce the chance of creating a subtle bug that modifies the internal state of the class instance, a bug buried deep in code that uses the class that may not be discovered immediately.

Another alternative would be to return a tuple instead of a list. Python tuples can’t be changed and so are inherently immutable.

Versions

$ python -V
Python 2.7.10

References

The StackOverflow post How to make an immutable object in Python? has a good discussion of an alternative to make collection types immutable – extend from the collection class and implement the __setattr__() and __delattr__() methods.

Add a Comment