Python namedtuple (Take the tuple to the next level)

In this tutorial, we will dive deep into namedtuples: what they are, how to create and manipulate them, and when to use them (or not).

Namedtuples are a part of Python’s built-in collections module, and they provide a convenient way to bundle data under one name.

They are a subclass of Python’s built-in tuple data type, but come with a twist: namedtuples are tuples where each value (or “field”) can also be accessed by a unique, user-defined name.

Namedtuples are a great choice where you need to group related data together in a structured, immutable format.

 

 

Creating Namedtuples

Creating namedtuples in Python involving the namedtuple() factory function for creating tuple subclasses.

The namedtuple requires a typename and a field name as its parameters. The typename will be the name of the new class.

The field names can be provided as an iterable of strings or as a single space/comma-separated string.

from collections import namedtuple

# Define a Car namedtuple
Car = namedtuple('Car', 'brand model year color')

# Instantiate a Car
my_car = Car('Tesla', 'Model S', 2023, 'Red')
print(my_car)

Output:

Car(brand='Tesla', model='Model S', year=2023, color='Red')

In this code, we import the namedtuple class from the collections module and define a Car namedtuple with the field names ‘brand’, ‘model’, ‘year’, and ‘color’.

We then construct an instance of the Car namedtuple by passing the appropriate values.

Adding default values

From Python 3.7 onwards, the namedtuple function includes a defaults keyword argument to provide default values.

# Define a Car namedtuple with default color and year
Car = namedtuple('Car', 'brand model year color', defaults=('Unknown', 'Black'))

# Instantiate a Car without providing color and year
my_car = Car('Tesla', 'Model S')

print(my_car)

Output:

Car(brand='Tesla', model='Model S', year='Unknown', color='Black')

In the example above, we’ve provided default values for the ‘year’ and ‘color’ fields.

When we create a new Car namedtuple without specifying these fields, the defaults are used.

 

Accessing Elements

Once we have our namedtuple, we can access its elements in a few different ways.

Using index

Although namedtuples improve upon regular tuples by adding field names, we can still access elements using an index, just like a regular tuple.

from collections import namedtuple

# Define a Fruit namedtuple
Fruit = namedtuple('Fruit', ['name', 'color'])

# Create an instance of Fruit
apple = Fruit('Apple', 'Red')

print(apple[0])  # Access element by index

Output:

Apple

In this example, we access the name of the fruit using the index 0.

Using the field name (using dot notation)

One of the main features of namedtuples is the ability to access elements using field names. This is typically done using dot notation.

print(apple.name)

Output:

Apple

Here, we’re accessing the name of the fruit using its field name.

Using getattr()

Python’s built-in getattr() function allows us to access the value of an object’s attribute. In the case of namedtuples, we can use it to access field values.

print(getattr(apple, 'color'))

Output:

Red

 

Modifying a namedtuple (Using _replace() method)

Since namedtuples are immutable, we can’t directly modify them.

But there’s a workaround to this using the _replace() method, which returns a new instance with the replaced field(s).

from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)

# Modify the y-coordinate
p = p._replace(y=30)
print(p)

Output:

Point(x=10, y=30)

In this example, we replace the y-coordinate of the point from 20 to 30.

Converting namedtuple to a dictionary

To convert a namedtuple to a dictionary, use the _asdict() method. This is useful when you need key-value pairs from a namedtuple.

d = p._asdict()
print(d)

Output:

{'x': 10, 'y': 30}

Here, we convert our namedtuple to a dictionary, with the field names as keys and the corresponding values as values.

Using _fields method

The _fields attribute returns a tuple listing the field names. It can be useful when you need to iterate over all the fields of a namedtuple.

print(Point._fields)

Output:

('x', 'y')

Here, we’re printing the field names of the Point namedtuple.

Using _make

The _make() method allows us to create a new instance of a namedtuple from an iterable.

data = [100, 200]
new_point = Point._make(data)
print(new_point)

Output:

Point(x=100, y=200)

We create a new Point namedtuple from a list of values using the _make() method.

 

Iterating Over a Namedtuple

You can iterate over a namedtuple just like you would with a regular tuple. Here’s how:

from collections import namedtuple
Person = namedtuple('Person', ['name', 'age', 'city'])

# Create an instance of the namedtuple
person = Person('John Doe', 30, 'New York')

# Iterate over the namedtuple
for field in person:
    print(field)

Output:

John Doe
30
New York

In the above code, we define a namedtuple Person and create an instance person. We then iterate over person using a simple for loop, which prints each field in the namedtuple.

Iterate over field names and values

If you want to iterate over both the field names and values, you can convert the namedtuple to a dictionary and use the items() method:

from collections import namedtuple
Person = namedtuple('Person', ['name', 'age', 'city'])
p = Person('John Doe', 30, 'New York')
for field_name, field_value in p._asdict().items():
    print(f"{field_name}: {field_value}")

Output:

name: John Doe
age: 30
city: New York

In this code, we use the _asdict() method to convert the namedtuple to a dictionary, then use the items() method to get pairs of field names and values.

Using _fields() and getattr()

You can also use the _fields method in combination with getattr() to iterate over both field names and their corresponding values:

from collections import namedtuple
Person = namedtuple('Person', ['name', 'age', 'city'])
p = Person('John Doe', 30, 'New York')
for field_name in p._fields:
    print(f"{field_name}: {getattr(p, field_name)}")

Output:

name: John Doe
age: 30
city: New York

Here, the _fields method provides the field names, and getattr() is used to fetch the corresponding value for each field from the person namedtuple.

 

Sorting Namedtuples

You can sort a list of namedtuples similar to how you sort a list of tuples. The sorting is based on the natural order of the fields.

However, you can customize the sorting using the itemgetter function from the operator module.

from collections import namedtuple
from operator import itemgetter
Fruit = namedtuple('Fruit', ['name', 'color'])

# Create a list of fruits
fruits = [Fruit('Apple', 'Red'), Fruit('Banana', 'Yellow'), Fruit('Cherry', 'Red')]

# Sort the list of fruits by color
sorted_fruits = sorted(fruits, key=itemgetter(1))
for fruit in sorted_fruits:
    print(fruit)

Output:

Fruit(name='Apple', color='Red')
Fruit(name='Cherry', color='Red')
Fruit(name='Banana', color='Yellow')

In this example, we define a Fruit namedtuple and create a list of Fruit instances. We then sort this list by the ‘color’ field using the itemgetter function.

 

Namedtuples vs Tuples, Lists, and Dictionaries

Let’s compare the efficiency of these data types.

Our benchmark will focus on the creation, memory usage, and simple access of these data structures.

Here’s how we might approach it:

  1. We’ll create a Namedtuple, Tuple, List, and Dictionary, each containing the same elements.
  2. We’ll measure the time it takes to create each of these structures.
  3. We’ll measure the memory usage of each of these structures.
  4. We’ll measure the time it takes to access an element in each of these structures.
import sys
import timeit
from collections import namedtuple

# The size of the data structure
SIZE = 1000000
data = [(i, 'data{}'.format(i)) for i in range(SIZE)]

# Define the namedtuple type
DataNT = namedtuple('DataNT', 'id value')

# Conversion factor from seconds to milliseconds
SEC_TO_MSEC = 1000

# Create and measure time and memory for tuple
start = timeit.default_timer()
tuple_data = tuple(data)
tuple_time = (timeit.default_timer() - start) * SEC_TO_MSEC
tuple_mem = sys.getsizeof(tuple_data)

# Create and measure time and memory for namedtuple
start = timeit.default_timer()
namedtuple_data = tuple(DataNT(*d) for d in data)
namedtuple_time = (timeit.default_timer() - start) * SEC_TO_MSEC
namedtuple_mem = sys.getsizeof(namedtuple_data)

# Create and measure time and memory for list
start = timeit.default_timer()
list_data = list(data)
list_time = (timeit.default_timer() - start) * SEC_TO_MSEC
list_mem = sys.getsizeof(list_data)

# Create and measure time and memory for dictionary
start = timeit.default_timer()
dict_data = dict(data)
dict_time = (timeit.default_timer() - start) * SEC_TO_MSEC
dict_mem = sys.getsizeof(dict_data)

# Access and measure time for tuple
start = timeit.default_timer()
_ = tuple_data[SIZE//2]
tuple_access_time = (timeit.default_timer() - start) * SEC_TO_MSEC

# Access and measure time for namedtuple
start = timeit.default_timer()
_ = namedtuple_data[SIZE//2]
namedtuple_access_time = (timeit.default_timer() - start) * SEC_TO_MSEC

# Access and measure time for list
start = timeit.default_timer()
_ = list_data[SIZE//2]
list_access_time = (timeit.default_timer() - start) * SEC_TO_MSEC

# Access and measure time for dictionary
start = timeit.default_timer()
_ = dict_data[SIZE//2]
dict_access_time = (timeit.default_timer() - start) * SEC_TO_MSEC


results = {
    "tuple": {"creation_time_ms": tuple_time, "memory": tuple_mem, "access_time_ms": tuple_access_time},
    "namedtuple": {"creation_time_ms": namedtuple_time, "memory": namedtuple_mem, "access_time_ms": namedtuple_access_time},
    "list": {"creation_time_ms": list_time, "memory": list_mem, "access_time_ms": list_access_time},
    "dict": {"creation_time_ms": dict_time, "memory": dict_mem, "access_time_ms": dict_access_time},
}
print(results)

The results of the benchmark test:

Data Structure Creation Time (ms) Memory Usage (bytes) Access Time (ms)
Tuple 13.2023 8000040 0.0022
NamedTuple 1601.5981 8000040 0.0016
List 12.2202 8000056 0.0013
Dictionary 74.7433 41943136 0.0021

Lists and namedtuples provide the fastest access to elements. This is because they store elements in a contiguous block of memory.

 

Namedtuples in Classes

Namedtuples can be utilized within classes to enhance code readability and organization. Let’s create a class that uses a namedtuple.

from collections import namedtuple
Person = namedtuple('Person', ['name', 'age'])

class Classroom:
    def __init__(self):
        self.students = []

    def add_student(self, name, age):
        student = Person(name, age)
        self.students.append(student)

classroom = Classroom()
classroom.add_student('John', 16)
classroom.add_student('Emma', 17)
for student in classroom.students:
    print(student)

Output:

Person(name='John', age=16)
Person(name='Emma', age=17)

In this example, we have a Classroom class that uses the Person namedtuple to store information about students.

The add_student method creates a new Person namedtuple and adds it to the students list.

 

Namedtuples in Functions

Namedtuples can be returned from functions, which can improve the readability of your code by giving names to the elements of the tuple.

Let’s create a function that returns a namedtuple.

from collections import namedtuple
Result = namedtuple('Result', ['success', 'data'])
def fetch_data():
    data = "sample data"
    success = True
    return Result(success, data)
result = fetch_data()
print(result)

Output:

Result(success=True, data='sample data')

Here, we have a fetch_data function that returns a Result namedtuple, consisting of a boolean indicating whether the data fetch was successful, and the data itself.

 

Nesting Namedtuples

Namedtuples can also be nested to create more complex data structures. This can increase the readability of your code.

from collections import namedtuple
Address = namedtuple('Address', ['street', 'city', 'state', 'zip_code'])
Person = namedtuple('Person', ['name', 'age', 'address'])

# Create an Address instance
home_address = Address('123 Main St', 'Springfield', 'IL', '62701')

# Create a Person instance with a nested Address
person = Person('John Doe', 30, home_address)
print(person)

Output:

Person(name='John Doe', age=30, address=Address(street='123 Main St', city='Springfield', state='IL', zip_code='62701'))

In this example, we define an Address namedtuple and a Person namedtuple that includes an address field.

This allows us to nest an Address instance within a Person instance, creating a more structured and readable representation of the data.

 

Namedtuple vs. Tuple

Below is a comparison table between namedtuple and tuple in Python.

Aspect namedtuple tuple
Access method By field name or index or using getattr() By index only
Code readability High (each value has a name) Low (need to remember order of values)
Memory usage More than tuples (due to metadata) Less than namedtuples
Flexibility Fixed once defined Can include any values
Can be used as dict key Yes Yes
Mutability Immutable Immutable
Method support Has several helpful methods Has only count() and index() methods

 

Namedtuple vs. Dataclass

Below is a comparison table between namedtuple and dataclass in Python.

Aspect namedtuple dataclass
Introduced in Python 2.6 Python 3.7
Access method By field name or index By field name
Mutability Immutable Both mutable and immutable versions
Default values Not supported natively Supported
Methods Few built-in methods Can define custom methods
Inheritance Can be extended using subclasses Supports standard Python inheritance
Code readability High (each value has a name) High (each value has a name)
Memory usage Less than dataclasses More than namedtuples
Type hints Not supported natively Supported
Usage Better for simpler use cases Better for complex use cases

 

Practical examples of where to use namedtuples

Grouping data & records: Namedtuples can be an effective way to group related data together.

For instance, if you have various data related to a car such as the model, make, year, and color, you can group these data in a namedtuple.

from collections import namedtuple
Car = namedtuple('Car', ['make', 'model', 'year', 'color'])
car = Car('Toyota', 'Camry', 2018, 'Blue')
print(car)

Output:

Car(make='Toyota', model='Camry', year=2018, color='Blue')

In this example, a Car namedtuple is used to group data related to a specific car.

Returning multiple values from functions: Functions in Python can return multiple values, and often, these values are returned as a tuple.

Using a namedtuple instead of a regular tuple can provide more context to the returned values, improving readability.

from collections import namedtuple
from math import pi
CircleMetrics = namedtuple('CircleMetrics', ['area', 'circumference'])
def calculate_circle_metrics(radius):
    area = pi * radius * radius
    circumference = 2 * pi * radius
    return CircleMetrics(area, circumference)
metrics = calculate_circle_metrics(5)
print(metrics)

Output:

CircleMetrics(area=78.53981633974483, circumference=31.41592653589793)

In this example, the function calculate_circle_metrics calculates the area and circumference of a circle and returns a CircleMetrics namedtuple containing both values.

 

Examples of when not to use namedtuples

While namedtuples are useful and versatile, there are certain situations when other data structures might be a better fit.

  • When data needs to be mutable: Since namedtuples are immutable, they’re not suitable if you need a data structure where the values can be modified. In such cases, dictionaries or lists might be a better fit.
  • When you need a more complex data structure: While namedtuples can be nested, if you need a more complex or custom data structure, you might need to define your own class.

For instance, let’s take an example where we want to represent a student where we would need to add subjects dynamically. In this case, a dictionary would be a more suitable choice.

student = {
    'name': 'John Doe',
    'age': 16,
    'subjects': ['English', 'Math', 'Science']
}

# Add a new subject
student['subjects'].append('History')
print(student)

Output:

{'name': 'John Doe', 'age': 16, 'subjects': ['English', 'Math', 'Science', 'History']}

In this example, a student’s subjects are stored in a list inside a dictionary, allowing you to dynamically add or remove subjects.

Leave a Reply

Your email address will not be published. Required fields are marked *