Is it posible to use docstring for plain variable? For example I have module called t
def f():
"""f"""
l = lambda x: x
"""l"""
and I do
>>> import t
>>> t.f.__doc__
'f'
but
>>> t.l.__doc__
>>>
Example is similar to PEP 258's (search for "this is g").
Yes, you can do that! You can actually 'document' lambdas and variables in a module by attaching docstrings to them.
A Python docstring is a string used to document a Python module, class, function or method, so programmers can understand what it does without having to read the details of the implementation. Also, it is a common practice to generate online (html) documentation automatically from docstrings.
As mentioned above, Python docstrings are strings used right after the definition of a function, method, class, or module (like in Example 1). They are used to document our code. We can access these docstrings using the __doc__ attribute.
Since docstrings are Python strings, escape sequences such as \n will be parsed as if the corresponding character—for example a newline—occurred at the position of the escape sequence in the source code.
typing.Annotated
to provide a docstring for variables.I originally wrote an answer (see below) where I said this wasn't possible. That was true back in 2012 but Python has moved on. Today you can provide the equivalent of a docstring for a global variable or an attribute of a class or instance. You will need to be running at least Python 3.9 for this to work:
from __future__ import annotations
from typing import Annotated
Feet = Annotated[float, "feet"]
Seconds = Annotated[float, "seconds"]
MilesPerHour = Annotated[float, "miles per hour"]
day: Seconds = 86400
legal_limit: Annotated[MilesPerHour, "UK national limit for single carriageway"] = 60
current_speed: MilesPerHour
def speed(distance: Feet, time: Seconds) -> MilesPerHour:
"""Calculate speed as distance over time"""
fps2mph = 3600 / 5280 # Feet per second to miles per hour
return distance / time * fps2mph
You can access the annotations at run time using typing.get_type_hints()
:
Python 3.9.1 (default, Jan 19 2021, 09:36:39)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import calc
>>> from typing import get_type_hints
>>> hints = get_type_hints(calc, include_extras=True)
>>> hints
{'day': typing.Annotated[float, 'seconds'], 'legal_limit': typing.Annotated[float, 'miles per hour', 'UK national limit for single carriageway'], 'current_speed': typing.Annotated[float, 'miles per hour']}
Extract information about variables using the hints for the module or class where they were declared. Notice how the annotations combine when you nest them:
>>> hints['legal_limit'].__metadata__
('miles per hour', 'UK national limit for single carriageway')
>>> hints['day']
typing.Annotated[float, 'seconds']
It even works for variables that have type annotations but have not been assigned a value. If I tried to reference calc.current_speed I would get an attribute error but I can still access its metadata:
>>> hints['current_speed'].__metadata__
('miles per hour',)
The type hints for a module only include the global variables, to drill down you need to call get_type_hints()
again on functions or classes:
>>> get_type_hints(calc.speed, include_extras=True)
{'distance': typing.Annotated[float, 'feet'], 'time': typing.Annotated[float, 'seconds'], 'return': typing.Annotated[float, 'miles per hour']}
I only know of one tool so far that can use typing.Annotated
to store documentation about a variable and that is Pydantic. It is slightly more complicated than just storing a docstring though it actually expects an instance of pydantic.Field
. Here's an example:
from typing import Annotated
import typing_extensions
from pydantic import Field
from pydantic.main import BaseModel
from datetime import date
# TypeAlias is in typing_extensions for Python 3.9:
FirstName: typing_extensions.TypeAlias = Annotated[str, Field(
description="The subject's first name", example="Linus"
)]
class Subject(BaseModel):
# Using an annotated type defined elsewhere:
first_name: FirstName = ""
# Documenting a field inline:
last_name: Annotated[str, Field(
description="The subject's last name", example="Torvalds"
)] = ""
# Traditional method without using Annotated
# Field needs an extra argument for the default value
date_of_birth: date = Field(
...,
description="The subject's date of birth",
example="1969-12-28",
)
Using the model class:
>>> guido = Subject(first_name='Guido', last_name='van Rossum', date_of_birth=date(1956, 1, 31))
>>> print(guido)
first_name='Guido' last_name='van Rossum' date_of_birth=datetime.date(1956, 1, 31)
Pydantic models can give you a JSON schema:
>>> from pprint import pprint
>>> pprint(Subject.schema())
{'properties': {'date_of_birth': {'description': "The subject's date of birth",
'example': '1969-12-28',
'format': 'date',
'title': 'Date Of Birth',
'type': 'string'},
'first_name': {'default': '',
'description': "The subject's first name",
'example': 'Linus',
'title': 'First Name',
'type': 'string'},
'last_name': {'default': '',
'description': "The subject's last name",
'example': 'Torvalds',
'title': 'Last Name',
'type': 'string'}},
'required': ['date_of_birth'],
'title': 'Subject',
'type': 'object'}
>>>
If you use this class in a FastAPI application the OpenApi specification has example and description for all three of these taken from the relevant Field.
And here's the original answer which was true back then but hasn't stood the test of time:
The docstring is always an attribute of an object (module, class or function), not tied to a specific variable.
That means if you could do:
t = 42
t.__doc__ = "something" # this raises AttributeError: '__doc__' is read-only
you would be setting the documentation for the integer 42 not for the variable t
. As soon as you rebind t
you lose the docstring. Immutable objects such as numbers of strings sometimes have a single object shared between different users, so in this example you would probably actually have set the docstring for all occurences of 42
throughout your program.
print(42 .__doc__) # would print "something" if the above worked!
For mutable objects it wouldn't necessarily be harmful but would still be of limited use if you rebind the object.
If you want to document an attribute of a class then use the class's docstring to describe it.
Epydoc allows for docstrings on variables:
While the language doesn't directly provides for them, Epydoc supports variable docstrings: if a variable assignment statement is immediately followed by a bare string literal, then that assignment is treated as a docstring for that variable.
Example:
class A:
x = 22
"""Docstring for class variable A.x"""
def __init__(self, a):
self.y = a
"""Docstring for instance variable A.y"""
Well, even though Python does not treat strings defined immediately after a global definition as a docstring for the variable, sphinx does and it is certainly not a bad practice to include them.
debug = False
'''Set to True to turn on debugging mode. This enables opening IPython on
exceptions.
'''
Here is some code that will scan a module and pull out names of global variable definitions, the value and a docstring that follows.
def GetVarDocs(fname):
'''Read the module referenced in fname (often <module>.__file__) and return a
dict with global variables, their value and the "docstring" that follows
the definition of the variable
'''
import ast,os
fname = os.path.splitext(fname)[0]+'.py' # convert .pyc to .py
with open(fname, 'r') as f:
fstr = f.read()
d = {}
key = None
for node in ast.walk(ast.parse(fstr)):
if isinstance(node,ast.Assign):
key = node.targets[0].id
d[key] = [node.value.id,'']
continue
elif isinstance(node,ast.Expr) and key:
d[key][1] = node.value.s.strip()
key = None
return d
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With