56

I have a list of namedtuples named Books and am trying to increase the price field by 20% which does change the value of Books. I tried to do:

from collections import namedtuple
Book = namedtuple('Book', 'author title genre year price instock')
BSI = [
       Book('Suzane Collins','The Hunger Games', 'Fiction', 2008, 6.96, 20),
       Book('J.K. Rowling', "Harry Potter and the Sorcerer's Stone", 'Fantasy', 1997, 4.78, 12)]
for item in BSI:
    item = item.price*1.10
print(item.price)

But I keep getting :

 Traceback (most recent call last):
 print(item.price)
 AttributeError: 'float' object has no attribute 'price'

I understand that I cannot set the fields in a namedtuple. How do I go about updating price?

I tried to make it into a function:

def restaurant_change_price(rest, newprice):
    rest.price = rest._replace(price = rest.price + newprice)
    return rest.price

print(restaurant_change_price(Restaurant("Taillevent", "French", "343-3434", "Escargots", 24.50), 25))

but I get an error with replace saying:

 rest.price = rest._replace(price = rest.price + newprice)
 AttributeError: can't set attribute

Can someone let me know why this is happening?

Leon Surrao
  • 637
  • 1
  • 6
  • 12

3 Answers3

79

Named tuples are immutable, so you cannot manipulate them.

Right way of doing it:

If you want something mutable, you can use recordtype.

from recordtype import recordtype

Book = recordtype('Book', 'author title genre year price instock')
books = [
   Book('Suzane Collins','The Hunger Games', 'Fiction', 2008, 6.96, 20),
   Book('J.K. Rowling', "Harry Potter and the Sorcerer's Stone", 'Fantasy', 1997, 4.78, 12)]

for book in books:
    book.price *= 1.1
    print(book.price)

PS: You may need to pip install recordtype if you don't have it installed.

Bad way of doing it:

You may also keep using namedtuple with using the _replace() method.

from collections import namedtuple

Book = namedtuple('Book', 'author title genre year price instock')
books = [
   Book('Suzane Collins','The Hunger Games', 'Fiction', 2008, 6.96, 20),
   Book('J.K. Rowling', "Harry Potter and the Sorcerer's Stone", 'Fantasy', 1997, 4.78, 12)]

for i in range(len(books)):
    books[i] = books[i]._replace(price = books[i].price*1.1)
    print(books[i].price)
Sait
  • 19,045
  • 18
  • 72
  • 99
  • sorry.. It was supposed to be BSI not Books.. I edited the question. – Leon Surrao Jul 06 '15 at 19:53
  • Is there a way to achieve the same using the _replace method? – Leon Surrao Jul 07 '15 at 16:59
  • 2
    @LeonSurrao I updated the post with `_replace()` method as you wish. – Sait Jul 08 '15 at 05:38
  • 13
    I don't understand the python community... That `namedtuple._replace()` is pretty idiomatic in functional languages, why is that "private" here!? It really shouldn't be a bad way to do it, it's certainly better than to convert to mutable madness – mike3996 Jul 08 '15 at 06:03
  • @progo Assume you have millions of items in your *tuple*. If you use `_replace`, you need to copy all items again to another *tuple* and trash the old one. That doesn't sound like the best way to me. – Sait Jul 08 '15 at 06:08
  • @Sait: only the references are copied, and tuples aren't designed to be large varying-length lists for that matter. It may not be ideal in Python but there's a reason people prefer immutability in data mangling. – mike3996 Jul 08 '15 at 06:19
  • @Sait. I made a function out of it that just takes a restaurant object. But I kept getting the same Attribute error. Can you tell me what am I doing wrong? I updated the question. – Leon Surrao Jul 08 '15 at 19:13
  • 2
    Instead of `rest.price = rest._replace(price = rest.price + newprice)`, do `rest = rest._replace(price = rest.price + newprice)`. Note that `_replace()` returns you a new instance of `namedtuple`. – Sait Jul 08 '15 at 19:16
  • 18
    @progo,`_replace` is actually not "private" or internal. based on [this](https://docs.python.org/3/library/collections.html#collections.namedtuple) doc: _To prevent conflicts with field names, the method and attribute names start with an underscore._ – Yoel Gluschnaider Apr 07 '17 at 15:02
  • [recordtype](https://pypi.org/project/recordtype/) looks nice, but unfortunately it [only supports Python 2.x](https://bitbucket.org/ericvsmith/recordtype/pull-requests/1/add-python-3-support/) (as of 2018-07), and the question is explicitly tagged 3.x. – Henrik Heimbuerger Jul 19 '18 at 11:09
  • As a Python 3 alternative to `recordtype`, I've switched to [namedlist](https://pypi.org/project/namedlist/), which works pretty much exactly the same and has Python 3 out of the box. – Henrik Heimbuerger Jul 19 '18 at 11:23
  • 2
    @Sait "Assume you have millions of items in your tuple." This is not a likely scenario, is it? – quant_dev Oct 09 '18 at 11:26
  • What is meant by "immutable" in the context of namedtuples? The value of a field of a namedtuple object can't be modified/changed? – Minh Tran Oct 22 '18 at 05:15
  • The reason _replace() is "privateish" is to avoid conflicting with attributes defined by the user. For that reason, namedtuples do not define any methods that don't exist on tuple (count and index). IMHO A better API choice here would be to have all the namedtuple helper functions in their own namespace rather than as member functions. – Evan May 20 '19 at 19:28
  • dataclass is a newer, built-in to Python way to get this done. Hence, the answer below by @vlad-belzen should be preferred, IMHO. See PEP 557 for dataclass introduction, https://www.python.org/dev/peps/pep-0557, it mentions recordtype as a valuable module that is incorporated in the functionality of dataclass. – pauljohn32 May 29 '20 at 17:12
  • Considering that `_replace` is not private, it is actually a very good solution, because it keeps immutability, which is indeed a standard pattern in functional programming. The "right" solution is an external dependency and introduces mutability. Depending on the use case, the answer gets "right" and "bad" wrong. It would be better to just state pros/cons instead of making opinionated judgements. – bluenote10 Sep 20 '20 at 07:56
59

In Python >= 3.7 you can use dataclass decorator with the new variable annotations feature to produce mutable record types:

from dataclasses import dataclass


@dataclass
class Book:
    author: str
    title: str
    genre: str
    year: int
    price: float
    instock: int


BSI = [
    Book("Suzane Collins", "The Hunger Games", "Fiction", 2008, 6.96, 20),
    Book(
        "J.K. Rowling",
        "Harry Potter and the Sorcerer's Stone",
        "Fantasy",
        1997,
        4.78,
        12,
    ),
]

for item in BSI:
    item.price *= 1.10
    print(f"New price for '{item.title}' book is {item.price:,.2f}")

Output:

New price for 'The Hunger Games' book is 7.66
New price for 'Harry Potter and the Sorcerer's Stone' book is 5.26
Vlad Bezden
  • 83,883
  • 25
  • 248
  • 179
  • 7
    I wish I could give more upvotes--this maintains the simplicity of a `namedtuple`, adds the mutability of a class, and does not require any other packages, lightweight or heavy. – hlongmore Jul 22 '20 at 21:49
  • Best answer, uses only builtins. Exactly what dataclass is for. – Zim Jun 11 '22 at 20:16
3

This looks like a task for Python's data analysis library, pandas. It's really, really easy to do this sort of thing:

In [6]: import pandas as pd
In [7]: df = pd.DataFrame(BSI, columns=Book._fields)
In [8]: df
Out[8]: 
           author                                  title    genre  year  \
0  Suzane Collins                       The Hunger Games  Fiction  2008   
1    J.K. Rowling  Harry Potter and the Sorcerers Stone  Fantasy  1997   

   price  instock  
0   6.96       20  
1   4.78       12  

In [9]: df['price'] *= 100
In [10]: df
Out[10]: 
           author                                  title    genre  year  \
0  Suzane Collins                       The Hunger Games  Fiction  2008   
1    J.K. Rowling  Harry Potter and the Sorcerer's Stone  Fantasy  1997   

   price  instock  
0    696       20  
1    478       12  

Now isn't that just much, much better than labouring with namedtuples?

LondonRob
  • 73,083
  • 37
  • 144
  • 201