If thats all your using your classes for, then a named tuple is probably a better solution, or a dataclass. Though I normally just use dicts in that situation. If I see someone create a class without any methods, or atleast planned methods, I don't let it through code review.
EDIT: Also, Raymond Hettinger created named tuples. I'm not normally one for call to authority, or hero worship, but I am a huge fan of his. I recommend that anyone interested in Python should watch as many of his talks as they can.
EDIT2: As masklinn pointed out, another really good use of named tuples is when you're already returning a tuple, and you realize it would be better if it had names. You could change it to a named tuple without breaking any of the existing code. Unless they're doing something dumb like halfassing type checking at runtime. (this use case is in the article, which i didn't read at first)
Well in and of itself none, in the sense that anything a namedtuple can do you could do by hand (it really just defines a class). However namedtuple:
* extends tuples, so a namedtuple is literally a tuple (which is useful)
* sets up a bunch of properties for the "named fields", which are basically just names on the tuple elements
* sets up a few other utility methods e.g. nice formatting, `_make`, `_asdict`, `_replace`
Now the latter two are nice, and mostly replicated by dataclasses (or attrs). The first one is the raison d'être of namedtuples though: originally their purpose is to "upgrade" tuple return values into richer / clearer types e.g. urlparse originally returned a 6-utple which is not necessarily super wieldy / clear, you can probably infer that the 3rd element is the path but… after upgrading to namedtuple it's just `result.path which is usually much clearer.
And because namedtuples are still classes in and of themselves, you can inherit from them to create a class with a `__dict__` with relative ease.
I feel like none of the sibling answers actually answer your question which is "absolutely nothing." The function namedtuple is code generator that constructs a class definition and then eval()'s it.
The reason you reach for it is because it's tedious to write the same methods over and over to get things like a nice repr, methods covert between dicts, or pickling support.
The source from Python 3.6 is much more readable than 3.9 so I recommend reading that if you want to see how it works.
NamedTuple or namedtuple instances are tuple instances that have the same properties that regular tuples have. They are immutable (you cannot reassign their fields), you can index into them (a[0], a[1] instead of a.x and a.y), you can unpack them with *a. They can have methods like regular classes can, including additional @property methods. A NamedTuple class cannot inherit from another class, not even other named tuples.