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

One of Python's design rules is "readability counts". Python even has an official style guide, PEP 8, and almost every Python project follows it. If you learn these conventions early, your code will look like the code experienced Python programmers write (people call this Pythonic code).

This is a short lesson, but it matters. Ignoring these conventions is one of the most common reasons code looks like a beginner wrote it.


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

Beginners often write comments that say what the code does:

python
# BAD: the comment only repeats what the code says
x = x + 1                       # increment x by 1

Experienced programmers write comments that say why the code does what it does:

python
# GOOD: the comment explains why, which the code cannot show
x = x + 1                       # skip the header row before reading data

If a comment only repeats what the code says, delete it. Good names should explain the code instead.


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 while the program runs, and you can read it 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 Will Use Most

PEP 8 is long. These are the rules you will need most often:

Indentation: 4 spaces, never tabs

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

Lines under ~80 characters

If a line is getting long, break it:

python
# OK: this line fits comfortably
total = item_price * quantity + tax + shipping

# Too long, so 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"            # a leading underscore means "internal, don't use"

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 to separate code

  • 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 clear convention for imports: put them 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, but 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 all of PEP 8. There are tools that check it for you.

ToolWhat it does
blackFormats your code automatically. Fast, with almost no settings to choose. Very widely used.
ruffLinter (and now also a formatter). Reports style problems and suggests fixes. Very 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 won't need to format code by hand.


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

Some of the best-known lines:

  • 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.

Python programmers really do use these ideas when they 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 used only 1 space per level)

  • Docstring explains intent

  • Spaces around operators and after commas

  • Redundant # multiply comment removed

  • Redundant else removed (the early return already ends the function)

  • Temporary z removed: the function returns the expression directly

Each change is small, but together they make the code much easier to read.


8. Mistakes You'll Hit

1. Mixing tabs and spaces
Python 3 stops with an error if tabs and spaces are mixed inconsistently, so your script won't run. Configure your editor to insert spaces when you press Tab.

2. Naming variables in CamelCase
userName = "alice" works, but style checkers will warn about it. Use user_name. CamelCase is for class names only.

3. Skipping docstrings on public functions
If someone else (or you, months later) 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
really 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 only ONE comment, the one that says why discount comes before GST
    ...

print(round(order_total(100, 3), 2))   # 354.0  (below the threshold, so only GST is added)
print(round(order_total(100, 8), 2))   # 849.6  (the discount first, then GST)
Hint 1: A comment that repeats the code makes the code worse # 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 explain 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 explains the code, and it can never get out of date.
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

Five 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 the one question the code cannot answer: why this order.


What You Learned

  • Comments explain why. They should not just repeat what the code does.
  • 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, and this one teaches you how to do it the way everyone else does. That helps you read other people's code, and it helps other people read yours.

Next: Input & Output.


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 did, as well as whether a test passed.

Found this useful? Share it with a friend who’s learning Python.