How to write a Docstring
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 + twoFor a function, the docstring is generally made up of 3 sections.
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.
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 + twoIf 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 + twoArguments
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>
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 + twoReturned value
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.
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 + twoRarest 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
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 + twoIf 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 + twoExamples
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 + twoDocumentation:
One-liner docstring
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))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.imagDocumentation:
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.imagAttribute
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"""
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.imagMethod
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.imagIf 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.
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.imagNote 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.