Indisputably immutable

For many of us, as we develop as coders, we want to continue to grow our knowledge. We pour over the standard library (STL) of this wonderful language, looking for hidden gems. There are many such gems in the Python STL. Such as all the fun things you can do with sets and itertools. But one of the lesser used (which is a real shame) is found in the collections module: namedtuples.

So….what is a named tuple?

from collections import namedtuple

Address = namedtuple("Address", ["number", "street", "city", "state", "zip_code"])

the_prez = Address("1600", "Pennsylvania Avenue", "Washington", "DC", "20500")

print("{the_prez.number} {the_prez.street}".format(**locals()))

 

Pretty boring stuff all around, right? Who cares that you can get dotted notation from this? Why not just write your own class?  You totally can.  There’s nothing to it.

class Address(object):
    def __init__(self, number, street, city, state, zip_code):
        self.number = number
        self.street = street
        self.city = city
        self.state = state
        self.zip_code = zip_code

Yep. Gotta love the classic Python class. Super explicit, which is keeping in one of the core tenants of the Python. But, then, depending on your use-case they may have a weakness.  Let’s say that the type of class you have written is all about data storage and retrieval.  That means you’d really like the data to be reliable and, in a word: immutable.  If you just write the aforementioned class and someone wants to change the street number where the president lives it’s pretty simple stuff:

class Address(object):
    def __init__(self, number, street, city, state, zip_code):
        self.number = number
        self.street = street
        self.city = city
        self.state = state
        self.zip_code = zip_code

the_prez = Address("1600", "Pennsylvania Avenue", "Washington", "DC", "20500")
the_prez.number = "1601"

Boom, now the postman will now deliver your postcard from the grand canyon to the wrong house.

“But,” you say, “I could just override __setitem__ and be done with my class!” You are correct. But, maybe, just maybe you’re a lazy coder (as I believe all coders should be from time to time), and you want to have immutibility without having to rewrite a core behavior of your class?  Why, what you’d be talking about would have the immutability of a tuple, with the flexibility of a class! Sounds crazy?  Sounds impossible?  Well, guess what?  You can have it.

Named tuples, just as the name implies is a tuple (an immutable data type) which uses names instead of numerical indexing. Simple. But, why the devil would you use them if you already control the code? Is it simply the immutability?

class Address(namedtuple("Address", ["number", "street", "city", "state", "zip_code"])):

    def __new__(cls, number=None, street=None, city=None, state=None, zip_code=None):
        return super(Address, cls).__new__(
            cls,
            number=number,
            street=street,
            city=city,
            state=state,
            zip_code=zip_code)

So, what happens if you try to redirect that grand canyon postcard by changing the house number now?

Traceback (most recent call last):
 File "namedtuplecraziness.py", line 74, in
 the_prez.number = "1601"
 AttributeError: can't set attribute

Nope.  Sorry.  Protected by immutability!

Of course, there are always oddities.  If you were to have a mutable variable in the namedtuple, the variable retains its mutability, while the other (non-mutable) variables remain immutable.  Here’s an example where we make the ‘number’ field into a list.  Which means we can now change it through direct index-based assignment or even through append.

Here’s an example where we make the ‘number’ field into a list.  Which means we can now change it through direct index-based assignment or even through append.

from collections import namedtuple

class Address(namedtuple("Address", ["number", "street", "city", "state", "zip_code"])):
    def __new__(cls, number=None, street=None, city=None, state=None, zip_code=None):
        return super(Address, cls).__new__(
            cls,
            number=[number],
            street=street,
            city=city,
            state=state,
            zip_code=zip_code)

a = Address()
print(a)
Address(number=[None], street=None, city=None, state=None, zip_code=None)
a.number[0] = 1600

print(a)
Address(number=[1600], street=None, city=None, state=None, zip_code=None)

a.number.append(1700)

print(a)
Address(number=[1600, 1700], street=None, city=None, state=None, zip_code=None)

Of course, just because it’s a list, and therefore mutable, doesn’t mean you can re-assign it.  That’s when the immutability of the namedtuple asserts itself.

a.number = 1600
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
can't set attribute

 

One of the first things you should know about Python is, EVERYTHING IS AN OBJECT. That means that you can inherit from it. Boom. I just inherited from my named tuple, and now I can add items to the class to define how data is retrieved! Super nifty!

Wow.  Who cares, you say.  Big deal?  What are you, some type of wierdo?   Yes.  Yes, I am.

So, let’s say, that you have a database of addresses. And you want to get a printable mailing address for an entry? Well, you could easily apply the queried records from the database into our Address model (we’ll just use this name as a reference). Now, we have a whole list of these entries. Perhaps we’re having a party, and we want to mail invites to everyone.  This is where the whole nametuple inheritance gives you some pretty cool flexibility.

class Address(namedtuple("Address", ["number", "street", "city", "state", "zip_code"])):

    def __new__(cls, number=None, street=None, city=None, state=None, zip_code=None):
        return super(Address, cls).__new__(
            cls,
            number=number,
            street=street,
            city=city,
            state=state,
            zip_code=zip_code)
   
    def mailing_address(self):
      return ("{self.number} {self.street}\n"
              "{self.city}, {self.state} {self.zip_code}"
             ).format(**locals())

Nice and simple. Or perhaps you’re going to export your entire rolodex to csv (for some reason)?

class Address(namedtuple("Address", ["number", "street", "city", "state", "zip_code"])):

    def __new__(cls, number=None, street=None, city=None, state=None, zip_code=None):
        return super(Address, cls).__new__(
            cls,
            number=number,
            street=street,
            city=city,
            state=state,
            zip_code=zip_code)
   
    def mailing_address(self):
      return ("{self.number} {self.street}\n"
              "{self.city}, {self.state} {self.zip_code}"
             ).format(**locals())

    def to_csv(self):
        return ",".join(self._asdict().values()

 

Now you could just iterate over that list of Address entries and write out the to_csv() call!  It would likely look something like:

addresses = [all your address entries would be here]
with open("rolodex.csv", 'wb') as f:
    [f.write(a.to_csv() for a in addresses]

I’m not advocating for a replacement of traditional classes.

My opinion: Python classes should fall into one of three basic types:

  1. A class could contain data (such as our namedtuple example here)
  2. a class could provide encapsulation of action (like a database library).
  3. Or a class could provide actuation/transformation of data (kind of a mix of #1 and #2)

If you’re dealing with data which is merely going to be acted upon and never changed, then take a swing with these fancy namedtuples.  If you’re doing lots of actuation, write a normal class.   At all times, never should a good coder assume they know the best way to do something.  You’d be surprised how often when I make this assumption, some young, talented coder comes along and corrects my ego.