Python Comments & Docstrings
Write code that explains itself
A complete, runnable reference for Python's two documentation mechanisms: # comments (ignored by the interpreter) and """docstrings""" (stored in __doc__ and retrievable at runtime). Every code block includes a numbered line-by-line breakdown, plus PEP 257 conventions, three docstring styles, and practice exercises.
"""Sales report utilities. Usage: from reports import summarize print(summarize([100, 250, 75])) """ A module-level constant — the comment explains why, not what DEFAULT_CURRENCY = "USD" fallback when no country is set def summarize(amounts: list[float]) -> dict: """Return a summary of the given amounts. Args: amounts: A non-empty list of numeric values. Returns: A dict with keys ``count``, ``total``, and ``average``. """ total = sum(amounts) return { "count": len(amounts), "total": total, "average": total / len(amounts), } print(summarize([100, 250, 75]))
📚 What This Lesson Covers
Eleven sections, each with runnable code and a numbered explanation of every line
🎯 Why Comments and Docstrings Matter
Code is read far more often than it is written — comments and docstrings are how you make that reading cheap
Comments and docstrings solve two different problems. A comment explains why a particular piece of code exists — the decision, workaround, or constraint that isn't obvious from the code itself. A docstring explains what a module, class, or function does — its public contract, retrievable by IDEs, Sphinx, and help(). Mixing these up is one of the most common documentation mistakes in real codebases.
The split, in one line each
Comment: "Why is this retry set to 3? Because the payment API times out roughly 1 in 10 requests."
Docstring: "Retry the given callable up to N times with exponential backoff."
📖 Docstrings — Strings That Document Code
A docstring is a string literal that becomes the first statement of a module, class, or function
🔍 How a Docstring Works
When a triple-quoted string is the very first statement after a def, class, or at the top of a file, Python stores it in the object's __doc__ attribute. That makes it retrievable at runtime — the key difference from a comment.
def add(a: int, b: int) -> int: """Return the sum of two integers.""" return a + b print(add.__doc__) print(add(2, 3)) def add_no_doc(a, b): This is a comment, not a docstring. return a + b print(add_no_doc.__doc__)
- 1Function definition with type hints. The body follows on subsequent indented lines.
- 2The
"""…"""on the first line of the body becomes the function's docstring. Python stores it inadd.__doc__. - 3The function returns the sum of the two arguments.
- 5Prints the docstring — the string is retrievable at runtime. Output:
Return the sum of two integers. - 6Calls the function — output
5. - 8–10A function with a
#comment instead of a docstring. The comment is ignored entirely; the function has no docstring. - 12Prints
None—add_no_doc.__doc__isNonebecause no docstring exists.
help(), IDE tooltips, Sphinx-generated docs, and doctest.📄 Function, Class, and Module Docstrings
Docstrings can appear in three places. A module docstring goes at the very top of a .py file; a class docstring goes right after the class line; a function docstring goes right after the def line.
"""geometry.py — small 2D helpers. This module-level docstring describes the file. """ class Circle: """A circle with a given radius.""" def __init__(self, radius: float) -> None: """Initialise a circle. Args: radius: Must be non-negative. """ if radius < 0: raise ValueError("radius must be non-negative") self.radius = radius def area(self) -> float: """Return the area of the circle (πr²).""" import math return math.pi * self.radius ** 2 c = Circle(2) print(f"area ≈ {c.area():.2f}")
- 1–4The module docstring. Because it's the first statement in the file, Python stores it in
__doc__at module level —geometry.__doc__returns this text. - 6Class definition — no docstring yet, but a docstring will go on the next line.
- 7Class docstring. It describes the class's purpose. Stored in
Circle.__doc__. - 9The
__init__method takes aradiusargument. Its own docstring follows on the next line. - 10–15
__init__'s docstring — a short summary plus a parameter description. The blank line and indentation follow PEP 257 conventions. - 16–18Validation logic. The docstring talks about why the argument must be non-negative — the exception message confirms it, but the docstring makes it discoverable without reading the code.
- 20–23The
areamethod with its own docstring. Notice the formula πr² is documented in plain text — that's a helpful hint, not code repetition. - 25–26Creates a
Circle(2)and prints its area to two decimal places. Expected output:area ≈ 12.57.
♻️ One-Line vs Multi-Line Docstrings
For trivially simple functions, a one-line docstring — ending with a period — is enough. For anything more complex, use the multi-line form with a summary line, a blank line, then details.
def double(n: int) -> int: """Return n multiplied by 2.""" return n * 2 def clamp(value: float, lo: float, hi: float) -> float: """Restrict value to the closed interval [lo, hi]. If value is below lo, returns lo. If value is above hi, returns hi. Otherwise returns value unchanged. Args: value: The number to clamp. lo: The lower bound (inclusive). hi: The upper bound (inclusive). Returns: The clamped value. """ return max(lo, min(value, hi)) print(double(7)) print(clamp(15, 0, 10)) print(clamp(-3, 0, 10)) print(clamp(5, 0, 10))
- 1–3
doubleis simple enough that its docstring fits on one line, ending with a period. The function body is the return expression. - 5–6
clampuses a multi-line docstring. The first line is a summary — one sentence, ends with a period, appears in generated docs tables. - 7A blank line separates the summary from the rest. PEP 257 requires this for multi-line docstrings.
- 8–10The details section — behaviour described in prose, one condition per line. No bullet markers required, though they're common.
- 12–17An
Args:section listing parameters. This convention (from Google style, discussed later) is what Sphinx and IDE tooltips parse. - 19–20A
Returns:section describing the return value. - 22Implementation —
max(lo, min(value, hi))clamps in a single expression. - 24–27Four test calls confirming the clamp behaves as documented. Expected output:
14,10,0,5.
📜 PEP 257 — The Docstring Convention
The Python community's official style guide for docstrings, adopted in 2001 and still current
📐 The Rules, Condensed
PEP 257 is short — you can read the whole thing in about ten minutes. Here are the rules that matter most in practice:
- ✓Use triple double quotes for all docstrings:
"""Like this.""" - ✓End one-line docstrings with a period, right before the closing quotes
- ✓For multi-line docstrings, the summary on the first line ends with a period, and the closing
"""goes on its own line - ✓Leave a blank line before and after the docstring in a class (but not before the docstring of a function)
- ✓Document the public contract — not implementation details or internal state
- ✓Start with an imperative verb: "Return…", "Compute…", "Raise…", not "Returns…" or "This function computes…"
def mean(values: list[float]) -> float: """Return the arithmetic mean of the given values. Args: values: A non-empty sequence of numeric values. Returns: The arithmetic mean as a float. Raises: ValueError: If ``values`` is empty. """ if not values: raise ValueError("values must not be empty") return sum(values) / len(values) print(mean([2, 4, 6, 8]))
- 1Function signature — the type hints already declare the public contract.
- 2The summary line: imperative verb ("Return"), full sentence, ends with a period. This is what appears in the module index.
- 3Blank line separating summary from details.
- 4–5
Args:section describing the input parameter. The colon aftervaluesis convention — some style guides use a dash instead. - 7–8
Returns:section describing the return value and its type. - 10–11
Raises:section documenting which exception is raised and under what condition. TheValueErroris part of the public contract — callers need to know it can happen. - 12–14Implementation. Note that the docstring says what happens (a ValueError for empty input), and the code makes it happen. The two are kept in sync.
- 16Test call — expected output
5.0.
🎨 Three Docstring Styles in the Wild
Different ecosystems have converged on different formats — pick one per project and be consistent
🅰️ Google Style
The most readable for humans, widely used at Google and in many open-source projects. Uses indented sections with a colon after the name.
def fetch(url: str, timeout: int = 10) -> bytes: """Fetch the given URL and return the response body. Args: url: A fully-qualified HTTP or HTTPS URL. timeout: Seconds to wait before raising. Returns: The response body as raw bytes. Raises: TimeoutError: If the request exceeds ``timeout``. """ import urllib.request with urllib.request.urlopen(url, timeout=timeout) as r: return r.read() print(len(fetch("https://example.com")))
- 1Signature with default parameter value —
timeout=10is documented as an optional override. - 2Google-style summary — imperative, one line, ends with a period.
- 3Blank line separating summary from sections.
- 4–6
Args:section with parameter names and descriptions, indented two spaces. Google style uses a colon after each parameter, not a dash. - 8–9
Returns:section describing the return value type and meaning. - 11–12
Raises:section listing exceptions that callers may encounter. - 14–16Implementation using
urllib.request. Thewithblock ensures the response is closed after reading. - 18Test call — prints the response size in bytes.
🅱️ NumPy / SciPy Style
Widely used in scientific Python (NumPy, SciPy, pandas, scikit-learn). More structured than Google style, with parameter names on their own lines followed by a type and a description.
def correlate(x: list[float], y: list[float]) -> float: """Compute the Pearson correlation coefficient. Parameters ---------- x : list of float First sequence of numeric values. Must have the same length as ``y``. y : list of float Second sequence of numeric values. Returns ------- float The Pearson correlation coefficient in [-1.0, 1.0]. Raises ------ ValueError If ``x`` and ``y`` have different lengths. """ if len(x) != len(y): raise ValueError("x and y must have equal length") n = len(x) mx = sum(x) / n my = sum(y) / n num = sum((a - mx) * (b - my) for a, b in zip(x, y)) den = (sum((a - mx) ** 2 for a in x) * sum((b - my) ** 2 for b in y)) ** 0.5 return num / den print(correlate([1, 2, 3], [2, 4, 6]))
- 1Signature — both parameters are
list[float]. - 2Summary line, imperative, ends with period.
- 4–5
Parameterssection header with a dashed underline (this exact length of dashes matters — Sphinx and IDEs parse it). - 6–9Each parameter on two lines:
name : typefirst, then an indented description. Long descriptions can wrap. - 11–12
Returnssection — the return type on its own line, then an indented description. - 14–16
Raisessection — exception name on its own line, description below. - 18–24Implementation. The Pearson formula is broken into named intermediates for readability rather than one enormous expression.
- 26Test call — expected output
1.0(the two sequences are perfectly correlated).
🅲 reStructuredText / Sphinx Style
The oldest and most "machine-first" style, used with Sphinx. Uses :param name: and :returns: field markers.
def slugify(text: str) -> str: """Convert text to a URL-safe slug. Lowercases the input, replaces whitespace runs with single hyphens, and strips leading/trailing hyphens. :param text: The input string to slugify. :type text: str :returns: The slugified string. :rtype: str """ import re s = text.strip().lower() s = re.sub(r"\s+", "-", s) return s.strip("-") print(slugify(" Hello, Python World! "))
- 1Signature — takes a
str, returns astr. - 2Summary line — imperative, ends with period.
- 4–6Detailed description as plain prose. Sphinx renders this as the body of the docstring.
- 8–9
:param text:and:type text:— the Sphinx field syntax. The:type:line is redundant if type hints are present, but traditional Sphinx projects still include it. - 10–11
:returns:and:rtype:— the return value and its type. - 13–16Implementation.
re.sub(r"\s+", "-", s)replaces runs of whitespace with one hyphen..strip("-")removes hyphens from the ends. - 18Test call — expected output
"hello,-python-world!"because punctuation isn't stripped by this simple version. A real slugify would handle punctuation too.
⚖️ Which Style Should You Choose?
| Style | Readability | Machine-parseable | Common in |
|---|---|---|---|
| ★★★★★ | ★★★★ | Google, many OSS projects | |
| NumPy | ★★★★ | ★★★★★ | NumPy, SciPy, pandas, scikit-learn |
| Sphinx / rST | ★★ | ★★★★★ | Older Python projects, Read the Docs |
napoleon) and by most IDEs, and compact. If you're contributing to the scientific Python ecosystem, match NumPy style instead.👁️ Viewing Docstrings at Runtime
Docstrings aren't static — they're accessible from live objects
🔎 help(), __doc__, and pydoc
Three ways to read a docstring, each useful in a different context.
def greet(name: str) -> str: """Return a friendly greeting. Args: name: The person to greet. Returns: A greeting string of the form "Hello,!". """ return f"Hello, {name}!" print(greet.__doc__) print("---") print(greet.__doc__.splitlines()[0])
- 1Function definition — a simple, well-documented
greet. - 2–9The full docstring. Google style, with
Args:andReturns:sections. - 10Returns a formatted greeting — never called in this snippet, only inspected.
- 12Prints the full docstring — exactly as typed, including newlines and indentation.
- 13A separator line for readability.
- 14Accesses the first line only — the summary.
splitlines()[0]is a common pattern when you want just the short description for an index or table of contents.
help(greet) — it renders the docstring with the function signature above it, plus any Args:/Returns: sections. For a module, python -m pydoc mymodule generates a formatted HTML page.🧪 Doctest — Tests Embedded in Docstrings
Write examples that double as tests, so your documentation never drifts from reality
🧪 Docstring Examples That Are Also Tests
The doctest module scans docstrings for lines starting with >>>, executes them, and compares the output to the lines that follow. If they don't match, the test fails.
def add(a: int, b: int) -> int: """Return the sum of a and b. >>> add(2, 3) 5 >>> add(-1, 1) 0 >>> add(0, 0) 0 """ return a + b import doctest result = doctest.testmod() print(result)
- 1Function definition —
addtakes two integers. - 2Summary line — imperative, ends with a period.
- 3–9Docstring containing three interactive examples. Each
>>>line is Python code thatdoctestwill run; the line immediately after must be the exact expected output. - 10The actual implementation. Notice the docstring examples are now a contract — if the implementation changed to return
a - b, doctest would fail immediately. - 12Imports
doctest. - 13
doctest.testmod()scans the current module's docstrings, runs each>>>block, and reports results. - 14Prints the result — a
TestResultsobject showing how many examples were run and how many failed. Expected output:TestResults(failed=0, attempted=3).
unittest or pytest.⚠️ Doctest Gotcha — Output Must Match Exactly
Whitespace, quotes, and float precision all matter. A common error is writing an expected output that looks correct but doesn't match the actual string byte-for-byte.
def divide(a: float, b: float) -> float: """Divide a by b. >>> divide(10, 3) 3.3333333333333335 >>> round(divide(10, 3), 2) 3.33 """ return a / b
- 1Signature — both parameters are floats.
- 3–4The expected output
3.3333333333333335is the exact float Python computes for10/3. If you write3.3333, the doctest fails. - 5–6Wrap the call in
round()to make the output predictable. This is the recommended pattern for float-returning functions. - 8The implementation.
round() or repr(), or write a comparison like abs(divide(10, 3) - 3.333) < 0.001. Floating-point doctests are a common source of false failures.✅ Comments & Docstrings — Best Practices
The habits that keep code readable six months after you wrote it
🎯 Do
- ✓Use comments to explain why, not what. "We retry here because the payment API is flaky" is useful; "this line retries" is not.
- ✓Use docstrings on every public function and class — these are what IDEs and
help()display. - ✓Follow PEP 257 — triple double quotes, summary on the first line, blank line before details.
- ✓Pick one style per project (Google, NumPy, or Sphinx) and stick to it.
- ✓Document public contracts: parameters, return values, exceptions raised, side effects.
- ✓Update comments and docstrings when you change the code — a stale comment is worse than no comment.
🚫 Don't
- ✗Don't comment obvious code.
x = x + 1 # increment xadds noise without adding value. - ✗Don't use triple-quoted strings as comments — they're executed string literals, not real comments.
- ✗Don't duplicate the function name in the docstring.
def get_user(): """Gets the user."""tells the reader nothing. - ✗Don't leave commented-out code in production. Version control preserves history — deleted code doesn't need to live in comments.
- ✗Don't let docstrings drift. A docstring that no longer matches the code actively misleads.
- ✗Don't put secrets in comments. They end up in commits, logs, and dumps.
🏋️ Practice — Check Your Understanding
Three exercises with solutions. Try each before expanding the answer.
Exercise 1 — Comment or Docstring?
For each scenario, decide whether a comment or a docstring is the better fit.
- Explaining why a retry count is set to 5 instead of 1.
- Describing the parameters a public
fetch()function accepts. - Noting that a particular line works around a known bug in a third-party library.
- Explaining what a module
utils.pyis for.
Show solution
1. Comment — this is the "why," and it belongs near the retry logic, not in a public docstring.
2. Docstring — parameters are part of the function's public contract.
3. Comment — workaround notes belong next to the code they protect, not in the public API docs.
4. Module docstring — a one-paragraph summary at the top of the file.
Exercise 2 — Fix the Docstring
This function has a docstring that violates PEP 257. Rewrite it.
def area_of_rectangle(w, h): '''Computes the area.''' return w * h
Show solution
def area_of_rectangle(w: float, h: float) -> float: """Return the area of a rectangle. Args: w: The rectangle's width. h: The rectangle's height. Returns: The area, computed as w × h. """ return w * h
Three fixes applied: (1) triple double quotes instead of triple single quotes, (2) imperative verb ("Return" not "Computes") with a period, (3) the function name isn't duplicated — the docstring adds information instead of repeating area_of_rectangle.
Exercise 3 — Add a Doctest
Add a doctest to this function that verifies it returns the correct greeting.
def greet(name: str) -> str: """Return a friendly greeting.""" return f"Hello, {name}!"
Show solution
def greet(name: str) -> str: """Return a friendly greeting. >>> greet("Ana") 'Hello, Ana!' >>> greet("Bob") 'Hello, Bob!' """ return f"Hello, {name}!"
The expected output 'Hello, Ana!' must include the quotes — repr() of a string includes them, and doctest compares against repr() by default. Writing Hello, Ana! without quotes would fail.
❓ Frequently Asked Questions
Common questions about Python comments and docstrings, answered directly
Python does not have a true multi-line comment syntax. The idiomatic approach is to prefix each line with #. Some developers use triple-quoted strings as multi-line comments, but those are actually unassigned string literals — they are still evaluated at runtime (cheaply, since they're constants) and discarded. If the string is the first statement in a module, class, or function, Python stores it in __doc__ as a docstring instead. Use # for real comments; reserve triple-quoted strings for docstrings.
A comment (#) is ignored by the interpreter and never stored anywhere. It exists only for human readers. A docstring is a string literal that Python stores as the __doc__ attribute of a module, class, or function, and it is retrievable at runtime via help() or __doc__. Comments explain why code exists; docstrings explain what a public interface does. This is why docstrings power IDE tooltips, Sphinx-generated docs, and doctest — comments power none of these.
PEP 257 recommends triple double quotes ("""...""") for all docstrings. Triple single quotes ('''...''') work but are not the convention. Using one consistent style across a codebase makes it easy for tooling (Sphinx, pydoc, IDE introspection) to parse docstrings reliably — and it avoids strange interactions when the docstring itself contains an apostrophe.
Three ways: (1) use the help() built-in — help(my_function) renders the docstring with the function signature above it; (2) access the __doc__ attribute directly — print(my_function.__doc__) returns the raw string; (3) run python -m pydoc mymodule on the command line to generate a formatted HTML or text page for an entire module. All three read the same underlying attribute.
📋 Quick-Reference Table
Every comment and docstring pattern at a glance — bookmark this page for quick reference
| Construct | Syntax | When to use |
|---|---|---|
| Single-line comment | # text | Explain why a line or block exists |
| Inline comment | x = 10 # note | Short clarifications next to specific code |
| Multi-line comment | # line 1# line 2 | Longer explanations, TODOs, workaround notes |
| Function docstring | """Return the sum.""" | Document what a public function does |
| Class docstring | """A circle with a radius.""" | Describe a class's purpose and invariants |
| Module docstring | Triple-quoted string at top of file | Describe an entire module's contents |
| Multi-line docstring | Summary line + blank line + details | Non-trivial functions, classes, modules |
| Google style | Args: / Returns: / Raises: | Readable docs, general projects |
| NumPy style | Parameters\n---------- | Scientific Python (NumPy, SciPy, pandas) |
| Sphinx / rST style | :param name: / :returns: | Sphinx projects, older codebases |
| Doctest | >>> f(2, 3)\n5 | Short illustrative examples that double as tests |
| View docstring | help(func) / func.__doc__ | Interactive inspection, debugging, learning |
Educational Use Disclaimer
This page is part of Poetic Bytes, a free educational resource for learning Python programming. The code examples are provided for instruction and demonstration. They are not production-audited and should not be used as-is in security-sensitive, financial, or safety-critical systems without independent review. See our full disclaimer and terms of use.
📬 Python, weekly
Short lessons, real code, and ecosystem news — sent about once a week
🗣️ Learner Voices
No comments yet — your voice starts the conversation.
💬 Python Comment Types
Single-line, inline, and multi-line — plus what Python doesn't have
1️⃣ Single-Line and Inline Comments
Any text after a
#on a line is a comment — from that character to the end of the line. It is discarded by the interpreter and never stored anywhere.#to the end of the line is a comment. The line has no effect on the program — it exists only for human readers.x = 10runs. The# An inline comment…part after it is ignored. Inline comments must be separated from code by at least two spaces (PEP 8).10. The comment doesn't appear in output.#inside a comment doesn't nest — the whole line is already a comment."https://example.com"is a string literal, not a comment — the#would be part of the string. The trailing comment is ignored.x = 10 # comment, notx = 10 #comment. Small detail, but it keeps code readable when you're scanning thousands of lines.2️⃣ Multi-Line Comments — The Truth
Python has no dedicated multi-line comment syntax. Two common patterns exist, but only one is idiomatic.
#. This is the correct way to write a multi-line comment in Python.#.__doc__.grepand linters treat the string as code (because it is code), not as documentation. It also creates a small runtime cost. Use#for real multi-line comments. Reserve triple-quoted strings for docstrings, which belong at the top of a module, class, or function.