Skip to main content

Python Comments vs Docstrings Practice Problems & Exercises

Practice: Comments vs Docstrings

10 problems3 Easy4 Medium3 Hard30–45 min
← Back to lesson

Easy

#1Docstring ExplorerEasy
__doc__built-inintrospection

Access the __doc__ attribute on Python's built-in str.split method. Print the first line of its docstring. Then print whether len.__doc__ contains the word "length".

Python
# Access the docstring of str.split and print the first line
first_line = str.split.__doc__.split('\n')[0]
print(first_line)

# Check if len's docstring mentions "length"
print("length" in len.__doc__)
Solution
first_line = str.split.__doc__.split('\n')[0]
print(first_line)

print("length" in len.__doc__)

Why it works: Every function and method in Python carries its docstring as the __doc__ attribute. This is not just decoration — it is executable metadata stored at runtime. You can programmatically inspect, search, and process docstrings. This is the foundation of help(), IDE tooltips, and documentation generators like Sphinx.

Expected Output
Return a list of the words in the string, using sep as the delimiter string.\nTrue
Hints

Hint 1: Every built-in function has a `__doc__` attribute that returns its docstring as a string.

Hint 2: Access it with dot notation: `some_function.__doc__`.

#2Comment or Docstring?Easy
commentsdocstringsbest-practice

Write classify_documentation(code_snippet) that identifies whether a code snippet contains a comment, a docstring, or both.

print(classify_documentation("# TODO: fix this later"))
print(classify_documentation('def f():\n """Does stuff."""\n pass'))
print(classify_documentation('def f():\n """Does stuff."""\n # helper\n pass'))
print(classify_documentation("x = 10 # magic number"))
Solution
def classify_documentation(code_snippet: str) -> str:
has_comment = any(
line.lstrip().startswith('#')
for line in code_snippet.split('\n')
)
has_docstring = '"""' in code_snippet or "'''" in code_snippet
if has_comment and has_docstring:
return "both"
elif has_docstring:
return "docstring"
else:
return "comment"

Key distinction: Comments (#) are stripped by the parser and never exist at runtime. Docstrings (triple-quoted strings as the first statement of a module, class, or function) become the __doc__ attribute and persist at runtime. This fundamental difference determines when to use which: comments explain why to future developers reading source; docstrings describe what to users who may never see the source.

def classify_documentation(code_snippet: str) -> str:
    """Given a code snippet string, return 'comment', 'docstring', or 'both'.

    Rules:
    - If it contains a line starting with # -> has comment
    - If it contains triple quotes (triple-double or triple-single) -> has docstring
    - Return 'both' if both are present
    """
    pass
Expected Output
comment\ndocstring\nboth\ncomment
Hints

Hint 1: Check for lines that start with `#` (after stripping whitespace) for comments.

Hint 2: Check for `"""` or `'''` for docstrings.

#3Help Output PredictorEasy
helpdocstring__doc__

Predict what happens when you call help() on a function with a docstring versus one without. Then write code to demonstrate the difference programmatically.

Python
def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}!"

def mystery(x):
    return x * 2

# Print the docstring that help() would show for greet
print(greet.__doc__)

# Print the docstring for mystery
print(mystery.__doc__)

# Show how to handle the None case
print(mystery.__doc__ if mystery.__doc__ else "No docstring found.")
Solution
def greet(name):
"""Say hello to someone."""
return f"Hello, {name}!"

def mystery(x):
return x * 2

print(greet.__doc__)
print(mystery.__doc__)
print(mystery.__doc__ if mystery.__doc__ else "No docstring found.")

Output:

greet(name)
Say hello to someone.
None
No docstring found.

Why this matters: help() internally reads __doc__. When there is no docstring, __doc__ is None — not an empty string, not missing, but explicitly None. This is important because you cannot call .strip() or .split() on None. Always check before processing: if func.__doc__: is the safe pattern.

Expected Output
greet(name)\n    Say hello to someone.\nNone\nNo docstring found.
Hints

Hint 1: `help()` reads the `__doc__` attribute. No docstring means `help()` shows minimal output.

Hint 2: A function without a docstring has `__doc__` set to `None`.


Medium

#4Google-Style Docstring WriterMedium
google-styledocstringdocumentation

Write a complete Google-style docstring for calculate_bmi. It must include Args, Returns, Raises, and Examples sections. Then verify your docstring is accessible at runtime.

result = calculate_bmi(70, 1.75)
print(result)

# Verify docstring quality
doc = calculate_bmi.__doc__
print("Args:" in doc)
print("Returns:" in doc)
print("Raises:" in doc)
print("weight_kg" in doc)
Solution
def calculate_bmi(weight_kg: float, height_m: float) -> dict:
"""Calculate Body Mass Index and return BMI with category.

Computes BMI using the standard formula weight / height^2 and
classifies the result into WHO-defined categories.

Args:
weight_kg (float): Body weight in kilograms. Must be positive.
height_m (float): Height in meters. Must be positive.

Returns:
dict: A dictionary with two keys:
- 'bmi' (float): The calculated BMI rounded to 1 decimal.
- 'category' (str): One of 'underweight', 'normal',
'overweight', or 'obese'.

Raises:
ValueError: If weight_kg or height_m is not positive.

Examples:
>>> calculate_bmi(70, 1.75)
{'bmi': 22.9, 'category': 'normal'}
>>> calculate_bmi(90, 1.80)
{'bmi': 27.8, 'category': 'overweight'}
"""
if height_m <= 0:
raise ValueError("Height must be positive.")
if weight_kg <= 0:
raise ValueError("Weight must be positive.")

bmi = weight_kg / (height_m ** 2)

if bmi < 18.5:
category = "underweight"
elif bmi < 25.0:
category = "normal"
elif bmi < 30.0:
category = "overweight"
else:
category = "obese"

return {"bmi": round(bmi, 1), "category": category}


result = calculate_bmi(70, 1.75)
print(result)

doc = calculate_bmi.__doc__
print("Args:" in doc)
print("Returns:" in doc)
print("Raises:" in doc)
print("weight_kg" in doc)

Google-style conventions:

  • First line: one-sentence summary (imperative mood: "Calculate", not "Calculates").
  • Args: each param on its own line, name (type): description.
  • Returns: describe the shape and meaning of the return value.
  • Raises: list each exception and when it is raised.
  • Examples: use >>> doctest format so tools like pytest --doctest-modules can run them automatically.
def calculate_bmi(weight_kg: float, height_m: float) -> dict:
    """WRITE YOUR GOOGLE-STYLE DOCSTRING HERE.

    Args:

    Returns:

    Raises:

    Examples:

    """
    if height_m <= 0:
        raise ValueError("Height must be positive.")
    if weight_kg <= 0:
        raise ValueError("Weight must be positive.")

    bmi = weight_kg / (height_m ** 2)

    if bmi < 18.5:
        category = "underweight"
    elif bmi < 25.0:
        category = "normal"
    elif bmi < 30.0:
        category = "overweight"
    else:
        category = "obese"

    return {"bmi": round(bmi, 1), "category": category}
Expected Output
{'bmi': 22.9, 'category': 'normal'}\nTrue\nTrue\nTrue\nTrue
Hints

Hint 1: Google style uses `Args:`, `Returns:`, `Raises:`, and `Examples:` sections.

Hint 2: Each arg is documented as `name (type): description` indented under `Args:`.

Hint 3: Your docstring should mention all parameters, the return type, and the ValueError.

#5Docstring Parameter ParserMedium
parsingdocstringgoogle-style

Implement parse_params(docstring) that extracts parameter names and descriptions from a Google-style docstring's Args: section.

doc1 = """Calculate BMI.

Args:
weight_kg (float): Body weight in kilograms.
height_m (float): Height in meters.

Returns:
dict: BMI result.
"""

doc2 = """Process items.

Args:
items (list): List of items to process.
reverse (bool): If True, process in reverse order.
"""

print(parse_params(doc1))
print(parse_params(doc2))
Solution
import re

def parse_params(docstring: str) -> dict:
params = {}
lines = docstring.split('\n')
in_args = False

for line in lines:
stripped = line.strip()

if stripped == 'Args:':
in_args = True
continue

if in_args:
# Stop at next section or empty non-indented line
if stripped and not line.startswith(' ') and not line.startswith('\t'):
break
if stripped.endswith(':') and not stripped.startswith(' '):
break

# Match param line: name (type): description
match = re.match(r'\s+(\w+)\s*\([^)]*\):\s*(.*)', line)
if match:
params[match.group(1)] = match.group(2).strip()

return params


doc1 = """Calculate BMI.

Args:
weight_kg (float): Body weight in kilograms.
height_m (float): Height in meters.

Returns:
dict: BMI result.
"""

doc2 = """Process items.

Args:
items (list): List of items to process.
reverse (bool): If True, process in reverse order.
"""

print(parse_params(doc1))
print(parse_params(doc2))

How it works: The parser is a simple state machine: once it sees Args:, it enters "argument parsing" mode and matches each indented line against the pattern name (type): description. It stops when it hits a non-indented line or another section header. This is essentially what Sphinx's Napoleon extension does internally to convert Google-style docstrings into reStructuredText.

def parse_params(docstring: str) -> dict:
    """Parse parameter descriptions from a Google-style docstring.

    Returns a dict mapping parameter names to their descriptions.
    Only parses the 'Args:' section.

    Example:
        Input docstring with:
            Args:
                name (str): The user's name.
                age (int): The user's age in years.
        Returns: {'name': 'The user\'s name.', 'age': 'The user\'s age in years.'}
    """
    pass
Expected Output
{'weight_kg': 'Body weight in kilograms.', 'height_m': 'Height in meters.'}\n{'items': 'List of items to process.', 'reverse': 'If True, process in reverse order.'}
Hints

Hint 1: Find the line containing `Args:`, then parse subsequent indented lines.

Hint 2: Each param line looks like: ` name (type): description`.

Hint 3: Stop parsing when you hit a line that is not indented or is another section header like `Returns:`.

#6Runtime Proof: Docstrings vs CommentsMedium
runtime__doc__commentsintrospection

Prove that docstrings are accessible at runtime while comments are not. Demonstrate this by writing two functions — one documented with a comment, one with a docstring — and showing what Python retains.

Python
import dis

def with_docstring():
    """This function has a docstring."""
    pass

def with_comment():
    # This function has a comment.
    pass

# Prove docstring is runtime-accessible
print(f"Docstring: {with_docstring.__doc__}")

# Prove comment is NOT runtime-accessible
has_comment_at_runtime = with_comment.__doc__ is not None
print(f"Comment exists at runtime: {has_comment_at_runtime}")

# Docstring is a real Python object
print(f"Docstring is a str: {isinstance(with_docstring.__doc__, str)}")

# Docstrings survive compilation — they are stored in the code object
print(f"Docstring survives: {with_docstring.__code__.co_consts[0] == with_docstring.__doc__}")
Solution
import dis

def with_docstring():
"""This function has a docstring."""
pass

def with_comment():
# This function has a comment.
pass

print(f"Docstring: {with_docstring.__doc__}")
has_comment_at_runtime = with_comment.__doc__ is not None
print(f"Comment exists at runtime: {has_comment_at_runtime}")
print(f"Docstring is a str: {isinstance(with_docstring.__doc__, str)}")
print(f"Docstring survives: {with_docstring.__code__.co_consts[0] == with_docstring.__doc__}")

The fundamental difference:

  • Comments are stripped during lexical analysis (tokenization). They never make it into bytecode. They exist only in .py source files.
  • Docstrings are compiled into the code object's co_consts tuple and assigned to the function's __doc__ attribute. They are real str objects that consume memory and can be inspected, tested, and processed at runtime.

This is why docstrings power help(), Sphinx, doctest, and IDE intellisense — they are executable metadata, not decorative text.

Expected Output
Docstring: This function has a docstring.\nComment exists at runtime: False\nDocstring is a str: True\nDocstring survives: True
Hints

Hint 1: Comments are stripped by the parser and never appear in `__doc__` or any runtime attribute.

Hint 2: Docstrings become the `__doc__` attribute — they are real string objects at runtime.

Hint 3: Use `inspect.getsource()` to show that comments only exist in source code, not in compiled bytecode.

#7Docstring Presence ValidatorMedium
introspectionclassesvalidation

Write validate_docstrings(cls) that inspects a class and reports which public methods have docstrings and which do not.

class DataProcessor:
"""A sample data processor."""

def save(self, path):
"""Save data to the given path."""
pass

def load(self, path):
"""Load data from the given path."""
pass

def process(self, data):
pass

def validate(self, data):
pass

result = validate_docstrings(DataProcessor)
print(f"Missing: {result['missing']}")
print(f"Present: {result['present']}")
print(f"Coverage: {result['coverage'] * 100:.1f}%")
Solution
import inspect

def validate_docstrings(cls) -> dict:
missing = []
present = []

for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
# Skip private/dunder methods
if name.startswith('_'):
continue
if method.__doc__:
present.append(name)
else:
missing.append(name)

total = len(missing) + len(present)
coverage = len(present) / total if total > 0 else 1.0

return {
'missing': missing,
'present': present,
'coverage': coverage,
}


class DataProcessor:
"""A sample data processor."""

def save(self, path):
"""Save data to the given path."""
pass

def load(self, path):
"""Load data from the given path."""
pass

def process(self, data):
pass

def validate(self, data):
pass

result = validate_docstrings(DataProcessor)
print(f"Missing: {result['missing']}")
print(f"Present: {result['present']}")
print(f"Coverage: {result['coverage'] * 100:.1f}%")

Why this matters in production: Documentation coverage is a real metric enforced by tools like interrogate, pydocstyle, and Sphinx's coverage builder. Many teams set a minimum threshold (e.g., 80%) in CI. The pattern here — programmatically inspecting classes via inspect — is exactly how those tools work under the hood.

import inspect

def validate_docstrings(cls) -> dict:
    """Check that all public methods of a class have docstrings.

    Returns a dict with:
        - 'missing': list of method names without docstrings
        - 'present': list of method names with docstrings
        - 'coverage': float (0.0 to 1.0) representing percentage with docstrings
    """
    pass
Expected Output
Missing: ['process', 'validate']\nPresent: ['save', 'load']\nCoverage: 50.0%
Hints

Hint 1: Use `inspect.getmembers(cls, predicate=inspect.isfunction)` to get all methods.

Hint 2: Filter out private methods (those starting with `_`) unless you want to check them too.

Hint 3: A method has a docstring if its `__doc__` attribute is not `None`.


Hard

#8Docstring-to-Dict ConverterHard
parsinggoogle-stylenumpy-styleadvanced

Build a parser that converts both Google-style and NumPy-style docstrings into structured dictionaries.

google_doc = """Calculate the weighted average of values.

Computes a weighted mean, falling back to simple mean
if no weights are provided.

Args:
values (list): List of numeric values.
weights (list, optional): Optional list of weights.

Returns:
float: The weighted average.

Raises:
ValueError: If values is empty.
ValueError: If weights length doesn't match values.
"""

numpy_doc = """Compute the distance between two points.

Uses the Euclidean distance formula.

Parameters
----------
point_a : tuple
First point as (x, y) tuple.
point_b : tuple
Second point as (x, y) tuple.

Returns
-------
float
Euclidean distance between the points.
"""

result1 = docstring_to_dict(google_doc, style="google")
print(f"Summary: {result1['summary']}")
print(f"Params: {len(result1['params'])}")
for name, info in result1['params'].items():
print(f" {name}: {info['description']}")
print(f"Returns: {result1['returns']['type']} - {result1['returns']['description']}")
print(f"Raises: {result1['raises'][0]['exception']}")

print("---")

result2 = docstring_to_dict(numpy_doc, style="numpy")
print(f"Summary: {result2['summary']}")
print(f"Params: {len(result2['params'])}")
for name, info in result2['params'].items():
print(f" {name}: {info['description']}")
print(f"Returns: {result2['returns']['type']} - {result2['returns']['description']}")
Solution
import re

def docstring_to_dict(docstring: str, style: str = "google") -> dict:
lines = docstring.strip().split('\n')
result = {
'summary': '',
'description': '',
'params': {},
'returns': None,
'raises': [],
}

# Extract summary (first non-empty line)
result['summary'] = lines[0].strip()

if style == "google":
return _parse_google(lines, result)
else:
return _parse_numpy(lines, result)


def _parse_google(lines, result):
section = None
desc_lines = []
current_param = None

# Find where description ends and sections begin
i = 1
while i < len(lines):
stripped = lines[i].strip()
if stripped in ('Args:', 'Returns:', 'Raises:', 'Examples:'):
break
desc_lines.append(stripped)
i += 1

result['description'] = ' '.join(l for l in desc_lines if l).strip()

while i < len(lines):
stripped = lines[i].strip()

if stripped == 'Args:':
section = 'args'
i += 1
continue
elif stripped == 'Returns:':
section = 'returns'
i += 1
continue
elif stripped == 'Raises:':
section = 'raises'
i += 1
continue
elif stripped.endswith(':') and not lines[i].startswith(' '):
section = None
i += 1
continue

if section == 'args':
match = re.match(r'\s+(\w+)\s*\(([^)]*)\):\s*(.*)', lines[i])
if match:
name, typ, desc = match.group(1), match.group(2), match.group(3)
result['params'][name] = {'type': typ.strip(), 'description': desc.strip()}
current_param = name

elif section == 'returns':
match = re.match(r'\s+(\w+):\s*(.*)', lines[i])
if match:
result['returns'] = {
'type': match.group(1).strip(),
'description': match.group(2).strip(),
}

elif section == 'raises':
match = re.match(r'\s+(\w+):\s*(.*)', lines[i])
if match:
result['raises'].append({
'exception': match.group(1).strip(),
'description': match.group(2).strip(),
})

i += 1

return result


def _parse_numpy(lines, result):
desc_lines = []
section = None
i = 1
current_param_name = None
current_param_type = None

# Collect description until first section header
while i < len(lines):
if i + 1 < len(lines) and re.match(r'^-+$', lines[i + 1].strip()):
break
desc_lines.append(lines[i].strip())
i += 1

result['description'] = ' '.join(l for l in desc_lines if l).strip()

while i < len(lines):
stripped = lines[i].strip()

# Check for section header (word followed by dashed underline)
if i + 1 < len(lines) and re.match(r'^-+$', lines[i + 1].strip()):
if 'parameter' in stripped.lower():
section = 'params'
elif 'return' in stripped.lower():
section = 'returns'
elif 'raise' in stripped.lower():
section = 'raises'
i += 2 # Skip header and underline
continue

if section == 'params':
# Param definition line: "name : type"
match = re.match(r'^(\w+)\s*:\s*(.*)', stripped)
if match and not lines[i].startswith(' '):
current_param_name = match.group(1)
current_param_type = match.group(2).strip()
result['params'][current_param_name] = {
'type': current_param_type,
'description': '',
}
elif current_param_name and stripped:
# Description line (indented)
existing = result['params'][current_param_name]['description']
if existing:
result['params'][current_param_name]['description'] += ' ' + stripped
else:
result['params'][current_param_name]['description'] = stripped

elif section == 'returns':
if not lines[i].startswith(' ') and stripped:
current_return_type = stripped
result['returns'] = {'type': current_return_type, 'description': ''}
elif result['returns'] and stripped:
existing = result['returns']['description']
if existing:
result['returns']['description'] += ' ' + stripped
else:
result['returns']['description'] = stripped

elif section == 'raises':
match = re.match(r'^(\w+)', stripped)
if match and not lines[i].startswith(' '):
result['raises'].append({
'exception': match.group(1),
'description': '',
})
elif result['raises'] and stripped:
result['raises'][-1]['description'] += stripped

i += 1

return result


google_doc = """Calculate the weighted average of values.

Computes a weighted mean, falling back to simple mean
if no weights are provided.

Args:
values (list): List of numeric values.
weights (list, optional): Optional list of weights.

Returns:
float: The weighted average.

Raises:
ValueError: If values is empty.
ValueError: If weights length doesn't match values.
"""

numpy_doc = """Compute the distance between two points.

Uses the Euclidean distance formula.

Parameters
----------
point_a : tuple
First point as (x, y) tuple.
point_b : tuple
Second point as (x, y) tuple.

Returns
-------
float
Euclidean distance between the points.
"""

result1 = docstring_to_dict(google_doc, style="google")
print(f"Summary: {result1['summary']}")
print(f"Params: {len(result1['params'])}")
for name, info in result1['params'].items():
print(f" {name}: {info['description']}")
print(f"Returns: {result1['returns']['type']} - {result1['returns']['description']}")
print(f"Raises: {result1['raises'][0]['exception']}")

print("---")

result2 = docstring_to_dict(numpy_doc, style="numpy")
print(f"Summary: {result2['summary']}")
print(f"Params: {len(result2['params'])}")
for name, info in result2['params'].items():
print(f" {name}: {info['description']}")
print(f"Returns: {result2['returns']['type']} - {result2['returns']['description']}")

Architecture insight: This is essentially a simplified version of what Napoleon (Sphinx extension) and griffe (mkdocstrings parser) do. Real parsers also handle multi-line descriptions, nested types like dict[str, list[int]], continuation indentation, and cross-references. The state-machine approach (tracking which section you are in) is the standard pattern for docstring parsers.

import re

def docstring_to_dict(docstring: str, style: str = "google") -> dict:
    """Parse a docstring into a structured dictionary.

    Supports both Google and NumPy styles.

    Returns a dict with keys:
        - 'summary': first line of the docstring
        - 'description': extended description (if any)
        - 'params': dict mapping param name -> {'type': ..., 'description': ...}
        - 'returns': {'type': ..., 'description': ...} or None
        - 'raises': list of {'exception': ..., 'description': ...}
    """
    pass
Expected Output
Summary: Calculate the weighted average of values.\nParams: 2\n  values: List of numeric values.\n  weights: Optional list of weights.\nReturns: float - The weighted average.\nRaises: ValueError\n---\nSummary: Compute the distance between two points.\nParams: 2\n  point_a: First point as (x, y) tuple.\n  point_b: Second point as (x, y) tuple.\nReturns: float - Euclidean distance between the points.
Hints

Hint 1: For Google style: sections are `Args:`, `Returns:`, `Raises:` followed by indented content.

Hint 2: For NumPy style: sections are `Parameters`, `Returns`, `Raises` followed by a dashed underline `----------`.

Hint 3: Parse the summary as everything before the first blank line. The description is everything between summary and the first section.

#9Auto-Docstring DecoratorHard
decoratortype-hintsinspectmetaprogramming

Create a @auto_docstring decorator that generates Google-style docstrings from type hints. If the function already has a one-line docstring, use it as the summary; otherwise, use the function name in title case.

@auto_docstring
def calculate(x: int, y: int) -> float:
return x / y

@auto_docstring
def transform(data: list, strategy: str = "default", verbose: bool = False) -> dict:
"""Transform data using the given strategy."""
return {}

print("calculate:")
print(calculate.__doc__)
print("---")
print("transform:")
print(transform.__doc__)
Solution
import inspect
import functools

def auto_docstring(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

sig = inspect.signature(func)
hints = func.__annotations__

# Use existing docstring as summary, or generate from name
existing = func.__doc__
if existing:
summary = existing.strip().split('\n')[0]
else:
summary = func.__name__.replace('_', ' ').capitalize() + '.'

# Build Args section
args_lines = []
for name, param in sig.parameters.items():
type_name = hints.get(name, type(None)).__name__ if name in hints else 'Any'
args_lines.append(f" {name} ({type_name}): No description provided.")

# Build Returns section
returns_line = None
if 'return' in hints:
ret_type = hints['return'].__name__
returns_line = f" {ret_type}: No description provided."

# Assemble docstring
parts = [f" {summary}", ""]
if args_lines:
parts.append(" Args:")
for line in args_lines:
parts.append(f" {line}")
parts.append("")
if returns_line:
parts.append(" Returns:")
parts.append(f" {returns_line}")

wrapper.__doc__ = '\n'.join(parts)
return wrapper


@auto_docstring
def calculate(x: int, y: int) -> float:
return x / y

@auto_docstring
def transform(data: list, strategy: str = "default", verbose: bool = False) -> dict:
"""Transform data using the given strategy."""
return {}

print("calculate:")
print(calculate.__doc__)
print("---")
print("transform:")
print(transform.__doc__)

How it works: The decorator uses inspect.signature() to extract parameter names and __annotations__ to get type hints. It constructs a Google-style docstring and assigns it to the wrapper's __doc__. functools.wraps copies over the original __name__, __module__, and other attributes.

Production parallel: This is how tools like pydantic's BaseModel, FastAPI's route decorators, and click's CLI decorators auto-generate documentation from type hints. The pattern of "reflect on annotations at definition time and produce metadata" is central to modern Python metaprogramming.

import inspect
import functools

def auto_docstring(func):
    """Decorator that auto-generates a Google-style docstring
    from the function's type hints and parameter names.

    If the function already has a docstring, prepend the
    auto-generated parameter info to it.
    """
    pass
Expected Output
calculate:\n  Calculate.\n\n  Args:\n      x (int): No description provided.\n      y (int): No description provided.\n\n  Returns:\n      float: No description provided.\n---\ntransform:\n  Transform data using the given strategy.\n\n  Args:\n      data (list): No description provided.\n      strategy (str): No description provided.\n      verbose (bool): No description provided.\n\n  Returns:\n      dict: No description provided.
Hints

Hint 1: Use `inspect.signature(func)` to get parameter names and annotations.

Hint 2: Use `func.__annotations__` or `sig.return_annotation` for the return type.

Hint 3: Use `functools.wraps(func)` to preserve the original function metadata, then override `__doc__`.

#10Documentation Coverage CheckerHard
inspectmodulescoveragetooling

Build a comprehensive documentation coverage checker that analyzes a module and reports exactly which public items are documented and which are not. Test it against a sample module you create dynamically.

Python
import types
import inspect

def check_doc_coverage(module) -> dict:
    result = {
        'module_doc': module.__doc__ is not None and len(module.__doc__.strip()) > 0,
        'functions': {},
        'classes': {},
        'total_documented': 0,
        'total_items': 0,
        'coverage_pct': 0.0,
    }

    documented = 0
    total = 0

    # Count module docstring
    total += 1
    if result['module_doc']:
        documented += 1

    # Check public functions
    for name, obj in inspect.getmembers(module, inspect.isfunction):
        if name.startswith('_'):
            continue
        has_doc = obj.__doc__ is not None and len(obj.__doc__.strip()) > 0
        result['functions'][name] = has_doc
        total += 1
        if has_doc:
            documented += 1

    # Check public classes and their methods
    for name, cls in inspect.getmembers(module, inspect.isclass):
        if name.startswith('_'):
            continue
        cls_has_doc = cls.__doc__ is not None and len(cls.__doc__.strip()) > 0
        methods = {}
        total += 1
        if cls_has_doc:
            documented += 1

        for mname, method in inspect.getmembers(cls, inspect.isfunction):
            if mname.startswith('_'):
                continue
            m_has_doc = method.__doc__ is not None and len(method.__doc__.strip()) > 0
            methods[mname] = m_has_doc
            total += 1
            if m_has_doc:
                documented += 1

        result['classes'][name] = {
            'has_doc': cls_has_doc,
            'methods': methods,
        }

    result['total_documented'] = documented
    result['total_items'] = total
    result['coverage_pct'] = (documented / total * 100) if total > 0 else 100.0

    return result


# Create a sample module dynamically
sample = types.ModuleType("sample_module")
sample.__doc__ = "A sample module for testing coverage."

def public_func():
    """A well-documented function."""
    pass

def another_func():
    pass

class MyClass:
    """A documented class."""
    def public_method(self):
        """A documented method."""
        pass
    def undocumented_method(self):
        pass

class BareMethods:
    def do_something(self):
        pass

# Attach to module
sample.public_func = public_func
sample.another_func = another_func
sample.MyClass = MyClass
sample.BareMethods = BareMethods

# Run coverage check
report = check_doc_coverage(sample)

print(f"Module docstring: {report['module_doc']}")
print(f"\nFunctions:")
for name, has_doc in report['functions'].items():
    print(f"  {name}: {has_doc}")
print(f"\nClasses:")
for cname, info in report['classes'].items():
    print(f"  {cname}:")
    print(f"    class doc: {info['has_doc']}")
    for mname, has_doc in info['methods'].items():
        print(f"    {mname}: {has_doc}")

d = report['total_documented']
t = report['total_items']
print(f"\nCoverage: {d}/{t} = {report['coverage_pct']:.1f}%")
Solution

The solution is embedded in the starter code above. Here is the core logic explained:

import inspect
import types

def check_doc_coverage(module) -> dict:
result = {
'module_doc': module.__doc__ is not None and len(module.__doc__.strip()) > 0,
'functions': {},
'classes': {},
'total_documented': 0,
'total_items': 0,
'coverage_pct': 0.0,
}

documented = 0
total = 0

# Module docstring counts as one item
total += 1
if result['module_doc']:
documented += 1

# Public functions (skip _private)
for name, obj in inspect.getmembers(module, inspect.isfunction):
if name.startswith('_'):
continue
has_doc = obj.__doc__ is not None and len(obj.__doc__.strip()) > 0
result['functions'][name] = has_doc
total += 1
if has_doc:
documented += 1

# Public classes + their public methods
for name, cls in inspect.getmembers(module, inspect.isclass):
if name.startswith('_'):
continue
cls_has_doc = cls.__doc__ is not None and len(cls.__doc__.strip()) > 0
methods = {}
total += 1
if cls_has_doc:
documented += 1

for mname, method in inspect.getmembers(cls, inspect.isfunction):
if mname.startswith('_'):
continue
m_has_doc = method.__doc__ is not None and len(method.__doc__.strip()) > 0
methods[mname] = m_has_doc
total += 1
if m_has_doc:
documented += 1

result['classes'][name] = {'has_doc': cls_has_doc, 'methods': methods}

result['total_documented'] = documented
result['total_items'] = total
result['coverage_pct'] = (documented / total * 100) if total > 0 else 100.0
return result

Production tools that do this:

  • interrogate: CLI tool that checks docstring coverage and can enforce a minimum threshold in CI (interrogate --fail-under 80).
  • pydocstyle: Checks docstring style compliance (PEP 257, Google, NumPy).
  • Sphinx coverage: The Sphinx documentation builder has a coverage extension that reports undocumented API items.

Key insight: The inspect module is Python's reflection API. getmembers() with predicates like isfunction and isclass gives you the same view of a module that dir() does, but with filtering. This is the foundation of every documentation tool, linter, and IDE plugin that analyzes Python code structure.

import inspect
import types

def check_doc_coverage(module) -> dict:
    """Analyze documentation coverage of a module.

    Checks all public functions, classes, and methods for docstrings.

    Returns a dict with:
        - 'module_doc': bool (does the module have a docstring?)
        - 'functions': dict mapping name -> bool (has docstring)
        - 'classes': dict mapping class_name -> {
              'has_doc': bool,
              'methods': dict mapping method_name -> bool
          }
        - 'total_documented': int
        - 'total_items': int
        - 'coverage_pct': float
    """
    pass
Expected Output
Module docstring: True\n\nFunctions:\n  public_func: True\n  another_func: False\n\nClasses:\n  MyClass:\n    class doc: True\n    public_method: True\n    undocumented_method: False\n  BareMethods:\n    class doc: False\n    do_something: False\n\nCoverage: 4/8 = 50.0%
Hints

Hint 1: Use `inspect.getmembers()` with predicates like `inspect.isfunction` and `inspect.isclass`.

Hint 2: For classes, iterate their methods with `inspect.getmembers(cls, predicate=inspect.isfunction)`.

Hint 3: Filter out names starting with `_` to focus on public API.

Hint 4: Count the module docstring, each function, each class, and each method as separate items.

© 2026 EngineersOfAI. All rights reserved.