Comments #
Comments are text in your code that the Python interpreter completely ignores — not executed, not affecting program behavior. They serve one purpose: communicating with the humans who read the code, whether that’s your teammates, open-source contributors, or yourself six months from now. The catch: a sloppily written comment can mislead more than no comment at all. This article covers how to write comments that genuinely help: when to comment, what’s worth explaining, the docstring formats the Python ecosystem uses, and the comment anti-patterns you should avoid.
Single-Line Comments #
A single-line comment starts with #. All text after # until the end of the line is ignored by the interpreter. This is the most common type of comment in everyday code.
# This is a single-line comment
print("Hello, World!")
x = 10 # inline comment — at the end of a code line
y = x * 2 # result: 20
Inline comments (at the end of a line) should be separated from the code by at least two spaces, followed by # and the text. This is the PEP 8 convention.
# ANTI-PATTERN: inline comment too cramped
x=10 #initial value
# CORRECT: two spaces before #, one space after #
x = 10 # initial value
Multi-Line Comments #
Python doesn’t have a multi-line comment syntax like /* ... */ in C or Java. The common approach is stacking several # comments in a row:
# This function computes a student's final grade
# based on the average of the midterm exam (UTS),
# the final exam (UAS), and daily assignment scores.
# Weights: midterm 30%, final 40%, assignments 30%
def compute_final_grade(midterm, final, assignment):
return (midterm * 0.30) + (final * 0.40) + (assignment * 0.30)
Triple-quoted strings ("""...""" or '''...''') are sometimes used as multi-line comments, but technically they’re string literals not assigned to any variable — not real comments. The interpreter still processes the string (creating a string object in memory); it’s just that the result is discarded.
# ANTI-PATTERN: triple-quote as a comment in the middle of a function
def process_data(data):
"""
Processing steps:
1. Validate input
2. Normalize
3. Save to database
""" # ← this is not a comment, this is a discarded string
validate(data)
normalize(data)
save(data)
# CORRECT: use # for non-docstring comments
def process_data(data):
# Processing steps:
# 1. Validate input
# 2. Normalize
# 3. Save to database
validate(data)
normalize(data)
save(data)
Exception: triple-quotes are the right choice for docstrings — the first string inside a function, class, or module. Docstrings are covered in the next section.
Docstrings #
A docstring (documentation string) is a string literal placed as the first statement inside a function, class, method, or module. Unlike regular # comments, docstrings are stored by the interpreter as the __doc__ attribute of that object — meaning they can be accessed programmatically via help() or your IDE.
To better understand the fundamental difference in how the Python interpreter processes regular comments versus docstrings, look at the parser flow below:
flowchart TD
Source["Source Code File (.py)"] --> Parser["Python Parser / Compiler"]
Parser --> Comment["Regular Comment (#)"]
Parser --> Docstring["Docstring (First String Literal)"]
Comment -->|"Ignored & Discarded"| Discard["Not Part of the Bytecode (.pyc)"]
Docstring -->|Compiled to Bytecode| Object["__doc__ Attribute on the Object"]
Object -->|Accessed at Runtime| Runtime["Access via print(obj.__doc__) or help(obj)"]So a regular comment serves purely the readers of the raw code, while a docstring becomes an integral part of the object’s metadata that lives in the runtime memory of your application.
Here’s an example of a basic docstring on a function:
def add(a, b):
"""Returns the sum of two numbers."""
return a + b
# Access the docstring programmatically
print(add.__doc__) # → Returns the sum of two numbers.
help(add) # → shows the full documentation in the terminal
One-Line Docstrings #
Used for simple functions or methods whose purpose is clear from the name. Written on a single line, starting and ending with """ on the same line.
def square(x):
"""Returns x squared."""
return x ** 2
def is_even(n):
"""Returns True if n is an even number."""
return n % 2 == 0
Multi-Line Docstrings #
Used for more complex functions, classes, or modules. The first line is a short summary (one sentence), followed by a blank line, then further details.
def divide(a, b):
"""
Divides two numbers with zero-division handling.
This function differs from the regular / operator because it returns
None instead of raising ZeroDivisionError when the divisor is 0.
Args:
a (float): The number being divided (dividend).
b (float): The number dividing (divisor).
Returns:
float | None: The result of the division, or None if b is 0.
Examples:
>>> divide(10, 2)
5.0
>>> divide(10, 0)
None
"""
if b == 0:
return None
return a / b
Docstring Formats #
There are several popular docstring formats in the Python ecosystem. Pick one and use it consistently across your whole project.
Google Style (Most Common) #
The format used by Google and many modern projects. More concise and visually easier to read.
def transfer_balance(from_account, to_account, amount):
"""
Moves a balance between two bank accounts.
This function is atomic — if either operation fails,
the whole transfer is rolled back and the balance returns
to its original state.
Args:
from_account (str): ID of the sender's account.
to_account (str): ID of the recipient's account.
amount (float): Amount of balance to transfer. Must be positive.
Returns:
bool: True if the transfer succeeded, False if it failed.
Raises:
ValueError: If the amount is not positive.
AccountNotFoundError: If either account doesn't exist.
Examples:
>>> transfer_balance("ACC001", "ACC002", 500000)
True
"""
if amount <= 0:
raise ValueError(f"Transfer amount must be positive, not {amount}")
# implementation ...
NumPy/SciPy Style #
The format used by data science libraries like NumPy, SciPy, and pandas. More verbose but highly structured — a good fit for data science projects.
def compute_statistics(data):
"""
Computes descriptive statistics from a set of numeric data.
Parameters
----------
data : list of float
A set of numeric values. Must not be empty.
Returns
-------
dict
A dictionary with the keys 'mean', 'median', 'std', 'min', 'max'.
Raises
------
ValueError
If data is an empty list.
Examples
--------
>>> compute_statistics([1, 2, 3, 4, 5])
{'mean': 3.0, 'median': 3.0, 'std': 1.58, 'min': 1, 'max': 5}
"""
if not data:
raise ValueError("Data must not be empty")
# implementation ...
Docstrings for Classes #
A class docstring explains the purpose of the class as a whole. Instance attributes are usually documented here or in __init__.
class ShoppingCart:
"""
Represents a shopping cart in an e-commerce system.
The cart stores a list of items along with their quantities and
provides methods to compute the total price, taking discounts
and taxes into account.
Attributes:
items (list): The list of items in the cart.
discount (float): The discount percentage (0.0 to 1.0).
tax (float): The tax percentage applied.
Examples:
>>> cart = ShoppingCart()
>>> cart.add_item("Python Book", 75000, quantity=2)
>>> cart.total()
150000
"""
def __init__(self, discount=0.0, tax=0.11):
"""
Initializes a new shopping cart.
Args:
discount (float): The discount percentage. Default 0.0 (no discount).
tax (float): The tax percentage. Default 0.11 (11% VAT).
"""
self.items = []
self.discount = discount
self.tax = tax
Docstrings for Modules #
Placed at the very top of a Python file, before all imports. Explains the module’s purpose, its contents, and a short usage example.
"""
Utility module for processing and validating user data.
This module provides functions for:
- Validating email, phone number, and national ID formats
- Normalizing names and addresses
- Parsing data from various input formats
Usage example::
from utils.user_validator import validate_email, normalize_name
email = validate_email("[email protected]")
name = normalize_name(" john doe ")
Dependencies:
- re (stdlib)
- phonenumbers >= 8.0
Author: Unis Badri
Created: 2024-01
"""
import re
import phonenumbers
Good Comments vs Bad Comments #
This is the part most often overlooked. Bad comments aren’t just useless — stale or misleading comments actively damage code readability.
# ANTI-PATTERN: a comment that only repeats what's obvious from the code
x = x + 1 # add 1 to x
name = name.strip() # remove whitespace from name
result = [] # create an empty list
# CORRECT: a comment that explains WHY, not WHAT
x = x + 1 # offset because the API index starts at 1, not 0
name = name.strip() # HTML form input sometimes contains invisible whitespace
result = [] # this list is filled lazily, only if condition X holds
# ANTI-PATTERN: a lying comment (worse than no comment at all)
# Computes the average score
def compute_total(score_list): # ← function name doesn't match the comment
return sum(score_list)
# CORRECT: comments and code must always stay in sync
# Sums up all the values in the list
def compute_total(score_list):
return sum(score_list)
# ANTI-PATTERN: commented-out code left behind
def process_payment(order):
# old_validation(order) # ← why is this commented? still used?
# send_notif_v1(order) # ← is this still relevant?
new_validation(order)
send_notif_v2(order)
# CORRECT: delete dead code. Use git to look at history if needed.
def process_payment(order):
new_validation(order)
send_notif_v2(order)
The most dangerous comments are stale ones — the code changed but the comment wasn’t updated. When someone reads a comment that contradicts the code, they have to guess which one is right. If you change code logic, always check whether the surrounding comments are still accurate.
Special Marker Comments (TODO / FIXME / NOTE) #
A widely recognized convention in the Python community, supported by many IDEs, is using specific tags in comments to mark unfinished work:
# TODO: add validation for None input
def compute_discount(price, percent):
return price * (1 - percent / 100)
# FIXME: this function crashes on an empty list — hasn't been fixed yet
def get_first(data):
return data[0]
# NOTE: this API endpoint will be deprecated in v3.0, use /v3/users
def get_user_v2(user_id):
return requests.get(f"/v2/users/{user_id}")
# HACK: temporary workaround for a bug in library X version 2.1.3
# Remove this once the library is upgraded to >= 2.2.0
result = result + 0.000001
Many IDEs like VS Code and PyCharm highlight these tags visually and let you see all of them in one list — so they’re much easier to track than regular comments.
Summary #
#comments for code, docstrings for APIs — use#inside function bodies to explain logic, and docstrings at the start of functions/classes/modules to document the public interface.- Explain WHY, not WHAT — “add 1 to x” is useless; “offset because the API index starts at 1” is genuinely useful.
- Don’t use triple-quotes as multi-line comments in the middle of a function — use several
#lines in a row.- One-line docstrings for simple functions; multi-line docstrings with
Args,Returns,Raisesfor complex ones.- Pick one docstring format and stay consistent — Google Style for general projects, NumPy Style for data science projects.
- Stale comments are more dangerous than no comments — always update comments when you change code logic.
- Delete commented-out code — use git history to see old code, not piles of ambiguous
#lines.- Use TODO/FIXME/NOTE tags to mark unfinished work — IDE-supported and easier to track than regular comments.