PythonMastery
beginner 10 min read · lesson 5 of 19 in Python Fundamentals

Comments, Docstrings, and the Style That Makes Python Readable

1 · The lesson

read

Python's tagline is "readability counts". The community even has an official style guide — PEP 8 — that almost every Python codebase follows. Learning the conventions early makes your code feel "Pythonic" instead of "Python-shaped".

This is the shortest lesson in the path, but skipping it is one of the most common reasons beginner code looks beginner.


1. Comments — the WHY, Not the WHAT

A comment starts with #. Everything after it on that line is ignored by Python.

python
# This is a comment
x = 5                           # this is an inline comment

# Block comments can span
# multiple lines like this

The rookie habit is to comment what the code does:

python
# BAD — the code already tells us this
x = x + 1                       # increment x by 1

The pro habit is to comment why the code does what it does:

python
# GOOD — explains intent that isn't visible from the code
x = x + 1                       # skip the header row before reading data

If a comment only restates the code, delete it. Names should do the work.


2. Docstrings — Comments That Tools Can Read

A docstring is a string literal placed as the first statement of a module, function, class, or method. Python keeps it accessible at runtime as obj.__doc__. Tools like help(), IDEs, and documentation generators all read them.

python
def calculate_tip(amount, percent=15):
    """Calculate the tip on a bill amount.

    Args:
        amount: The bill amount in dollars.
        percent: Tip percentage (default 15).

    Returns:
        The tip as a float.
    """
    return amount * percent / 100

# The docstring is now discoverable:
print(calculate_tip.__doc__)
help(calculate_tip)             # opens the full doc page

Use triple double-quotes """...""" for docstrings (PEP 257 convention). Single-line docstrings sit on one line:

python
def square(x):
    """Return x squared."""
    return x * x

3. PEP 8 — The Style Rules You'll Actually Use

PEP 8 is 30 pages long; here's the 90% you need:

Indentation: 4 spaces, never tabs

python
def greet(name):
    if name:                    # 4 spaces in
        print(f"Hi, {name}")    # 4 more (8 total)

Lines under ~80 characters

If a line is getting long, break it:

python
# OK — fits comfortably
total = item_price * quantity + tax + shipping

# Too long — break it
total = (
    item_price * quantity
    + tax
    + shipping_for(destination)
)
+ setup added so this can run · defines shipping, tax, shipping_for, destination, item_price, quantity
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

shipping = _AutoMock('shipping')
tax = _AutoMock('tax')
def shipping_for(*_a, **_kw):
    print('-> shipping_for() called')
    return _AutoMock('shipping_for()')
destination = _AutoMock('destination')
item_price = _AutoMock('item_price')
quantity = _AutoMock('quantity')

Naming conventions

python
my_variable = 1                 # snake_case for variables and functions
def calculate_total():          # snake_case for functions too
    pass

class UserAccount:              # PascalCase for classes
    pass

MAX_RETRIES = 5                 # SCREAMING_SNAKE for constants

_private_thing = "x"            # leading underscore = "internal, don't touch"

Spaces around operators and after commas

python
# GOOD
x = 1 + 2
fn(a, b, c)

# UGLY
x=1+2
fn(a,b,c)
+ setup added so this can run · defines fn, a, b, c
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def fn(*_a, **_kw):
    print('-> fn() called')
    return _AutoMock('fn()')
a = _AutoMock('a')
b = _AutoMock('b')
c = _AutoMock('c')

Blank lines for breathing room

  • 2 blank lines between top-level functions/classes
  • 1 blank line between methods inside a class
  • 1 blank line inside functions to separate logical chunks

4. Imports — Where and How

PEP 8 has a strong convention for imports: at the top of the file, in three groups separated by a blank line.

python
# 1. Standard library
import os
import sys
from pathlib import Path

# 2. Third-party packages
import requests
import pandas as pd

# 3. Your own modules
from myapp.config import settings
from myapp.utils import helpers

One import per line for import x; from x import a, b, c is fine on one line.

python
# GOOD
import os
import sys

# BAD (works, but not Pythonic)
import os, sys

5. The Tools That Enforce All This

You don't have to remember PEP 8 — there are tools that do it for you.

ToolWhat it does
blackAuto-formats your code. Opinionated, fast, no config. The current default.
ruffLinter (and now formatter) — flags violations and offers fixes. Extremely fast.
flake8Older linter, still common.
isortSorts your imports into the three PEP 8 groups.
mypyStatic type checker (relevant later, when you get to type hints).

Most editors run these on save. Set up black and ruff once and you'll never think about formatting again.


6. The Zen of Python

Python has a built-in poem about its philosophy. Type import this in any Python REPL to see it:

python
import this

A few greatest hits:

  • Beautiful is better than ugly.
  • Explicit is better than implicit.
  • Simple is better than complex.
  • Readability counts.
  • There should be one — and preferably only one — obvious way to do it.
  • If the implementation is hard to explain, it's a bad idea.

These aren't just slogans — they actually shape how Pythonistas review each other's code.


7. Before-and-After: Real-Code Cleanup

Before:

python
def f(x,y):
 if x>0 and y>0:
  z=x*y       # multiply
  return z
 else:
  return 0

After:

python
def area_of_rectangle(width, height):
    """Return the area, or 0 if either side is non-positive."""
    if width > 0 and height > 0:
        return width * height
    return 0

What changed:


  • Function name says what it does

  • 4-space indent (the original was 1-space inconsistent)

  • Docstring explains intent

  • Spaces around operators and after commas

  • Redundant # multiply comment removed

  • Redundant else removed (early return reads cleaner)

  • Temporary z removed — just return the expression

That's the cumulative effect of style.


8. Mistakes You'll Hit

1. Mixing tabs and spaces
Python 3 forbids it outright — your script won't even run. Configure your editor to insert spaces when you press Tab.

2. Naming variables in CamelCase
userName = "alice" will work but flag a style warning. Use user_name. CamelCase is for class names only.

3. Skipping docstrings on public functions
If someone else (including future-you) might call this function, give it a one-line docstring at minimum.


🎯 Your Turn — Fix the Comments, Not the Code

The function below works correctly. Every comment in it is useless — each one
describes what the line does, which the line already says. What a reader
actually needs to know is why the rules are what they are.

Rewrite it: delete the noise comments, add a docstring, give the variables real
names, and leave exactly one comment explaining the non-obvious rule.

python
def calc(x, y):
    # multiply x by y
    t = x * y
    # if t is more than 500 then discount
    if t > 500:
        # multiply by 0.9
        t = t * 0.9
    # add 18 percent
    t = t * 1.18
    # return t
    return t

The non-obvious part: orders over ₹500 get a 10% discount, and GST of 18% is
applied after the discount — not before. That ordering is a tax rule, not a
programming decision, and it is the one thing a future reader cannot infer.

Skeleton:

python
def order_total(unit_price, quantity):
    # TODO 1: write a docstring saying what it returns
    # TODO 2: name the running amount something meaningful
    # TODO 3: keep ONE comment — the one explaining discount-before-GST
    ...

print(round(order_total(100, 3), 2))   # 354.0  -> below threshold, GST only
print(round(order_total(100, 8), 2))   # 849.6  -> discount, then GST
Hint 1 — A comment that repeats the code has negative value # multiply by 0.9 tells you nothing that t * 0.9 did not. Worse, if the rate changes to 0.85 and someone forgets the comment, it now lies. The only comments worth keeping are the ones the code cannot express.
Hint 2 — Names carry more than comments do t forces the reader to hold "t is the running total" in their head. total does not. Renaming is usually a better fix than commenting — a good name is a comment that can never go stale.
Show full solution
python
GST_RATE = 0.18
BULK_DISCOUNT_RATE = 0.10
BULK_DISCOUNT_THRESHOLD = 500


def order_total(unit_price, quantity):
    """Return the payable total for an order, including GST.

    Orders above the bulk threshold receive a discount before tax.
    """
    total = unit_price * quantity

    if total > BULK_DISCOUNT_THRESHOLD:
        total = total * (1 - BULK_DISCOUNT_RATE)

    # GST applies to the discounted amount, not the list price — taxing the
    # pre-discount total would overcharge the customer.
    return total * (1 + GST_RATE)


print(round(order_total(100, 3), 2))   # 354.0
print(round(order_total(100, 8), 2))   # 849.6

Six comments became one. The rates moved into named constants, so the numbers
explain themselves and there is a single place to change them. The one surviving
comment answers a question the code genuinely cannot: why this order.


Recap

  • Comments explain why — never just restate what.
  • Docstrings (triple-quoted, first statement) are machine-readable.
  • PEP 8 is the style guide everyone follows. Highlights: 4-space indent, snake_case for variables and functions, PascalCase for classes, SCREAMING_SNAKE for constants.
  • Tools like black, ruff, and isort enforce style automatically.
  • import this for the philosophy.

This lesson is a discipline lesson. Other lessons teach you what Python can do; this one teaches you how to do it the way everyone else does. That matters when reading other people's code — and when other people read yours.


Source: adapted from PEP 8 and PEP 257. PSF License.

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code actually did, not just whether a test passed.