Adding docstrings to namedtuples?

15,574

Solution 1

You can achieve this by creating a simple, empty wrapper class around the returned value from namedtuple. Contents of a file I created (nt.py):

from collections import namedtuple

Point_ = namedtuple("Point", ["x", "y"])

class Point(Point_):
    """ A point in 2d space """
    pass

Then in the Python REPL:

>>> print nt.Point.__doc__
 A point in 2d space 

Or you could do:

>>> help(nt.Point)  # which outputs...
Help on class Point in module nt:

class Point(Point)
 |  A point in 2d space
 |  
 |  Method resolution order:
 |      Point
 |      Point
 |      __builtin__.tuple
 |      __builtin__.object
 ...

If you don't like doing that by hand every time, it's trivial to write a sort-of factory function to do this:

def NamedTupleWithDocstring(docstring, *ntargs):
    nt = namedtuple(*ntargs)
    class NT(nt):
        __doc__ = docstring
    return NT

Point3D = NamedTupleWithDocstring("A point in 3d space", "Point3d", ["x", "y", "z"])

p3 = Point3D(1,2,3)

print p3.__doc__

which outputs:

A point in 3d space

Solution 2

In Python 3, no wrapper is needed, as the __doc__ attributes of types is writable.

from collections import namedtuple

Point = namedtuple('Point', 'x y')
Point.__doc__ = '''\
A 2-dimensional coordinate

x - the abscissa
y - the ordinate'''

This closely corresponds to a standard class definition, where the docstring follows the header.

class Point():
    '''A 2-dimensional coordinate

    x - the abscissa
    y - the ordinate'''
    <class code>

This does not work in Python 2.

AttributeError: attribute '__doc__' of 'type' objects is not writable.

Solution 3

Came across this old question via Google while wondering the same thing.

Just wanted to point out that you can tidy it up even more by calling namedtuple() right from the class declaration:

from collections import namedtuple

class Point(namedtuple('Point', 'x y')):
    """Here is the docstring."""

Solution 4

Is it possible to add a documentation string to a namedtuple in an easy manner?

Yes, in several ways.

Subclass typing.NamedTuple - Python 3.6+

As of Python 3.6 we can use a class definition with typing.NamedTuple directly, with a docstring (and annotations!):

from typing import NamedTuple

class Card(NamedTuple):
    """This is a card type."""
    suit: str
    rank: str

Compared to Python 2, declaring empty __slots__ is not necessary. In Python 3.8, it isn't necessary even for subclasses.

Note that declaring __slots__ cannot be non-empty!

In Python 3, you can also easily alter the doc on a namedtuple:

NT = collections.namedtuple('NT', 'foo bar')

NT.__doc__ = """:param str foo: foo name
:param list bar: List of bars to bar"""

Which allows us to view the intent for them when we call help on them:

Help on class NT in module __main__:

class NT(builtins.tuple)
 |  :param str foo: foo name
 |  :param list bar: List of bars to bar
...

This is really straightforward compared to the difficulties we have accomplishing the same thing in Python 2.

Python 2

In Python 2, you'll need to

  • subclass the namedtuple, and
  • declare __slots__ == ()

Declaring __slots__ is an important part that the other answers here miss .

If you don't declare __slots__ - you could add mutable ad-hoc attributes to the instances, introducing bugs.

class Foo(namedtuple('Foo', 'bar')):
    """no __slots__ = ()!!!"""

And now:

>>> f = Foo('bar')
>>> f.bar
'bar'
>>> f.baz = 'what?'
>>> f.__dict__
{'baz': 'what?'}

Each instance will create a separate __dict__ when __dict__ is accessed (the lack of __slots__ won't otherwise impede the functionality, but the lightweightness of the tuple, immutability, and declared attributes are all important features of namedtuples).

You'll also want a __repr__, if you want what is echoed on the command line to give you an equivalent object:

NTBase = collections.namedtuple('NTBase', 'foo bar')

class NT(NTBase):
    """
    Individual foo bar, a namedtuple

    :param str foo: foo name
    :param list bar: List of bars to bar
    """
    __slots__ = ()

a __repr__ like this is needed if you create the base namedtuple with a different name (like we did above with the name string argument, 'NTBase'):

    def __repr__(self):
        return 'NT(foo={0}, bar={1})'.format(
                repr(self.foo), repr(self.bar))

To test the repr, instantiate, then test for equality of a pass to eval(repr(instance))

nt = NT('foo', 'bar')
assert eval(repr(nt)) == nt

Example from the documentation

The docs also give such an example, regarding __slots__ - I'm adding my own docstring to it:

class Point(namedtuple('Point', 'x y')):
    """Docstring added here, not in original"""
    __slots__ = ()
    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

...

The subclass shown above sets __slots__ to an empty tuple. This helps keep memory requirements low by preventing the creation of instance dictionaries.

This demonstrates in-place usage (like another answer here suggests), but note that the in-place usage may become confusing when you look at the method resolution order, if you're debugging, which is why I originally suggested using Base as a suffix for the base namedtuple:

>>> Point.mro()
[<class '__main__.Point'>, <class '__main__.Point'>, <type 'tuple'>, <type 'object'>]
                # ^^^^^---------------------^^^^^-- same names!        

To prevent creation of a __dict__ when subclassing from a class that uses it, you must also declare it in the subclass. See also this answer for more caveats on using __slots__.

Solution 5

Since Python 3.5, docstrings for namedtuple objects can be updated.

From the whatsnew:

Point = namedtuple('Point', ['x', 'y'])
Point.__doc__ += ': Cartesian coodinate'
Point.x.__doc__ = 'abscissa'
Point.y.__doc__ = 'ordinate'
Share:
15,574
Rickard
Author by

Rickard

Interested in math, finance and functional programming

Updated on June 23, 2022

Comments

  • Rickard
    Rickard about 2 years

    Is it possible to add a documentation string to a namedtuple in an easy manner?

    I tried

    from collections import namedtuple
    
    Point = namedtuple("Point", ["x", "y"])
    """
    A point in 2D space
    """
    
    # Yet another test
    
    """
    A(nother) point in 2D space
    """
    Point2 = namedtuple("Point2", ["x", "y"])
    
    print Point.__doc__ # -> "Point(x, y)"
    print Point2.__doc__ # -> "Point2(x, y)"
    

    but that doesn't cut it. Is it possible to do in some other way?

  • Taylor D. Edmiston
    Taylor D. Edmiston over 9 years
    Note: This is only valid for Python 3. In Python 2: AttributeError: attribute '__doc__' of 'type' objects is not writable.
  • BoltzmannBrain
    BoltzmannBrain about 7 years
    Although not as concise and clear as the other answers, this should be the accepted answer because it highlights the importance of __slots__. Without it, you're losing the lightweight value of a namedtuple.
  • BoltzmannBrain
    BoltzmannBrain about 7 years
    Important that you include __slots__ = () in the class. Otherwise you create a __dict__ for your attrs, losing the lightweight nature of namedtuple.
  • exhuma
    exhuma almost 7 years
    Won't subclassing convert the namedtuple into a full-fledged "object"? Thereby losing some of the performance gains from named-tuples?
  • ali_m
    ali_m over 6 years
    If you add __slots__ = () to the derived subclass you can retain the memory and performance advantages of using namedtuple
  • nbedou
    nbedou about 6 years
    I obtain "NameError: name 'NamedTuple' is not defined"
  • nodakai
    nodakai almost 6 years
  • Bachsau
    Bachsau almost 5 years
    It still adds another level to the MRO, which is not justified for a docstring. However, one can simply assign to __doc__ and have a customized docstring saved in the original object.