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:
- Start with a punchy summary line that uses the imperative mood (“Calculate the sum” rather than “Calculates the sum”)
- Add a blank line (trust me, this matters for parsing)
- If needed, follow with a more detailed explanation
- List out your parameters, return values, and any exceptions that might pop up
- 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:
- Matches your project’s ecosystem (NumPy style for scientific libraries, etc.)
- You and your team can read comfortably
- 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:
- Check that all your functions and classes have docstrings (
D1
rules) - Verify docstring formatting matches your chosen style (
D2
andD4
rules) - 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.