Adding docstrings to namedtuples?
Asked Answered
M

11

96

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?

Muncy answered 22/10, 2009 at 10:55 Comment(0)
C
55

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
Callan answered 22/10, 2009 at 11:3 Comment(3)
Won't subclassing convert the namedtuple into a full-fledged "object"? Thereby losing some of the performance gains from named-tuples?Guildroy
If you add __slots__ = () to the derived subclass you can retain the memory and performance advantages of using namedtupleLabonte
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.Schreck
H
75

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.

Hootman answered 4/12, 2013 at 23:43 Comment(0)
G
68

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."""
Garish answered 27/3, 2013 at 19:30 Comment(1)
Important that you include __slots__ = () in the class. Otherwise you create a __dict__ for your attrs, losing the lightweight nature of namedtuple.Tavis
C
55

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
Callan answered 22/10, 2009 at 11:3 Comment(3)
Won't subclassing convert the namedtuple into a full-fledged "object"? Thereby losing some of the performance gains from named-tuples?Guildroy
If you add __slots__ = () to the derived subclass you can retain the memory and performance advantages of using namedtupleLabonte
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.Schreck
C
39

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__.

Conservative answered 17/2, 2015 at 18:19 Comment(1)
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.Tavis
T
8

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'
Targe answered 4/9, 2016 at 19:20 Comment(0)
S
6

In Python 3.6+ you can use:

class Point(NamedTuple):
    """
    A point in 2D space
    """
    x: float
    y: float
Stellastellar answered 7/3, 2018 at 13:40 Comment(2)
I obtain "NameError: name 'NamedTuple' is not defined"Beckiebeckley
@nbedou docs.python.org/3/library/typing.html#typing.NamedTupleTepic
J
3

No need to use a wrapper class as suggested by the accepted answer. Simply literally add a docstring:

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
Point.__doc__="A point in 2D space"

This results in: (example using ipython3):

In [1]: Point?
Type:       type
String Form:<class '__main__.Point'>
Docstring:  A point in 2D space

In [2]: 

Voilà!

Jepum answered 24/9, 2014 at 12:48 Comment(1)
Note: This is only valid for Python 3. In Python 2: AttributeError: attribute '__doc__' of 'type' objects is not writable.Transmontane
M
1

You could concoct your own version of the namedtuple factory function by Raymond Hettinger and add an optional docstring argument.  However it would be easier -- and arguably better -- to just define your own factory function using the same basic technique as in the recipe.  Either way, you'll end up with something reusable.

from collections import namedtuple

def my_namedtuple(typename, field_names, verbose=False,
                 rename=False, docstring=''):
    '''Returns a new subclass of namedtuple with the supplied
       docstring appended to the default one.

    >>> Point = my_namedtuple('Point', 'x, y', docstring='A point in 2D space')
    >>> print Point.__doc__
    Point(x, y):  A point in 2D space
    '''
    # create a base class and concatenate its docstring and the one passed
    _base = namedtuple(typename, field_names, verbose, rename)
    _docstring = ''.join([_base.__doc__, ':  ', docstring])

    # fill in template to create a no-op subclass with the combined docstring
    template = '''class subclass(_base):
        %(_docstring)r
        pass\n''' % locals()

    # execute code string in a temporary namespace
    namespace = dict(_base=_base, _docstring=_docstring)
    try:
        exec template in namespace
    except SyntaxError, e:
        raise SyntaxError(e.message + ':\n' + template)

    return namespace['subclass']  # subclass object created
Morphosis answered 28/7, 2010 at 4:23 Comment(0)
L
0

I created this function to quickly create a named tuple and document the tuple along with each of its parameters:

from collections import namedtuple


def named_tuple(name, description='', **kwargs):
    """
    A named tuple with docstring documentation of each of its parameters
    :param str name: The named tuple's name
    :param str description: The named tuple's description
    :param kwargs: This named tuple's parameters' data with two different ways to describe said parameters. Format:
        <pre>{
            str: ( # The parameter's name
                str, # The parameter's type
                str # The parameter's description
            ),
            str: str, # The parameter's name: the parameter's description
            ... # Any other parameters
        }</pre>
    :return: collections.namedtuple
    """
    parameter_names = list(kwargs.keys())

    result = namedtuple(name, ' '.join(parameter_names))

    # If there are any parameters provided (such that this is not an empty named tuple)
    if len(parameter_names):
        # Add line spacing before describing this named tuple's parameters
        if description is not '':
            description += "\n"

        # Go through each parameter provided and add it to the named tuple's docstring description
        for parameter_name in parameter_names:
            parameter_data = kwargs[parameter_name]

            # Determine whether parameter type is included along with the description or
            # if only a description was provided
            parameter_type = ''
            if isinstance(parameter_data, str):
                parameter_description = parameter_data
            else:
                parameter_type, parameter_description = parameter_data

            description += "\n:param {type}{name}: {description}".format(
                type=parameter_type + ' ' if parameter_type else '',
                name=parameter_name,
                description=parameter_description
            )

            # Change the docstring specific to this parameter
            getattr(result, parameter_name).__doc__ = parameter_description

    # Set the docstring description for the resulting named tuple
    result.__doc__ = description

    return result

You can then create a new named tuple:

MyTuple = named_tuple(
    "MyTuple",
    "My named tuple for x,y coordinates",
    x="The x value",
    y="The y value"
)

Then instantiate the described named tuple with your own data, ie.

t = MyTuple(4, 8)
print(t) # prints: MyTuple(x=4, y=8)

When executing help(MyTuple) via the python3 command line the following is shown:

Help on class MyTuple:

class MyTuple(builtins.tuple)
 |  MyTuple(x, y)
 |
 |  My named tuple for x,y coordinates
 |
 |  :param x: The x value
 |  :param y: The y value
 |
 |  Method resolution order:
 |      MyTuple
 |      builtins.tuple
 |      builtins.object
 |
 |  Methods defined here:
 |
 |  __getnewargs__(self)
 |      Return self as a plain tuple.  Used by copy and pickle.
 |
 |  __repr__(self)
 |      Return a nicely formatted representation string
 |
 |  _asdict(self)
 |      Return a new OrderedDict which maps field names to their values.
 |
 |  _replace(_self, **kwds)
 |      Return a new MyTuple object replacing specified fields with new values
 |
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |
 |  _make(iterable) from builtins.type
 |      Make a new MyTuple object from a sequence or iterable
 |
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |
 |  __new__(_cls, x, y)
 |      Create new instance of MyTuple(x, y)
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  x
 |      The x value
 |
 |  y
 |      The y value
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  _fields = ('x', 'y')
 |  
 |  _fields_defaults = {}
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from builtins.tuple:
 |  
 |  __add__(self, value, /)
 |      Return self+value.
 |  
 |  __contains__(self, key, /)
 |      Return key in self.
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getitem__(self, key, /)
 |      Return self[key].
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __len__(self, /)
 |      Return len(self).
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __mul__(self, value, /)
 |      Return self*value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __rmul__(self, value, /)
 |      Return value*self.
 |  
 |  count(self, value, /)
 |      Return number of occurrences of value.
 |  
 |  index(self, value, start=0, stop=9223372036854775807, /)
 |      Return first index of value.
 |      
 |      Raises ValueError if the value is not present.

Alternatively, you can also specify the parameter's type via:

MyTuple = named_tuple(
    "MyTuple",
    "My named tuple for x,y coordinates",
    x=("int", "The x value"),
    y=("int", "The y value")
)
Labiche answered 7/1, 2019 at 19:21 Comment(0)
S
0

Here is a good example of a NamedTuple that is documented with a Docstring. (Python 3.6+) Below the example, you will find my reasoning on why this is a good example.

Note: Python version information included below

from typing import NamedTuple 

class Point(NamedTuple):
    """Defines a point on a 2D Cartesian plane

    Args:
        x: Provide the x-axis location of the Point
        y: Provide the y-axis location of the Point
    """

    x: float
    "The x-coordinate of the point on the Cartesian plane."

    y: float
    "The y-coordinate of the point on the Cartesian plane."

# The below lines are only required if you care about full compatibility
# with Python's help function 
Point.x.__doc__ = "The x-coordinate of the point on the Cartesian plane."
Point.y.__doc__ = "The y-coordinate of the point on the Cartesian plane."

Why is the above example good?

  • The Docstring in the above example is written in the Google style guide format, which allows Visual Studio's Intellisense to understand the docstring. (I've only tried with Visual Studio Code)
  • The other benefit of the Google style guide format is that it is compatible with the Sphinx Documentation Generator. (See Compatibility notes)
  • The NamedTuple's fields (x and y) have their type defined via typehints. Using Typehints is preferable over embedding the type information within a docstring because they provide better type-checker and IntelliSense support when using modern code editors such as PyCharm or Visual Studio Code.

You can find more on the Google style guide format here: https://google.github.io/styleguide/pyguide.html

Compatibility Notes

I manually tested/verified that the above code is functional in Python 3.11 and that the docstring is compatible with Sphinx 7.2.6. However, when using Sphinx, you must modify the conf.py file to ensure compatibility with Google Style Docstring. You can find more about Sphinx compatibility here: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html

Semen answered 1/10, 2023 at 4:12 Comment(0)
F
-3

No, you can only add doc strings to modules, classes and function (including methods)

Fin answered 22/10, 2009 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.