We've mentioned docstrings before, but now we will cover them in greater detail. Docstrings are triple-quoted strings that have special significance within Python. When used, they form the __doc__ attribute of an object. There are many examples of projects that don't use docstrings, but it is highly advised to incorporate docstrings into your projects. If you do use them, review PEP 257 -- Docstring Conventions (https://www.python.org/dev/peps/pep-0257/) to see how to do them right; a Python Enhancement Proposal (PEP) is used to discuss changes to the Python language. Not following the guidelines is fine, as long as you're consistent within your code. However, if you try to use tools such as Docutils, you can have problems, as they expect the docstrings to be properly formatted; Docutils is a text processing system that converts plain text into formatted documents, such as HTML, and XML.
Docstrings are the very first item in a module, function, class, or method; if they are put elsewhere, chances are good that tools won't recognize them as docstrings. They have to be enclosed by a trio of either single or double-quote marks; that, in conjunction with their location at the beginning of each object, tells Python that they are docstrings and not ordinary triple-quoted strings.
Docstrings can be placed on a single line or, since they are triple-quoted strings, they can also spread across multiple lines. Normally, single lines are used to summarize a basic object while multi-line docstrings can provide more information about an object, such as its expected arguments or output parameters or even what the object is expected to do.
There are a lot of details about docstrings that we won't cover here but are explained in more detail in PEP 257. Some examples include what information to include in a class docstring, especially when creating subclasses, as well as information to include about a function or method. The key takeaway is that docstrings help document and define parts of Python programs and should be included whenever possible.
Related to docstrings are doctests. These are handled by the doctest module and function like unit tests, except they are created within a docstring. Doctests are best used as a way to keep docstrings up-to-date and ensure the code actually does what the documentation says it should. Doctests utilize the interactive Python interpreter to perform the tests, rather than run as separate test files. The following is an example of a doctest included within the docstring of a function:
def factorial(n): """Return the factorial of n, an exact integer >= 0. >>> [factorial(n) for n in range(6)] [1, 1, 2, 6, 24, 120] >>> factorial(30) 265252859812191058636308480000000 >>> factorial(-1) Traceback (most recent call last): ... ValueError: n must be >= 0 Factorials of floats are OK, but the float must be an exact integer: >>> factorial(30.1) Traceback (most recent call last): ... ValueError: n must be exact integer >>> factorial(30.0) 265252859812191058636308480000000 It must also not be ridiculously large: >>> factorial(1e100) Traceback (most recent call last): ... OverflowError: n too large """
We won't cover all of the docstrings for all of the code we have written, but the complete files in this book's code repository does include them. However, we will show the docstrings for valve.py as a representative example of how docstrings could be written. Note that we won't reprint the entire file or the actual code logic, but just the key objects that benefit from expanded docstrings.
In part 1 of the following code, we create the docstring for the parent Valve class. The first line is a basic summary of the class; this should be written as a single-line summary statement, in case we decide not to include any additional information.
Line 4 provides additional information about the class, specifically what the Cv parameter is and what it represents for a valve. Line 6 lists all of the parameters identified in the class, while line 8 lists the methods that are part of the class. Notice that we don't list getter/setter methods for valve positions, as those have been converted in to class properties:
# valve.py docstrings (part 1)
1class Valve: 2 """Generic class for valves. 3 4 Cv is the valve flow coefficient: number of gallons per minute at 60F through a fully open valve with a press. drop of 1 psi. For valves 1 inch or less in diameter, Cv is typically < 5. 5 6 Variables: name, position, Cv, deltaP, flow_in, flow_out, press_out, press_in 7 8 Methods: calc_coeff(), press_drop(), valve_flow_out(), get_press_out(), open(), close() 9 """
When we create the initialization method in part 2 in the following code, it is useful to provide the parameters that are being initialized (lines 4-10). These parameter statements include the name of the parameters, as well as a short sentence about what the parameter represents:
# valve.py docstrings (part 2)
1 def __init__(self, name="", sys_flow_in=0.0, sys_flow_out=0.0, drop=0.0, position=0, flow_coeff=0.0, press_in=0.0): 2 """Initialize valve. 3 4 :param sys_flow_out: Fluid flow out of the valve 5 :param drop: Pressure drop across the valve 6 :param press_in: Pressure at valve inlet 7 :param name: Instance name 8 :param sys_flow_in: Flow rate into the valve 9 :param position: Percentage valve is open 10 :param flow_coeff: Affect valve has on flow rate; assumes a 2 inch, wide open valve 11 """
In part 3 of the following code, we provide not only an argument parameter for the calc_coeff() method, but also indicate what the return object represents. You can provide the name/type of the return value, but it isn't necessary, as the code itself should be indicative enough:
# valve.py docstrings (part 3)
1 def calc_coeff(self, diameter): 2 """Roughly calculate Cv based on valve diameter. 3 4 :param diameter: Valve diameter 5 6 :return: Update valve flow coefficient 7 """ 8 9 def press_drop(self, flow_out, spec_grav=1.0): 10 """Calculate the pressure drop across a valve, given a flow rate. 11 12 Pressure drop = ((system flow rate / valve coefficient) ** 2) * spec. gravity of fluid 13 14 Cv of valve and flow rate of system must be known.
In part 4, we continue providing assumptions in line 1, then we list the input parameters in lines 3 and 4. With line 6, we provide the exception that is generated within this method, and finish up with lines 8 and 9 that provide the return object information. Note that, in line 9, the return type is provided; again, it's not required, but it might be useful if an actual object is returned, rather than just updated.
We start a new method in line 12, with lines 13 and 15 providing additional information about the method, as is normal for a docstring:
# valve.py docstrings (part 4)
1 Specific gravity of water is 1. 2 3 :param flow_out: System flow rate into the valve 4 :param spec_grav: Fluid specific gravity; default assumes fluid is water 5 6 :except ZeroDivisionError: Valve coefficient not provided 7 8 :return: Update pressure drop across valve 9 :rtype: float 10 """ 11 12 def valve_flow_out(self, flow_coeff, press_drop, spec_grav=1.0): 13 """Calculate the system flow rate through a valve, given a pressure drop. 14 15 Flow rate = valve coefficient / sqrt(spec. grav. / press. drop)
Part 5 continues the valve_flow_out() method by adding the incoming parameter arguments, the method exception, return object, and return type. Line 11 starts a new method:
# valve.py docstring (part 5)
1 :param flow_coeff: Valve flow coefficient 2 :param press_drop: Pressure drop (psi) 3 :param spec_grav: Fluid specific gravity 4 5 :except ValueError: Valve coefficient or deltaP <= 0 6 7 :return: Update system flow rate 8 :rtype: float 9 """ 10 11 def get_press_out(self, press_in): 12 """Get the valve outlet pressure, calculated from inlet pressure. 13 14 :param press_in: Pressure at valve inlet
In part 6, lines 1-3 complete the get_press_out() method docstring. With line 5, we gave the Gate valve subclass. Docstrings for subclasses are slightly different, as they identify what the parent class is (line 8) and the methods that are part of the subclass (lines 10-12).
We finish this code listing by starting the read_position() method and its initial docstring summary:
# valve.py docstring (part 6)
1 :return: Pressure at valve outlet 2 :rtype: float 3 """ 4 5class Gate(Valve): 6 """Open/closed valve. 7 8 Subclasses Valve. 9 10 Methods: 11 read_position() 12 turn_handle() 13 """ 14 def read_position(self): 15 """Identify the position of the valve.
In part 7, we finish the rest of the read_position() docstring and then create the docstring data for turn_handle():
# valve.py docstring (part 7)
1 :return: Indication of whether the valve is open or closed. 2 :rtype: str 3 """ 4 5 def turn_handle(self, new_position): 6 """Change the status of the valve. 7 8 :param new_position: New valve position 9 10 :return: Update valve position 11 """
As these classes and methods are representative of how docstrings can be made, the rest of the file is not provided. However, the complete valve.py file and the rest of the code for this project are provided in this book's file repository for review by the reader.