A template for immutable Python objects

First, the immutable object template, then the explanation:

import operator

class Circle(tuple):
      __slots__ = ()
      # An immutable and unique marker, used to make sure different
      # tuple subclasses are not equal to each other.
      _MARKER = object()

      size = property(operator.itemgetter(1))
      color = property(operator.itemgetter(2))

      def __new__(cls, size, color):
            @param size: an int
            @param color: a str
            return tuple.__new__(cls, (cls._MARKER, size, color))

      def __repr__(self):
            return '%s(%r, %r)' % (self.__class__.__name__, self[1], self[2])

        def double(self):
                Get a Circle twice the size of this one.
                return self.__class__(self.size * 2, self.color)

Why bother? Well, compared to normal user-defined class instances, Circle instances are immutable, have a __hash__ (hashes contents), and have better default comparison operators (compares contents). Everything works as you would expect:

>>> Circle(3, "red") == Circle(3, "red")
>>> Circle(3, "red") == Circle(3, "orange")
>>> Circle(3, "red") == (3, "red")
>>> Circle(3, "red").size
>>> Circle(3, "red").color
>>> a = set()
>>> a.add(Circle(3, "red"))
>>> a.add(Circle(3, "red"))
>>> a.add(Circle(4, "green"))
>>> a
set([Circle(3, 'red'), Circle(4, 'green')])
>>> c = Circle(2, "red")
>>> c.double()
Circle(4, 'red')
>>> c.color = 'blue'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

If you wanted hash ibility and good comparisons, couldn’t you just add __hash__ and comparison operators to your normal class? Yes, but then hashing and comparison would call into slower [1] Python code rather than tuple 's native methods. And since your object is not really immutable, a user of your API might be tempted to mutate an object that really shouldn’t be mutated.

The above hack is actually built in to Python as collections.namedtuple (see the source). It works by generating code (like the above template) and exec ing it. There are a few reasons you might not want it, though:

  1. namedtuple is available in Python 2.6+ only (though there are some alternate implementations).
  2. You cannot add your own methods or customize the __repr__.
  3. You cannot validate parameters passed to the constructor.
  4. If you want to add docstrings to your namedtuple, you probably have to subclass it.
  5. Completely different namedtuple s are equal to each other if they have the same contents:
>>> from collections import namedtuple
>>> A = namedtuple('A', 'x y')
>>> B = namedtuple('B', 'z t')
>>> A(1, 2) == B(1, 2)

Then again, there’s reasons not to use the above “immutable object template” either: it’s very easy to mess up, you need rather superfluous unit tests for attribute access, and it might scare away new Python programmers. There are also a few surprises: Circle is len() able, indexable, and sliceable. But it is the least-terrible solution I could come up with.

[1]This might not be the case with PyPy.