Skip to main content

Python Docstrings and Type Hints: Complete Guide

Professional Python code requires more than logic—it requires clarity. Docstrings and type hints are the two cornerstones of writing self-documenting functions that your team (and your future self) can understand instantly. Docstrings explain what and why, while type hints explain what type of data flows through your code. Together, they enable IDE autocomplete, catch bugs before runtime, and make your code 40% easier to maintain (according to a 2025 developer survey). This guide shows you how to combine both for production-grade Python.

Writing code that works is half the battle. Making code that others can understand six months from now is the other half.

Key Takeaways

  • Docstrings are runtime-accessible documentation strings, different from comments, accessed via function.__doc__
  • Google-style docstrings provide a standard, readable format with Args, Returns, and Examples sections
  • Type hints annotate parameter and return value types, enabling static checkers like mypy to catch errors before execution
  • Combine docstrings and type hints: let the signature specify types, let the docstring explain purpose
  • The typing module provides List, Dict, Optional, Union, and other advanced type constructs

Prerequisites

This article builds on earlier concepts. You should understand:

  • Defining functions with parameters and return values
  • Python's basic data types: str, int, float, list, dict, bool
  • The concept of scope and how variables are passed to functions

What Are Docstrings and How Do They Differ from Comments?

A docstring (documentation string) is a string literal placed as the first statement in a module, function, class, or method. Unlike regular comments, docstrings are attached to Python objects at runtime and can be programmatically accessed.

Comments vs. Docstrings:

  • Comments (using #) are for developers reading source code; the interpreter ignores them
  • Docstrings (using """...""") are for code users; they're stored as the __doc__ attribute

This distinction matters: tools like IDEs, Sphinx, and documentation generators automatically extract docstrings to create help text and API docs.

def my_function():
"""This is a docstring. It explains the function's purpose."""
# This is a comment. It's for implementation notes.
pass

print(my_function.__doc__)
# Output: This is a docstring. It explains the function's purpose.

When you hover over my_function in VS Code or call help(my_function) in the Python shell, you see the docstring—never the comments.

How Do You Write Professional Docstrings?

The most widely adopted standard in the Python community is Google Style, which balances readability with structure. It's used by Google, Kubernetes, and thousands of open-source projects.

Single-Line Docstrings

For trivial functions, a one-line summary suffices. Always end with a period and keep it under 79 characters:

def add(a, b):
"""Adds two numbers together."""
return a + b

Multi-Line Docstrings (Google Style)

For anything more complex, use sections to organize information clearly:

def calculate_sale_price(original_price, discount_percent, is_member):
"""Calculates the final price after applying a discount.

A special additional discount is applied for members.

Args:
original_price: The starting price of the item.
discount_percent: The standard discount percentage (e.g., 20.0 for 20%).
is_member: True if the customer is a loyalty member.

Returns:
The final calculated price after all discounts.

Example:
>>> calculate_sale_price(100.0, 20.0, False)
80.0
>>> calculate_sale_price(100.0, 20.0, True)
75.0
"""
discount = original_price * (discount_percent / 100)
if is_member:
discount += original_price * 0.05 # Extra 5% for members
return original_price - discount

Google Style sections:

  • Summary line: One sentence describing the function's purpose
  • Extended description (optional): Additional details about behavior or side effects
  • Args: Each parameter name and its description
  • Returns: Description of the return value
  • Raises (optional): Exceptions the function may raise
  • Example (optional): A runnable usage example

This format is so standard that tools automatically parse it to generate API documentation.

What Are Type Hints and Why Should You Use Them?

Python is dynamically typed, meaning you don't declare types—they're inferred at runtime. This flexibility comes at a cost: type errors only appear when the wrong type is used, sometimes deep in execution. Type hints (annotations) specify expected types without enforcing them, enabling static analysis tools to catch errors before you run the code.

Type hints were introduced in Python 3.5 (PEP 484). They have zero runtime cost—Python ignores them at execution—but immense value for tooling.

def add(a: int, b: int) -> int:
"""Adds two numbers together."""
return a + b

add(5, 10) # Correct: int + int → int
add("a", "b") # Type checker warns: str is not int

A type checker like mypy analyzes this and warns about the second call without running it. Your IDE also uses hints to provide autocomplete and inline error squiggles.

Type Hint Syntax

For parameters: parameter_name: type

For return values: -> type

def greet(name: str) -> str:
"""Returns a greeting message."""
return f"Hello, {name}!"

How Do You Use Advanced Types from the typing Module?

For complex types beyond int, str, and bool, the typing module provides generic types. These are especially valuable for collections and optional values.

List, Dict, Set, Tuple

Specify the types of items in collections:

from typing import List, Dict

def process_names(names: List[str]) -> Dict[str, int]:
"""Returns a dict mapping each name to its character count."""
return {name: len(name) for name in names}

This tells developers and type checkers: expect a list of strings; return a dict with string keys and integer values.

Optional for Nullable Values

If a value can be None, use Optional[T] (equivalent to Union[T, None]):

from typing import Optional

def get_user_age(user_id: int) -> Optional[int]:
"""Returns the age if found, None otherwise."""
# Function logic here
return None # Valid because return type is Optional[int]

Union for Multiple Types

When a value can be one of several types:

from typing import Union

def parse_value(data: Union[str, int]) -> str:
"""Converts a string or int to string."""
return str(data)

Complete Example: Combining Docstrings and Type Hints

from typing import Optional, List

def create_greeting(name: str, age: Optional[int] = None) -> str:
"""Creates a personalized greeting message.

If age is provided, includes it in the greeting. Otherwise, omits it.

Args:
name: The person's name.
age: Optional age. Defaults to None.

Returns:
A personalized greeting string.

Example:
>>> create_greeting("Alice", 30)
'Hello Alice, you are 30 years old!'
>>> create_greeting("Bob")
'Hello Bob!'
"""
if age is not None:
return f"Hello {name}, you are {age} years old!"
return f"Hello {name}!"

Note: The type hints in the signature are the source of truth for types. The docstring's Args section focuses on what each parameter means, not what type it is—the signature already states that.

Frequently Asked Questions

Do type hints slow down my code?

No. Type hints are stripped out before execution and have zero runtime overhead. The only "cost" is the time spent writing them—which pays dividends in reduced bugs and better IDE support.

Should I use from typing import *?

No. Import only what you need: from typing import List, Optional. This keeps your namespace clean and makes it clear which types are in use.

What does Any mean?

Any is a type that disables all type checking for that variable. Use it sparingly—only when you truly cannot specify a type. Tools recommend avoiding it in public APIs.

from typing import Any

def debug_print(value: Any) -> None:
"""Accepts any type; useful for debugging."""
print(value)

Does every function need type hints?

Not strictly. For internal helper functions, hints may be overkill. But for any public API or function others will call, hints are essential. Start with public functions and type hints on complex parameters.

How do I run a static type checker?

Install mypy:

pip install mypy

Then check your code:

mypy my_script.py

It reports all type inconsistencies without running the code.

Further Reading


Challenge: Refactor the create_greeting function from this article. Add another parameter titles: Optional[List[str]] = None (a list of titles like ["Dr.", "Prof."]), and if provided, prepend the first title to the name in the greeting. Update both the docstring (Google style) and all type hints.

Next article: Our exploration of functions culminates with lambda functions, which allow you to create small, anonymous functions inline. We'll see how they integrate with higher-order functions like map() and filter().

Happy documenting!