This tutorial explains what is Python Docstring and how to use it to document Python functions with examples:
Functions are so important in Python to an extent that Python has tens of built-in functions. Python also gives us the possibility to create functions of our own.
However, functions don’t end just at creating them, we have to document them so that they are clear, readable, and maintainable. Also, functions have attributes that can be used for introspecting, and this enables us to handle functions in diverse ways.
=> Read Through The Easy Python Training Series.
Table of Contents:
Python Docstring
In this section, we will have a quick look at what functions are and this has been fully covered in Python Functions.
Functions are like mini-programs within a program and group a bunch of statements so that they can be used and reused throughout different parts of the program.
Python Function-related Statements With Code Example
Statements | Sample Code Example |
---|---|
def, parameters, return | def add(a, b=1, *args, **kwargs): return a + b + sum(args) + sum(kwargs.values()) |
calls | add(3,4,5, 9, c=1, d=8) # Output: 30 |
Documenting A Function
Most of us find it hard to document our functions as it could be time-consuming and boring.
However, while not documenting our code, in general, may seem okay for small programs, when the code gets more complex and large, it will be hard to understand and maintain.
This section encourages us to always document our functions no matter how small our programs may seem.
Importance Of Documenting A Function
There is a saying that “Programs must be written for people to read, and only incidentally for machines to execute”.
We can’t stress enough that documenting our functions helps other developers(including ourselves) to easily understand and contribute to our code.
I bet we have once come across a code we wrote years ago and we were like “What was I thinking..” This is because there was no documentation to remind us of what the code did, and how it did it.
That being said, documenting our functions or code, in general, bring the following advantages.
- Adds more meaning to our code, thereby making it clear and understandable.
- Ease maintainability. With proper documentation, we can return to our code years later and still be able to maintain the code swiftly.
- Ease contribution. In an open-source project, for example, many developers work on the codebase simultaneously. Poor or no documentation will discourage developers from contributing to our projects.
- It enables popular IDE’s debugging tools to effectively assist us in our development.
Documenting Functions With Python Docstrings
According to the PEP 257 — Docstring Conventions
“A docstring is a string literal that occurs as the first statement in a module, function, class, or method definition. Such a docstring becomes the __doc__ special attribute of the object.”
Docstrings are defined with triple-double quote(“””) string format. At a minimum, a Python docstring should give a quick summary of whatever the function is doing.
A function’s docstring can be accessed in two ways. Either directly via the function’s __doc__ special attribute or using the built-in help() function which accesses __doc__ behind the hood.
Example 1: Access a function’s docstring via the function’s __doc__ special attribute.
def add(a, b): """Return the sum of two numbers(a, b)""" return a + b if __name__ == '__main__': # print the function's docstring using the object’s special __doc__ attribute print(add.__doc__)
Output
NB: The docstring above represents a one-line docstring. It appears in one line and summarizes what the function does.
Example 2: Access a function’s docstring using the built-in help() function.
Run the following command from a Python shell terminal.
>>> help(sum) # access docstring of sum()
Output
NB: Press q to exit this display.
A multi-line Python docstring is more thorough, and may contain all the following:
- Function’s purpose
- Information about arguments
- Information about return data
Any other information that may seem helpful to us.
The example below shows a thorough way of documenting our functions. It starts by giving a short summary of what the function does, and a blank line followed by a more detailed explanation of the function’s purpose, then another blank line followed by information about arguments, return value, and any exceptions if any.
We also notice a break-space after the enclosing triple-quote before our function’s body.
Example 3:
def add_ages(age1, age2=30): """ Return the sum of ages Sum and return the ages of your son and daughter Parameters ------------ age1: int The age of your son age2: int, Optional The age of your daughter(default to 30) Return ----------- age : int The sum of your son and daughter ages. """ age = age1 + age2 return age if __name__ == '__main__': # print the function's docstring using the object's special __doc__ attribute print(add_ages.__doc__)
Output
NB: This is not the only way to document using docstring. Read on for other formats too.
Python Docstring Formats
The docstring format used above is the NumPy/SciPy-style format. Other formats also exist, we can also create our format to be used by our company or open-source. However, it is good to use well-known formats recognized by all developers.
Some other well-known formats are Google docstrings, reStructuredText, Epytext.
Example 4: By referencing code from example 3, use the docstring formats Google docstrings, reStructuredText, and Epytext to rewrite the docstrings.
#1) Google docstrings
"""Return the sum of ages Sum and return the ages of your son and daughter Args: age1 (int): The age of your son age2 (int): Optional; The age of your daughter ( default is 30) Returns: age (int): The sum of your son and daughter ages. """
#2) reStructuredText
"""Return the sum of ages Sum and return the ages of your son and daughter :param age1: The age of your son :type age1: int :param age2: Optional; The age of your daughter ( default is 30) :type age2: int :returns age: The sum of your son and daughter ages. :rtype: int """
#3) Epytext
"""Return the sum of ages Sum and return the ages of your son and daughter @type age1: int @param age1: The age of your son @type age2: int @param age2: Optional; The age of your daughter ( default is 30) @rtype: int @returns age: The sum of your son and daughter ages. """
How Other Tools Make Use Of DocStrings
Most tools like code editors, IDEs, etc make use of docstrings to provide us some functionalities that can help us in development, debugging, and testing.
Code Editor
Code editors like Visual Studio Code with its Python extension installed can be better and effectively assist us during development if we properly document our functions and classes with docstring.
Example 5:
Open Visual Studio Code with the Python extension installed, then save the code of example 2 as ex2_dd_ages.py. In the same directory, create a second file called ex3_import_ex2.py and paste in it the code below.
from ex2_add_ages import add_ages # import result = add_ages(4,5) # execute print(result)
Let’s not run this code but let’s hover (put our mouse over) add_ages in our editor.
We shall see the function’s docstring as shown in the image below.
We see that this helps us to have a preview of what the function does, what it is expecting as input, and also what to expect as a return value from the function without needing to check the function wherever it has been defined.
Test Modules
Python has a test module called doctest. It searches for pieces of docstring text beginning with the prefix >>>(input from the Python shell) and executes them to verify that they work and produce the exact expected result.
This provides a quick and easy way to write tests for our functions.
Example 6:
def add_ages(age1, age2= 30): """ Return the sum of ages Sum and return the ages of your son and daughter Test ----------- >>> add_ages(10, 10) 20 """ age = age1 + age2 return age if __name__ == '__main__': import doctest doctest.testmod() # run test
In the docstring above, our test is preceded by >>> and below it is the expected result, in this case, 20.
Let’s save the code above as ex4_test.py and run it from the terminal with the command.
Python ex4_test.py -v
Output
Functions Annotation
Apart from docstrings, Python enables us to attach metadata to our function’s parameters and return value, which arguably plays an important role in function documentation and type checks. This is referred to as function Annotations introduced in PEP 3107.
Syntax
def <function name>(<arg1>: expression, <arg2>: expression = <default value>)-> expression
As an example, consider a function that rounds up a float into an integer.
From the above figure, our annotations imply that the expected argument type should be afloat and the expected return type should be an integer.
Adding Annotations
There are two ways of adding annotations to a function. The first way is as seen in the above where the object annotations are attached to the parameter and return value.
The second way is to add them manually via the __annotations__ attribute.
Example 7:
def round_up(a): return round(a) if __name__ == '__main__': # check annotations before print("Before: ", round_up.__annotations__) # Assign annotations round_up.__annotations__ = {'a': float, 'return': int} # Check annotation after print("After: ", round_up.__annotations__)
Output
NB: Looking at the dictionary, we see that the parameter name is used as a key for the parameter and the string ‘return’ is used as a key for the return value.
Recall from the syntax above that annotations can be any valid expression.
So, it could be:
- A string describing the expected argument or return value.
- Other data types like List, Dictionary, etc.
Example 8: Define various annotations
def personal_info( n: { 'desc': "first name", 'type': str }, a: { 'desc': "Age", 'type': int }, grades: [float])-> str: return "First name: {}, Age: {}, Grades: {}".format(n,a,grades) if __name__ == '__main__': # Execute function print("Return Value: ", personal_info('Enow', 30, [18.4,15.9,13.0])) print("\n") # Access annotations of each parameter and return value print('n: ',personal_info.__annotations__['n']) print('a: ',personal_info.__annotations__['a']) print('grades: ',personal_info.__annotations__['grades']) print("return: ", personal_info.__annotations__['return'])
Output
Accessing Annotations
The Python interpreter creates a dictionary of the function’s annotation and dumps them in the function’s __annotations__ special attribute. So, accessing annotations is the same as accessing dictionary items.
Example 9: Access the annotations of a function.
def add(a: int, b: float = 0.0) -> str: return str(a+b) if __name__ == '__main__': # Access all annotations print("All: ",add.__annotations__) # Access parameter 'a' annotation print('Param: a = ', add.__annotations__['a']) # Access parameter 'b' annotation print('Param: b = ', add.__annotations__['b']) # Access the return value annotation print("Return: ", add.__annotations__['return'])
Output
NB: If a parameter takes a default value, then it has to come after the annotation.
Use Of Annotations
Annotations on their own don’t do much. The Python interpreter doesn’t use it to impose any restrictions whatsoever. They are just another way of documenting a function.
Example 10: Pass argument of a type different from the annotation.
def add(a: int, b: float) -> str: return str(a+b) if __name__ == '__main__': # pass strings for both arguments print(add('Hello','World')) # pass float for first argument and int for second argument. print(add(9.3, 10))
Output
We see that the Python interpreter doesn’t raise an exception or warning.
Despite this, annotations can be used to restrain data type arguments. It can be done in many ways but in this tutorial, we shall define a decorator that uses annotations to check for argument data types.
Example 11: Use annotations in decorators to check for an argument data type.
First, let’s define our decorator
def checkTypes(function): def wrapper(n, a, grades): # access all annotations ann = function.__annotations__ # check the first argument's data type assert type(n) == ann['n']['type'], \ "First argument should be of type:{} ".format(ann['n']['type']) # check the second argument's data type assert type(a) == ann['a']['type'], \ "Second argument should be of type:{} ".format(ann['a']['type']) # check the third argument's data type assert type(grades) == type(ann['grades']), \ "Third argument should be of type:{} ".format(type(ann['grades'])) # check data types of all items in the third argument list. assert all(map(lambda grade: type(grade) == ann['grades'][0], grades)), "Third argument should contain a list of floats" return function(n, a, grades) return wrapper
NB: The function above is a decorator.
Lastly, let’s define our function and use the decorator to check for any argument data type.
@checkTypes def personal_info( n: { 'desc': "first name", 'type': str }, a: { 'desc': "Age", 'type': int }, grades: [float])-> str: return "First name: {}, Age: {}, Grades: {}".format(n,a,grades) if __name__ == '__main__': # Execute function with correct argument’s data types result1 = personal_info('Enow', 30, [18.4,15.9,13.0]) print("RESULT 1: ", result1) # Execute function with wrong argument’s data types result2 = personal_info('Enow', 30, [18.4,15.9,13]) print("RESULT 2: ", result2)
Output
From the result above, we see that the first function call executed successfully, but the second function call raised an AssertionError indicating that the items in the third argument aren’t respecting the annotated data type. It is required that all the items in the third argument list be of type float.
Function Introspections
Function objects have many attributes that can be used for introspection. In order to view all these attributes, we can use the dir() function as shown below.
Example 13: Print out the attributes of a function.
def round_up(a): return round(a) if __name__ == '__main__': # print attributes using 'dir' print(dir(round_up))
Output
NB: The above shown are the attributes of user-defined functions that may be slightly different from built-in functions and class objects.
In this section, we will look at some attributes that can help us in function introspection.
Attributes of User-defined Functions
Attribute | Description | State |
---|---|---|
__dict__ | A dictionary that supports arbitrary function attributes. | Writable |
__closure__ | A None or tuple of cells containing bindings for the function's free variables. | Read-Only |
__code__ | Bytecode representing the compiled function metadata and function body. | Writable |
__defaults__ | A tuple containing default values for default arguments, or None if no default arguments. | Writable |
__kwdefaults__ | A dict containing default values for keyword-only parameters. | Writable |
__name__ | A str which is the function name. | Writable |
__qualname__ | A str which is the function's qualified name. | Writable |
We didn’t include __annotations__ in the table above because we already addressed it earlier in this tutorial. Let’s look closely at some of the attributes presented in the above table.
#1) dict
Python uses a function’s __dict__ attribute to store arbitrary attributes assigned to the function.
It is usually referred to as a primitive form of annotation. Though it is not a very common practice, it can become handy for documentation.
Example 14: Assign an arbitrary attribute to a function that describes what the function does.
def round_up(a): return round(a) if __name__ == '__main__': # set the arbitrary attribute round_up.short_desc = "Round up a float" # Check the __dict__ attribute. print(round_up.__dict__)
Output
#2) Python Closure
Closure enables a nested function to have access to a free variable of its enclosing function.
For closure to happen, three conditions need to be met:
- It should be a nested function.
- The nested function has access to its enclosing function variables(free variables).
- The enclosing function returns the nested function.
Example 15: Demonstrate the use of closure in nested functions.
The enclosing function (divide_by) gets a divisor and returns a nested function(dividend) that takes in a dividend and divides it by the divisor.
Open an editor, paste the code below and save it as closure.py
def divide_by(n): def dividend(x): # nested function can access 'n' from the enclosing function thanks to closure. return x//n return dividend if __name__ == '__main__': # execute enclosing function which returns the nested function divisor2 = divide_by(2) # nested function can still access the enclosing function's variable after the enclosing function # is done executing. print(divisor2(10)) print(divisor2(20)) print(divisor2(30)) # Delete enclosing function del divide_by # nested function can still access the enclosing function's variable after the enclosing function stops existing. print(divisor2(40))
Output
So, what’s the use of __closure__. This attribute returns a tuple of cell objects that defines the attribute cell_contents that holds all variables of the enclosing function.
Example 16: In the directory where closure.py was saved, open a terminal and start a Python shell with the command python and execute the code below.
>>> from closure import divide_by # import >>> divisor2 = divide_by(2) # execute the enclosing function >>> divide_by.__closure__ # check closure of enclosing function >>> divisor2.__closure__ # check closure of nested function (<cell at 0x7fb8a22d8110: int object at 0x1067d54a0>,) >>> divisor2.__closure__[0].cell_contents # access closed value 2
NB: __closure__ returns None if it is not a nested function.
#3) code, default, kwdefault, Name, qualname
__name__ returns the name of the function and __qualname__ returns the qualified name. A qualified name is a dotted name describing the function path from its module’s global scope. For top-level functions, __qualname__ is the same as __name__
Example 17: In the directory where closure.py in example 15 was saved, open a terminal and start a Python shell with the command python and execute the code below.
>>> from introspect import divide_by # import function >>> divide_by.__name__ # check 'name' of enclosing function 'divide_by' >>> divide_by.__qualname__ # check 'qualified name' of enclosing function 'divide_by' >>> divisor2 = divide_by(2) # execute enclosing function >>> divisor2.__name__ # check 'name' of nested function 'dividend' >>> divisor2.__qualname__ # check 'qualified name' of nested function 'divide_by.<locals>.dividend'
__defaults__ contains the values of a function’s default parameters while __kwdefaults__ contains a dictionary of a function’s keyword-only parameters and value.
__code__ defines the attributes co_varnames that holds the name of all the parameters of a function and co_argcount which holds the number of a function’s parameter except those prefixed with * and **.
Example 18:
def test(c, b=4, *,a=5): pass # do nothing if __name__ =='__main__': print("Defaults: ",test.__defaults__) print("Kwdefaults: ", test.__kwdefaults__) print("All Params: ", test.__code__.co_varnames) print("Params Count: ", test.__code__.co_argcount)
Output
NB:
- All default parameters after the empty * become keyword-only parameters(new in Python 3).
- co_argcount counts 2 because it doesn’t consider any argument variable prefixed with * or **.
Frequently Asked Questions
Q #1) Does Python enforce type hints?
Answer: In Python, type hints don’t do much by themselves. They are mostly used to inform the reader of the type of code a variable is expected to be. The good news is that its information can be used to implement type checks. This is commonly done in Python decorators.
Q #2) What is a Docstring in Python?
Answer: A docstring is the first string literal enclosed in triple-double quotes (“””), and immediately follows a class, module, or function’s definition. A docstring generally describes what the object is doing, its parameters, and its return value.
Q#3) How do you get a Python Docstring?
Answer: Generally, there are two ways of getting an object’s docstring. By using the object’s special attribute __doc__ or by using the built-in help() function.
Q #4) How do you write a good Docstring?
Answer: The PEP 257 contains the official Docstring conventions. Also, other well-known formats exist like Numpy/SciPy-style, Google docstrings, reStructured Text, Epytext.
Conclusion
In this tutorial, we looked at function documentation where we saw the importance of documenting our functions and also learned how we can document with docstring.
We also looked at functions introspection where we examined a few functions attributes that can be used for introspection.
=> Check Out The Perfect Python Training Guide Here.