Python Docstring: Documenting And Introspecting Functions

By Sruthy

By Sruthy

Sruthy, with her 10+ years of experience, is a dynamic professional who seamlessly blends her creative soul with technical prowess. With a Technical Degree in Graphics Design and Communications and a Bachelor’s Degree in Electronics and Communication, she brings a unique combination of artistic flair…

Learn about our editorial policies.
Updated March 7, 2024

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.

Python docstring -Documenting and introspecting Functions

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

StatementsSample Code Example
def, parameters, returndef add(a, b=1, *args, **kwargs): return a + b + sum(args) + sum(kwargs.values())
callsadd(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

Function Example

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

access docstring using the built-in help() function

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

docstring example

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.

Visual Studio Code showing function’s docstrings
Visual Studio Code showing function’s docstrings

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

Test Module example

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.

Function Annotation
Function Annotation

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

add_annotation

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

Define Annotation

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

Access Annotation - 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

use of annotations

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

decorator

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

Function Introspections

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

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

dict

#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

closure

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

attributes

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.

Was this helpful?

Thanks for your feedback!

Leave a Comment