Python Comments and Docstrings Best Practices

Comments are half of your code. They are so essential that without them, code can become incomprehensible and therefore make a project impossible to maintain. You can explain design decisions, complex algorithms, and highlight specific cases that are very important in development. Moreover, using docstrings, you can even auto-generate project documentation.

In this article, I would like to explain the best practices for writing comments and docstrings in Python, as well as the most basic conventions that would be highly recommended to follow.

Best Practices

Be clear and concise in your comments

Explain the purpose of the code, don't describe what is already obvious.

# 👍 Good
# Calculate 21% tax
tax = price * 0.21

# 👎 Bad
# Multiply price by 0.21
tax = price * 0.21

Explain the "why", not the "what"

# 👍 Good
# Use binary search to improve performance with large datasets
result = binary_search(data, target)

# 👎 Bad
# Call the binary search function
result = binary_search(data, target)

Update comments when you change the code

# 👎 Bad
# Calculate discount (10%)
discount = price * 0.15

Use TODO comments for pending tasks

# 👍 Good
def update_password(input):
    # TODO: crbug.com/192795 - Check against common password dictionary
    # TODO: Add minimum length validation
# 👍 Good
def check_email(input):
    # Regular expression: https://stackoverflow.com/questions/46155/how-can-i-validate-an-email-address-in-javascript
    pattern = re.compile(r'^(([^<>()[\]\\.,;:\s@"]+(...')
    return bool(pattern.match(input))

Document complex code and algorithms

# 👍 Good
# We use a weighted binary search to find the position of i
# in the array. We extrapolate the position based on the largest number
# in the array and the array size, then do binary search to
# get the exact number.
if i & (i-1) == 0:  # True if i is 0 or a power of 2
    binary_search_weighted(array, i)

Avoid obvious or redundant comments

# 👎 Bad - unnecessary comment
x = x + 1  # Increment x by 1

# 👍 Good - no comment because it's obvious
x = x + 1

Use comments to explain design decisions

# 👍 Good
# We use a dictionary instead of a list because
# we expect to expand the structure in the future
user_cache = {}

Comment exceptions and edge cases

# 👍 Good
def divide(a, b):
    if b == 0:
        # Avoid division by zero by returning None instead of raising exception
        # to maintain backward compatibility
        return None
    return a / b

Comment spacing

Comments should start at least 2 spaces from the code:

# 👍 Good
if i & (i-1) == 0:  # True if i is 0 or a power of 2

# 👎 Bad
if i & (i-1) == 0:# No spaces before comment

Format

Single line

# Can go above
x = 5  # Or at the end of the line

Multi-line

# This is a comment
# with multiple lines
# to explain something complex

"""
You can also use triple quotes
but it's better to reserve them for
docstrings
"""

Docstrings

To document functions, classes, and modules, use docstrings.

There are 3 docstring formats: Google, NumPy, and Sphinx/reStructuredText. The first is the easiest to read and understand, so it's recommended.

Function docstrings

def create_account(username: str, email: str, age: int) -> bool:
    """Create account with username and email.

    Creates a new user account with the provided credentials and validates
    the user's age for compliance with service requirements.

    Args:
        username: Username for the account. Must be unique and contain
            only alphanumeric characters and underscores.
        email: Email address for the account. Must be a valid email format
            and will be used for account verification.
        age: Age of the user in years. Must be 13 or older to create
            an account.

    Returns:
        True if the account was created successfully, False otherwise.

    Raises:
        ValueError: If username contains invalid characters or if age
            is below the minimum required age.
        EmailError: If the email format is invalid or already exists
            in the system.

    Example:
        >>> create_account("john_doe", "john@example.com", 25)
        True
        >>> create_account("jane_smith", "jane@test.org", 30)
        True
    """
    # Account creation logic would go here
    return True

Generator docstrings

Use Yields instead of Returns for generator functions:

def fibonacci_generator(n: int):
    """Generate the first n Fibonacci numbers.

    Args:
        n: Number of Fibonacci numbers to generate.

    Yields:
        int: The next Fibonacci number in the sequence.

    Example:
        >>> list(fibonacci_generator(5))
        [0, 1, 1, 2, 3]
    """
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

Class docstrings

class DatabaseManager:
    """Manages database connections and operations.

    This class handles all database interactions including connection
    pooling, query execution, and transaction management.

    Attributes:
        connection_string: Database connection string.
        max_connections: Maximum number of concurrent connections allowed.
        timeout: Connection timeout in seconds.

    Example:
        >>> db = DatabaseManager("postgresql://localhost/mydb")
        >>> db.connect()
        >>> results = db.execute_query("SELECT * FROM users")
    """

    def __init__(self, connection_string: str, max_connections: int = 10):
        """Initialize the database manager.

        Args:
            connection_string: Database connection string.
            max_connections: Maximum number of concurrent connections.
        """
        self.connection_string = connection_string
        self.max_connections = max_connections

Module docstrings

"""Database utilities for user management.

This module provides utilities for managing user accounts in the database,
including creation, deletion, authentication, and profile management.

Example usage:
    from user_db import create_user, authenticate_user

    create_user("john_doe", "john@example.com")
    is_valid = authenticate_user("john_doe", "password123")
"""

import hashlib
import sqlite3

Overridden methods with @override

from typing_extensions import override

class EmailService:
    def send_message(self, message: str) -> bool:
        """Send a message via email."""
        # Base implementation
        pass

class SMSService(EmailService):
    @override
    def send_message(self, message: str) -> bool:
        # Doesn't need full docstring if only changing internal behavior
        # but maintains the same contract
        pass

    @override
    def send_message(self, message: str) -> bool:
        """Send a message via SMS.

        Overrides the base email implementation to send via SMS instead.
        Message length is limited to 160 characters.

        Args:
            message: Message to send. Will be truncated if longer than 160 chars.

        Returns:
            True if message was sent successfully.

        Raises:
            ValueError: If message is empty.
        """
        if not message:
            raise ValueError("Message cannot be empty")
        return self._send_sms(message[:160])

Property docstrings

class Circle:
    def __init__(self, radius: float):
        self._radius = radius

    @property
    def radius(self) -> float:
        """The radius of the circle."""
        return self._radius

    @property
    def area(self) -> float:
        """The area of the circle."""
        return 3.14159 * self._radius ** 2

Additional rules to consider

Line limit in docstrings

Docstring summary lines should stay within the 80-character limit.

Don't use trivial docstrings in tests

# 👎 Bad
def test_user_creation():
    """Tests for user creation."""
    pass

# 👍 Good - only if it provides new information
def test_user_creation():
    """Test user creation with edge cases for special characters in usernames."""
    pass

# 👍 Better - without unnecessary docstring
def test_user_creation():
    pass

Consistency in style

Maintain consistency between descriptive and imperative style within the same file:

# Imperative style (recommended)
def fetch_user_data():
    """Fetch user data from the database."""
    pass

def update_user_profile():
    """Update user profile information."""
    pass

# Or descriptive style (but be consistent)
def fetch_user_data():
    """Fetches user data from the database."""
    pass

def update_user_profile():
    """Updates user profile information."""
    pass

Conclusions

Writing good comments speaks very well of a developer. You also create code that will be easy to pass on to other developers or to yourself in the future (memory is fragile). Include patterns, like docstrings, in your daily workflow and the project will benefit enormously.

And don't let laziness win: maintain constancy and consistency.

Bibliography

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Will you buy me a coffee?

You can use the terminal.

ssh customer@andros.dev -p 5555

Written by Andros Fenollosa

August 8, 2025

6 min of reading

You may also like