Writing Effective Docstrings: Google vs. NumPy vs. reStructuredText Styles

I’ve reviewed thousands of lines of Python code in my career, and I can tell you this with certainty: the difference between a library I love using and one I dread often comes down to its docstrings. You know what I mean - those triple-quoted strings ("""like this""") that turn a mysterious function into a well-documented friend.

But here’s the thing: writing good docstrings is more than just dropping some comments into your code. It’s about creating clear, consistent documentation that both humans and tools (like Sphinx’s autodoc) can understand. In this post, I’ll share what I’ve learned about writing docstrings that actually help people use your code.

What Makes a Good Docstring?

Think of a docstring like a good movie trailer - it needs to hook you quickly with the main plot (what the code does), give you enough detail to understand what you’re getting into (how to use it), but not bore you with every single detail. Here’s what that looks like in practice:

  1. Start with a punchy summary line that uses the imperative mood (“Calculate the sum” rather than “Calculates the sum”)
  2. Add a blank line (trust me, this matters for parsing)
  3. If needed, follow with a more detailed explanation
  4. List out your parameters, return values, and any exceptions that might pop up
  5. Optional extras like examples or “See Also” sections

But here’s where it gets interesting - there are three main styles for structuring all this information, and each has its own flavor. Let’s break them down.

The Three Schools of Docstring Style

1. The Classic: reStructuredText (reST)

This is Sphinx’s native language. It’s like the vanilla ice cream of docstring formats - it works everywhere without any extra toppings:

def calculate_interest(principal, rate, years):
    """Calculate compound interest over a period of years.

    Takes the initial investment and determines future value.

    :param principal: Initial amount invested
    :type principal: float
    :param rate: Annual interest rate (as decimal)
    :type rate: float
    :param years: Number of years to compound
    :type years: int
    :return: Final amount after compound interest
    :rtype: float
    :raises ValueError: If rate is negative or principal is zero
    """
    if principal <= 0 or rate < 0:
        raise ValueError("Principal must be positive and rate must be non-negative")
    return principal * (1 + rate) ** years

The beauty of reST style is that it works out of the box with Sphinx. The downside? Well, some find all those :param: and :type: fields a bit verbose.

2. The Popular Kid: Google Style

Google’s style is like the cool new coffee shop that opened up downtown - everyone seems to love it, and for good reason:

def calculate_interest(principal, rate, years):
    """Calculate compound interest over a period of years.

    Takes the initial investment and determines future value.

    Args:
        principal (float): Initial amount invested
        rate (float): Annual interest rate (as decimal)
        years (int): Number of years to compound

    Returns:
        float: Final amount after compound interest

    Raises:
        ValueError: If rate is negative or principal is zero
    """
    if principal <= 0 or rate < 0:
        raise ValueError("Principal must be positive and rate must be non-negative")
    return principal * (1 + rate) ** years

Clean, readable, and intuitive. The only catch? You’ll need to enable the sphinx.ext.napoleon extension in your Sphinx configuration.

3. The Scientist: NumPy Style

If you’re working in scientific Python, you might feel more at home with NumPy’s style:

def calculate_interest(principal, rate, years):
    """Calculate compound interest over a period of years.

    Takes the initial investment and determines future value.

    Parameters
    ----------
    principal : float
        Initial amount invested
    rate : float
        Annual interest rate (as decimal)
    years : int
        Number of years to compound

    Returns
    -------
    float
        Final amount after compound interest

    Raises
    ------
    ValueError
        If rate is negative or principal is zero
    """
    if principal <= 0 or rate < 0:
        raise ValueError("Principal must be positive and rate must be non-negative")
    return principal * (1 + rate) ** years

Like Google style, this needs the napoleon extension, but it’s particularly good at handling complex parameter descriptions - something you’ll appreciate if you’re documenting scientific functions.

Picking Your Style

Here’s my advice: pick the style that:

  1. Matches your project’s ecosystem (NumPy style for scientific libraries, etc.)
  2. You and your team can read comfortably
  3. Works with your documentation tools

But most importantly? Be consistent. Nothing confuses readers more than mixing styles within the same project.

The Real Secret to Great Docstrings

Want to know what really makes a docstring effective? It’s not about the style you choose - it’s about empathy for your users. When I’m writing docstrings, I try to answer the questions I wish other libraries’ docs had answered for me:

  • What does this function actually do?
  • What can go wrong when I use it?
  • Are there any gotchas I should know about?
  • Can I see a quick example?

And here’s a pro tip: write your docstrings first, before implementing the function. It’s like writing a contract with your future self and your users. If you can’t explain what your function will do clearly in the docstring, you might need to rethink your design.

Making It All Work with Sphinx

Whatever style you choose, you’ll want to make sure Sphinx can parse it. For reST style, you’re already set. For Google or NumPy style, add this to your conf.py:

extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.napoleon',
    # ... your other extensions ...
]

Enforcing Docstring Style with Ruff

Want to make sure your team sticks to a consistent docstring style? Ruff, a Python linter, has your back. Here’s how to configure it in your pyproject.toml:

[tool.ruff]
# Enable specific rules for docstring checking
select = [
    "D",    # pydocstyle
    "PD",   # pandas-vet
]

# Choose your docstring style
[tool.ruff.pydocstyle]
convention = "google"  # or "numpy" or "pep257"

# Additional docstring rules you might want
[tool.ruff.per-file-ignores]
# Ignore missing docstrings in tests
"tests/*" = ["D"]
# Ignore missing docstrings in type stub files
"*.pyi" = ["D"]

With this configuration, Ruff will:

  1. Check that all your functions and classes have docstrings (D1 rules)
  2. Verify docstring formatting matches your chosen style (D2 and D4 rules)
  3. Ensure proper whitespace and quotes (D3 rules)

You can run Ruff from the command line:

ruff check .  # to check for issues
ruff format . # to automatically format your code

Or better yet, integrate it with your IDE and CI/CD pipeline to catch docstring issues before they make it to your codebase.

If you’re transitioning an existing project to a new docstring style, you can use Ruff’s --fix option to automatically fix some common issues:

ruff check --fix .

The Bottom Line

Good docstrings aren’t just documentation - they’re a form of respect for the people who’ll use your code. Whether you choose reST, Google, or NumPy style, the key is to be clear, be consistent, and most importantly, be helpful.

Remember: the best code in the world is useless if nobody knows how to use it. Your docstrings are the bridge between your brilliant implementation and the people who need to use it. Make that bridge strong and welcoming.

Subscribe to the Newsletter

Get the latest posts and insights delivered straight to your inbox.