Class Helpers

Named Tuples

Whenever you need to wrap some values together, think of NamedTuple:

from typing import NamedTuple


class Location(NamedTuple):
    name: str
    lat: float
    long: float

The class this creates has an initializer which sets attributes on the Location object:

>>> location = Location('San Diego', 32.733999, -117.147777)
>>> location.name
'San Diego'

And it has a nice default string representation:

>>> location
Location(name='San Diego', lat=32.733999, long=-117.147777)

And like a tuple, you can iterate over (and therefore unpack) Location objects:

>>> name, lat, long = Location('San Diego', 32.733999, -117.147777)
>>> name
'San Diego'

Also like tuples, we cannot modify namedtuple objects (they’re immutable):

>>> location.name = 'Los Angeles'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

Also Location objects can be compared to each other using equality (if the values they contain are the same, they will be seen as “equal”):

>>> location2 = Location('San Diego', 32.733999, -117.147777)
>>> location == location2
True
>>> location3 = Location('Los Angeles', 32.733999, -117.147777)
>>> location == location3
False

Note

There’s also a function version of NamedTuple, in the collections module:

from collections import namedtuple

Location = namedtuple('Location', 'name lat long')

This is a function which returns a class. I find that a bit confusing and non-obvious, so I recommend the class version of NamedTuple in the typing module instead.

Data Classes

If you like the idea of named tuples, but you want more flexibility (something that helps you generate a class but doesn’t make an immutable tuple), definitely look into dataclasses.

The dataclasses module was added in Python 3.7. Data classes are basically a factory for making friendly classes without having to write your own boilerplate code for many common methods. It is (superficially) a bit similar to the way NamedTuple works, but is more powerful and very customizable.

Here’s a data class that is similar to the Location named tuple we made earlier:

from dataclasses import dataclass


@dataclass
class Location:
    name: str
    lat: float
    long: float
>>> location = Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> location
Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> location.name
'San Diego'

This class has a default initializer, string representation, and support for equality.

It isn’t iterable by default, though we could add iterability by adding a __iter__ method:

from dataclasses import astuple, dataclass


@dataclass
class Location:
    name: str
    lat: float
    long: float
    def __iter__(self):
        yield from astuple(self)
>>> location = Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> name, lat, long = location
>>> name
'San Diego'

And unlike NamedTuple, data classes are mutable by default:

>>> location.name = 'Los Angeles'
>>> location
Location(name='Los Angeles', lat=32.733999, long=-117.147777)

If you want to make your Location objects immutable, you can pass a frozen attribute to the dataclass decorator:

from dataclasses import astuple, dataclass


@dataclass(frozen=True)
class Location:
    name: str
    lat: float
    long: float
    def __iter__(self):
        yield from astuple(self)

Now our location objects are immutable:

>>> location = Location(name='San Diego', lat=32.733999, long=-117.147777)
>>> location.name = 'Los Angeles'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'name'

The new dataclasses library is essentially a more stripped-down version of the third-party attrs library. If you want more functionality, look into attrs as well.

When would you use these?

If you find yourself passing around tuples where each element represents a certain value, you could consider using a NamedTuple instead.

For example this function:

def split_name(name):
    first, last = name.rsplit(" ", 1)
    return first, last

Always returns a tuple of two values:

>>> split_name("Trey Hunner")
('Trey', 'Hunner')

We could instead use a NamedTuple instead:

from typing import NamedTuple

class Name(NamedTuple):
    """Class to store first and last names."""
    first: str
    last: str

def split_name(name):
    first, last = name.rsplit(" ", 1)
    return Name(first, last)

This will make our code more descriptive but it’ll also mean that if someone inspects the tuple returned from this function, they’ll more easily understand what it represents:

>>> split_name("Trey Hunner")
Name(first='Trey', last='Hunner')

I’d recommend dataclasses whenever you’d like to make a class with a few attributes to store data, but you don’t necessarily need a tuple.

Practically, in almost all cases where you could use a named tuple, I’d recommend using a dataclass instead. Dataclasses are friendly classes to work with, but they aren’t tuple-like, so they don’t have as many weird quirks as named tuples.

For example, here are some of the tuple-inherited quirks of the the Name named tuple above:

>>> name = Name("Trey", "Hunner")
>>> len(name)
2
>>> name + name
('Trey', 'Hunner', 'Trey', 'Hunner')
>>> name * 3
('Trey', 'Hunner', 'Trey', 'Hunner', 'Trey', 'Hunner')
>>> name[1:]
('Hunner',)
>>> "Trey" in name
True

Some of those might be nice-to-have features, but you could always implement the few nice-to-have features yourself using dunder methods.

For more on dataclasses, NamedTuple, and the third-part attrs library, see my talk, Easier Classes: Python Classes Without All The Cruft.

Dataclass Exercises

Ice Cream DataClasses

Rewrite the Size, Flavor, and IceCream classes in classes.py using data classes.

You can test each of these classes by running tests for Size, Flavor, and IceCream.