How to write a Docstring

Important

This documentation talks about docstring in python based on google style.

Docstring of Function

A docstring should give enough information to write a call to the function without reading the function’s code.

google docstring style

def col_name_str(name_op: DotMap, operator: _OPERATORTYPE) -> str:
    """
    Get the corresponding column name for a str functions from a MagicDotPath.

    Args:
        name_op: name of the operand(s).
        operator: Operator type.

    Returns:
        A column name with correct format.

    Raises:
        TypeOperatorErrir: If type of operator is not `StrFunctType`.

    Examples:
        >>> col_name_str(DotMap(op1="star_name"), StrFunctType.LOWER)
        >>> 'lower_star_name'
    """
    if not isinstance(operator, StrFunctType):
        raise TypeOperatorError([StrFunctType], type(operator))

    return f"{operator.name}_{name_op.op1}"

For the rest of this guide, we’ll use a simpler example.

def sum_between_two_number(one: int, two: int) -> int:
    return one + two

For a function, the docstring is generally made up of 3 sections.

  1. Function summary
  2. Arguments
  3. Returned value

We can also add sections for exceptions and exemples

Summary

The summary of the function should describe the function but generally not its implementation details (unless those details are relevant to how the function is to be used). The first sentence of the doctring must fit on one line. It must also begin with a capital letter and end with a full stop.

Caution

NEVER put the type of the argument or returned value(s) in the summary, they are in the function signature. You don’t want to change the docstring when you modify the arguments or returned value types.

def sum_between_two_number(one: int, two: int) -> int:
    """
    Get the sum between 2 number.
    """
    return one + two

If you want provide more details skip a line and write it, which can be on several lines, with a capital letter at the begin and a full stop at the end.

def sum_between_two_number(one: int, two: int) -> int:
    """
    Get the sum between 2 number.

    Here we can provide more details and this
    can be on several line.
    """
    return one + two

Arguments

Note

Arguments docstring must be indented after a line: Args:.

For the argument we’ll just list them with a short description. This part is useful for future developers (the you of the future or other people) or users of your function is it is intended to be used by people outside the project (for example if you make a library).

The syntax for an argument docstring is: the name of the argument (in small letter), a colon and the short description. Begin with a capital letter and a full stop at the end of the sentence. The description of the argument can be on several lines.

<name_of_the_argument>: <description_of_the_argument>

Caution

NEVER put the type of the argument in the description, they are in the function signature. You don’t want to change the docstring when you modify the arguments types.

def sum_between_two_number(one: int, two: int) -> int:
    """
    Get the sum between 2 number.

    Args:
        one: First operand for the sum.
        two: Second operand for the sum.
    """
    return one + two
If the function don’t take Arguments

Just don’t write this part.

Returned value

Note

Returned value docstring must be indented after a line: Returns:.

For the returned value we’ll put a description of the result of the function. A short description of what should be retrieved when we call the function. The sentence can be on several line and must begin with a capital letter and end with a full stop.

Caution

NEVER put the type of the returned value in the description, they are in the function signature. You don’t want to change the docstring when you modify the returned value type.

def sum_between_two_number(one: int, two: int) -> int:
    """
    Get the sum between 2 number.

    Args:
        one: First operand for the sum.
        two: Second operand for the sum.

    Returns:
        The sum between the 2 given number.
    """
    return one + two
If the function return nothings (None)

Just don’t write this part.

Rarest parts

With summary, argumetns and returned value, we can write 90% of the docstring in our projects. But sometimes our functions can raises exceptions and ans we’ll want to give examples.

Raise exceptions

Note

Raise docstring must be indented after a line: Raises:.

Rather like arguments we give the name of all exception can be raise and a short description of when this exception can be raise. Using the same syntax: the name of the exception, a colon and the short description. Begin with a capital letter and a full stop at the end of the sentence. The description of when this exception can be raise can be on several lines.

<name_of_the_exception>: <description_of_the_exxception>

def sum_between_two_number(one: int, two: int) -> int:
    """
    Get the sum between 2 number.

    Args:
        one: First operand for the sum.
        two: Second operand for the sum.

    Returns:
        The sum between the 2 given number.

    Raises:
        TypeError: If one of operand are not a number.
    """
    if not one.isnumeric() or not two.isnumeric():
        raise TypeError("One of arguments is not a number")

    return one + two

If the raise is in a called function (or operator) and not directly in your function we specified that it is an indrect raise.

def sum_between_two_number(one: int, two: int) -> int:
    """
    Get the sum between 2 number.

    Args:
        one: First operand for the sum.
        two: Second operand for the sum.

    Returns:
        The sum between the 2 given number.

    Raises:
        TypeError: Indirect raise by `+` if we try to add 2 operand
            which are not a number.
    """
    return one + two
If the function raise nothings

Just don’t write this part.

Examples

Note

Examples docstring must be indented after a line: Examples:.

To give examples on your function you can write doctests in the part Examples of a docstring. Each example must be sperated with a blank line.

The syntax a doctests is the same as in a python terminal. You can see more examples in the doctest module documentation

def sum_between_two_number(one: int, two: int) -> int:
    """
    Get the sum between 2 number.

    Args:
        one: First operand for the sum.
        two: Second operand for the sum.

    Returns:
        The sum between the 2 given number.

    Raises:
        TypeError: Indirect raise by `+` if we try to add 2 operand
            which are not a number.

    Examples:
        >>> sum_between_two_number(1, 2)
        3

        >>> sum_between_two_number(4, 5)
        9

        >>> sum_between_two_number(1, "a")
        Traceback (most recent call last):
        ...
        TypeError: unsupported operand type(s) for +: 'int' and 'str'
    """
    return one + two
Note

All these examples can be considered as tests and checked by pytest with the plugin: xdoctest

Documentation:

One-liner docstring

Use if and only if the function don’t take arguments, return nothings and raise nothings

If the function respect all these prerequisites you can write a one liner docstring.

To write a one-liner docstring, you need to write the summary on one line with a capital letter and a full stop.

def hello_world() -> None:
    """Print "hello world"."""

    print("hello world")

Docstring of a Class

Documenting a python class is a little different because it has no attributes and no return value. We’re going to document the class and its attributes separately.

@dataclass
class Command:
    """Class of command with command type and arguments of the command."""

    cmd_type: CommandTypeOrStr
    """Type of the command."""

    args: DotMap = field(default_factory=DotMap)
    """Arguments of the command."""

    def __eq__(self, other: Any) -> bool:
        """
        Try equality between two Command.

        Args:
            other: Other element to with which we test equality.

        Returns:
            True if self is equal to other given element,
            False otherwise.

        Raises:
            NotImplemented: If the given element is not of
            Command type.
        """
        if not isinstance(other, Command):
            return NotImplemented

        if not self.cmd_type == other.cmd_type:
            return False

        for key in self.args:
            if not self.args[key] == other.args[key]:
                return False

        return True

    def __ne__(self, other: Any) -> bool:
        """
        Try no-equality between two Command.

        Args:
            other: Other element to with which we test none equality.

        Returns:
            True if self is not equal to other given element,
            False otherwise.

        Raises:
            NotImplemented: If the given element is not of
            Command type.
        """
        return bool(not self.__eq__(other))
Note

This guide work classic classes and for dataclasses. In modern python all classes with attributes must use dataclasses.

For the guide we will use a simple class to representing a point in a 2D graphic. The class has a method to get a formatted message with coordinates.

@dataclass
class Point():

    pts_id: int
    x: int
    y: int

    def get_formatted_msg(self) -> str:
        return (
            f"Point n°{self.pts_id}:\n\t"
            f"x: {self.x}\n\t"
            f"y: {self.y}"
        )

    @property
    def r(self) -> float:
        return math.hypot(self.x, self.y)

    @property
    def phi(self) -> float:
        return cmath.phase(complex(x, y))

    @property
    def cplx(self) -> complex:
        return complex(x, y)

    @cplx.setter
    def cplx(self, c: complex) -> None:
        self.x = c.real
        self.y = c.imag

Documentation:

Class

To document the class we just write a little summary, as for a function this summary must be in one line and begin with a capital letter and end with a full stop.

@dataclass
class Point():
    """Point class to represent a dot in a 2D graphic."""

    pts_id: int
    x: int
    y: int

    def get_formatted_msg(self) -> str:
        return (
            f"Point n°{self.pts_id}:\n\t"
            f"x: {self.x}\n\t"
            f"y: {self.y}"
        )

    @property
    def r(self) -> float:
        return math.hypot(self.x, self.y)

    @property
    def phi(self) -> float:
        return cmath.phase(complex(x, y))

    @property
    def cplx(self) -> complex:
        return complex(x, y)

    @cplx.setter
    def cplx(self, c: complex) -> None:
        self.x = c.real
        self.y = c.imag

Attribute

Note

We need to document all attributes like arguments of function.

For the attribute we’ll just write a short description. This part is useful for future developers (the you of the future or other people) or users of your class is it is intended to be used by people outside the project (for example if you make a library).

The syntax for an attribute docstring is very simple. Just a short description in one line. Must begin with a capital letter annd end with a full stop.

"""description_of_attribute"""

Caution

NEVER put the type of the argument in the description, they are in the class signature. You don’t want to change the docstring when you modify attributes types.

@dataclass
class Point():
    """Point class to represent a dot in a 2D graphic."""

    pts_id: int
    """Id of the point."""

    x: int
    """X position of the point."""

    y: int
    """Y position of the point."""

    def get_formatted_msg(self) -> str:
        return (
            f"Point n°{self.pts_id}:\n\t"
            f"x: {self.x}\n\t"
            f"y: {self.y}"
        )

    @property
    def r(self) -> float:
        return math.hypot(self.x, self.y)

    @property
    def phi(self) -> float:
        return cmath.phase(complex(x, y))

    @property
    def cplx(self) -> complex:
        return complex(x, y)

    @cplx.setter
    def cplx(self, c: complex) -> None:
        self.x = c.real
        self.y = c.imag

Method

For a method it’s exactly the same as for a function, just we do not documente self.

@dataclass
class Point():
    """Point class to represent a dot in a 2D graphic."""

    pts_id: int
    """Id of the point."""

    x: int
    """X position of the point."""

    y: int
    """Y position of the point."""

    def get_formatted_msg(self) -> str:
        """
        Get a formatted message to represent a point.

        Returns:
            The formatted message of an instance of this Point class.
        """
        return (
            f"Point n°{self.pts_id}:\n\t"
            f"x: {self.x}\n\t"
            f"y: {self.y}"
        )

    @property
    def r(self) -> float:
        return math.hypot(self.x, self.y)

    @property
    def phi(self) -> float:
        return cmath.phase(complex(x, y))

    @property
    def cplx(self) -> complex:
        return complex(x, y)

    @cplx.setter
    def cplx(self, c: complex) -> None:
        self.x = c.real
        self.y = c.imag
Note

If the method take agrument, you need to think about documentating them.

Same for raises or examples.

Properties (aka accessors)

Properties are a way to create fake attribute that in fact execute code when accessed. The more common case are getter accessors: they provide a clean way to create read-only attributes (they can’t be modified). But documentation is the same even if they provide setting or deleting capabilities: they have to be documented like if they were attribute.

Caution

NEVER use a Return or Args section in a property docstring.

To create a property, use the @property decorator and document them as if they were attributes, not a method. Just mention any specific computation that could be of any importance to the developer/user:

import math
import cmath

@dataclass
class Point():
    """Point class to represent a dot in a 2D graphic."""

    x: int
    """X position of the point."""

    y: int
    """Y position of the point."""

    # Documentation for a simple readonly property
    @property
    def r(self) -> float:
        """Euclidian distance to the origin (0,0)."""
        return math.hypot(self.x, self.y)

    # Documentation for a more complex readonly property
    @property
    def phi(self) -> float:
        """
        Angle xOP where Ox is the horizontal axis and P the point.

        Positive when counter-clockwise.
        This is modulo 2*Pi
        """
        return cmath.phase(complex(x, y))

    # Documentation for a read-write property
    @property
    def cplx(self) -> complex:
        """Complex representing the position of the point in the Euler plane."""
        return complex(x, y)

    @cplx.setter
    def cplx(self, c: complex) -> None:
        """cplx setter."""
        self.x = c.real
        self.y = c.imag
Note

Note that most of the time setters don’t need a lot of description just something like “it’s a setter for this property”. Only provide more information if the computation done by the property has some side effect or is especialy heavy.