Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 4 – Functions, scope, closures and decorators

Docstrings, comments and PEP 8 style

Write docstrings that help() and editors show, follow PEP 8 naming and layout, and tell a comment that explains why from one that only repeats the code.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write useful docstrings for modules, functions and classes, with a PEP 257 summary line
  • Check the examples in a docstring with doctest
  • Apply PEP 8 naming and layout conventions
  • Decide when a comment helps and when it hurts
  • Choose between a formatter and a linter for a style problem

Before you start

What you’ll learn · Before you start

What you will learn

  • Write useful docstrings for modules, functions and classes, with a PEP 257 summary line
  • Check the examples in a docstring with doctest
  • Apply PEP 8 naming and layout conventions
  • Decide when a comment helps and when it hurts
  • Choose between a formatter and a linter for a style problem

Before you start

On this page

A teammate types help(monthly_payment) and gets back only the signature, although the author wrote two careful comments inside the function. Comments are for people reading the source, and Python throws them away; what help() shows is the docstring: A string written as the first statement of a module, class or function. Python keeps it as the object's __doc__ attribute, which help(), editors and doctest read., a string written as the first statement of a module, a function or a class, which Python keeps with the object as its __doc__ attribute, so that help(), editors and documentation tools can show it to anyone who uses your code. PEP 8: Python's style guide: the naming, layout and comment conventions most Python code follows, such as snake_case names for functions, four-space indents and lines of at most 79 characters. is Python’s style guide: the conventions most Python code follows, such as four-space indentation, snake_case: Writing a name in lower case with underscores between its words, such as shipping_cost. PEP 8 uses it for functions, variables and parameters. names for functions and variables, CapWords names for classes and lines of at most 79 characters.

Below you can follow a docstring from the source to help(), see what PEP 257 asks of its first line, let doctest: The standard library module that finds examples written after >>> in docstrings, runs them and reports each one whose output differs from the line below it. check its examples, clean up a function to PEP 8, and tell a comment that earns its place from one that only repeats the code.

A docstring is kept with the function; a comment is not

A docstring, as __doc__ and through help() Python · docstring_help.py
def km_to_miles(km):
    """Convert a distance in kilometres to miles."""
    return km / 1.609344


print(round(km_to_miles(42.195), 2))
print(km_to_miles.__doc__)
help(km_to_miles)

Output

26.22
Convert a distance in kilometres to miles.
Help on function km_to_miles in module __main__:

km_to_miles(km)
    Convert a distance in kilometres to miles.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 docstring_help.py

The string under the def line is not just a remark. Python stores it as km_to_miles.__doc__, and help() prints it under the function’s signature, as editors do when you point at the function or start typing a call. A comment in the same place would be invisible to all of them:

A docstring is kept as __doc__ for help(), editors and tools; a comment is dropped when Python reads the file and lives only in the source.Your source file:a docstring and a # commentThe docstring is keptas the function's __doc__help(), editors,doctest and documentation toolsThe comment is droppedwhen Python reads the fileOnly people readingthe source see it

Where a docstring and a comment end up

Text description of the diagram

The diagram starts from a source file that contains a docstring and a comment, and follows each one.

  • The docstring is kept with the function as its `__doc__` attribute. From there, `help()`, editors, `doctest` and documentation tools can read it, so people who use the function see it.
  • The comment is dropped when Python reads the file. Only people who read the source code see it.

So the two have different readers. A docstring tells someone who uses a function what it does and how to call it. A comment tells someone who changes the code something the code cannot say for itself.

help() has one fallback worth knowing. For a function without a docstring, it shows the comment lines directly above the def, which it reads from the source file:

help() falling back to a comment Python · comment_fallback.py
# Convert a distance in kilometres to miles.
def km_to_miles(km):
    return km / 1.609344


print("__doc__ is", km_to_miles.__doc__)
help(km_to_miles)  # no docstring: help() shows the comment above the def

Output

__doc__ is None
Help on function km_to_miles in module __main__:

km_to_miles(km)
    # Convert a distance in kilometres to miles.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 comment_fallback.py

That is as far as a comment gets: __doc__ is still None, so doctest and the other tools that read __doc__ find nothing, and the comment helps only while the source file is at hand. A docstring is stored with the function itself.

Key idea

A docstring is kept with the object, where help(), editors and doctest read it; a comment is dropped when Python reads the file and serves only someone reading the source.

Modules and classes have docstrings too

A module’s docstring is the first statement of its file, and a class’s docstring is the first statement of the class body, written the same way:

A module docstring and a class docstring Python · module_docstring.py
"""Convert temperatures between Celsius and Fahrenheit.

Run this file to print a short table. help() on this module shows the
first line next to the module's name.
"""


class Reading:
    """A temperature reading in degrees Celsius."""


def c_to_f(celsius):
    """Return the Fahrenheit value of a temperature in Celsius."""
    return celsius * 9 / 5 + 32


print(__doc__.splitlines()[0])
print(Reading.__doc__)
print(c_to_f.__doc__)
for c in (0, 37, 100):
    print(f"{c} °C = {c_to_f(c):g} °F")

Output

Convert temperatures between Celsius and Fahrenheit.
A temperature reading in degrees Celsius.
Return the Fahrenheit value of a temperature in Celsius.
0 °C = 32 °F
37 °C = 98.6 °F
100 °C = 212 °F

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 module_docstring.py

Inside a file, the module’s docstring is the global name __doc__. PEP 257 says that modules should normally have a docstring, listing what the module provides, and that a class’s docstring should summarise what the class does. Classes get their own module later in the track; their docstrings follow the rules of this lesson.

A good first line says what the function returns

PEP 257, Python’s docstring conventions, asks for a few simple things:

  • Use triple double quotes, """like this""", even for one line, so that the docstring can grow without changes to its quotes.
  • Make the first line a summary: one phrase that fits on the line and ends with a full stop.
  • Write it as a command, saying what the function does: “Return the area of a rectangle.”, “Convert a distance in kilometres to miles.”. PEP 257 asks for “Return …”, not “Returns …”.
  • Do not repeat the signature: help() shows the parameters already. Do say what comes back, which the signature cannot show.

When a function needs more than one line, write the summary, a blank line, and then the details: what each argument means, what is returned, what is raised and any side effect, such as a file that gets written.

A docstring with a summary and details Python · multiline_docstring.py
def monthly_payment(principal, annual_rate, months):
    """Return the fixed monthly payment that repays a loan.

    principal is the amount borrowed, annual_rate the yearly interest
    rate in per cent and months the number of monthly payments. With a
    rate of 0 the payment is simply principal / months.

    Raises ValueError if months is less than 1.
    """
    if months < 1:
        raise ValueError("months must be 1 or more")
    if annual_rate == 0:
        return round(principal / months, 2)
    r = annual_rate / 100 / 12
    return round(principal * r / (1 - (1 + r) ** -months), 2)


print(monthly_payment(500_000, 8.5, 240))
print(monthly_payment(1200, 0, 12))
print(monthly_payment.__doc__.splitlines()[0])

Output

4339.12
100.0
Return the fixed monthly payment that repays a loan.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 multiline_docstring.py

Step through the parts of that docstring and the places that show each one:

Interactive · step by step The parts of a docstring, and who shows them Step through the docstring of monthly_payment() from multiline_docstring.py. Its parts are on the left, the places that show them on the right.
The parts of a docstring, and who shows them, step 5 of 5. Showing Summary line, A blank line, Details, Lists and search, help() and editors. In focus: help() and editors.Summary lineReturn the fixed monthly …A blank lineends the summaryDetailsarguments, rate 0, ValueErrorLists and searchshow the summary alonehelp() and editorsshow the whole docstring

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. One line, a command. The summary line is one phrase in command form that ends with a full stop and says what the function returns: Return the fixed monthly payment that repays a loan.
  2. Shown on its own. The last line of the program prints only this line, as documentation tools do in their lists and search results, so it has to make sense on its own.
  3. A blank line. A blank line ends the summary, as PEP 257 asks. Everything after it is the longer description.
  4. What the signature cannot say. The details say what a caller cannot see in the signature: what each argument means, that a rate of 0 gives principal / months, and that months below 1 raise ValueError.
  5. help() shows it all. help(monthly_payment) and an editor's tooltip show the signature and then the whole docstring, without the indentation it has in the source.

The last line of the program prints only the summary, the line that tools show in lists and search results. That is why it has to make sense on its own. PEP 257 does not fix a format for the details. Many projects write them in a style that a documentation tool can read, such as the Google and NumPy styles that the Sphinx documentation generator understands; whichever style a project uses, use it everywhere.

A docstring is indented like the code around it, and Python removes that indentation for you:

The indentation is removed from __doc__ Python · doc_indent.py
def f():
    """Summary line.

    Details, indented like the code around them.
    """


print(repr(f.__doc__))

Output

'Summary line.\n\nDetails, indented like the code around them.\n'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 doc_indent.py

Version note

Since Python 3.13 the compiler strips the common indentation of every line of a docstring, so __doc__ holds the text without it, as above. Python 3.12 and earlier keep the spaces in __doc__, and help() removes them when it displays the docstring.

Checkpoint

Check: What Python keeps of a docstring · The summary line and the details

  1. Question 1 of 2 What does print(tax.__doc__) print for this function?

    Read the code, then choose one answer.

    def tax(amount):
        rate = 0.18
        """Return the tax on amount."""
        return amount * rate
    A hint

    Is the string the first statement of the body?

    Show the answer: Question 1 of 2

    Answer: None

    Python takes the docstring from the first statement of the body only. Move the string above rate = 0.18 and tax.__doc__ becomes "Return the tax on amount."

  2. Question 2 of 2 The docstring of monthly_payment() must also say that months below 1 raise ValueError. Where does that sentence go?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In the details, after a blank line below the summary line

    PEP 257 asks for a summary line, a blank line and then the details: what the arguments mean, what is returned, what is raised and any side effect. multiline_docstring.py puts "Raises ValueError if months is less than 1." there.

doctest runs the examples in a docstring

A docstring may show an example as it would look in the interactive shell, with >>> before what you type and the expected result on the next line. The standard library’s doctest module finds such examples, runs them and checks the results:

Two examples in a docstring, checked by doctest Python · doctest_demo.py
import doctest


def word_count(text):
    """Return the number of words in text.

    >>> word_count("Pack my box with five dozen liquor jugs")
    8
    >>> word_count("   ")
    0
    """
    return len(text.split())


# Run the examples in word_count's docstring and report each one.
doctest.run_docstring_examples(word_count, globals(), verbose=True,
                               name="word_count")

Output

Finding tests in word_count
Trying:
    word_count("Pack my box with five dozen liquor jugs")
Expecting:
    8
ok
Trying:
    word_count("   ")
Expecting:
    0
ok

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 doctest_demo.py

Step through what doctest did with the two examples of word_count():

Interactive · stage by stage How doctest checks the two examples of word_count() Step through doctest_demo.py. doctest reads the docstring, runs each example and compares what it printed with the line below it.
How doctest checks the two examples of word_count(), step 7 of 7. Showing Find the >>> lines, Run one example (word_count(" ")), Compare the output (expected 0, got 0), Report the result (ok). In focus: Report the result.next exampleFind the >>> linestwo in word_count()Run one exampleword_count(" ")Compare the outputexpected 0, got 0Report the resultokshows Expected and Got

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Two examples found. run_docstring_examples() reads the docstring of word_count() and finds two examples, each a line that starts with >>>, with the result it should give on the line below.
  2. The first example. It runs word_count("Pack my box with five dozen liquor jugs") as if it had been typed after the >>> of the interactive shell.
  3. 8 and 8. The call gives 8, and the line under the example says 8, so the two match.
  4. ok. With verbose=True, doctest reports each example as Trying, then Expecting, then ok.
  5. The second example. It goes back for the second example, word_count(" "), a string of spaces only.
  6. 0 and 0. split() with no argument finds no words in spaces, so the call gives 0, as the docstring says, and doctest reports ok again.
  7. When they differ. Had a result differed, doctest would print the example, what it expected and what it got instead, so the documentation cannot quietly go wrong.

run_docstring_examples() runs the examples of one function; with verbose=True it reports each one, and both are ok. When code and docstring disagree, doctest prints the example, what it expected and what it got instead, so the documentation cannot quietly go wrong. A whole module is checked with doctest.testmod(), or from the command line with python -m doctest file.py. The module on testing comes back to doctest, unittest and pytest. Press Edit, change the 8 under the first example to 9 and run the program again: doctest now reports that example as failed, with what it expected and what it got.

PEP 8 names tell you what kind of thing a name is

import this prints PEP 20, nineteen short principles that sum up Python’s design:

import this Python · zen.py
import this

Output

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 zen.py

“Readability counts” is the reason for everything in this lesson. PEP 8 starts from the same observation: code is read much more often than it is written. Its conventions make Python code look the same from one project to the next, and its naming rules let a reader tell a function from a class or a constant at a glance:

PEP 8 names, with examples from this lesson
Criterion Functions and variablesClassesConstants
Style snake_caseCapWordsUPPER_CASE
Examples shipping_cost, weight_kgReading, ShoppingCartBASE_FEE, MAX_RETRIES

Functions, variables and parameters use lower case with underscores: shipping_cost, weight_kg. Classes use CapWords: Reading, ShoppingCart. Constants, set once at module level, use capitals with underscores: BASE_FEE, MAX_RETRIES. A leading underscore, as in _cache, marks a name as internal to its module. The lesson on variables has the rest: never l, O or I as a one-letter name, and a trailing underscore, class_, when the name you want is a keyword.

Case Converter Turn a name such as ShippingCost or weightKg into snake_case, CapWords or UPPER_CASE.

Checkpoint

Check: Examples checked by doctest · PEP 8 names

  1. Question 1 of 2 A docstring holds >>> word_count("a b") with 3 on the line below, and word_count() returns the number of words. What does doctest report?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A failed example, with Expected 3 and Got 2

    doctest runs each >>> line as if typed in the shell and compares what it gives with the expected text. A mismatch is reported with the example, Expected and Got, so the documentation cannot quietly drift from the code.

  2. Question 2 of 2 A module sets the delivery fee once, at the top, and never changes it. Which name follows PEP 8?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: DELIVERY_FEE

    PEP 8 writes functions and variables in snake_case, classes in CapWords and module-level constants in UPPER_CASE, so DELIVERY_FEE tells every reader that the value is set once.

A consistent layout lets any Python programmer read your file

PEP 8’s layout rules are just as short:

  • Indent by four spaces per level. Spaces are preferred to tabs, which PEP 8 keeps only for code that is already indented with them.
  • Keep lines to at most 79 characters, and comments and docstrings to 72. PEP 8 lets a team agree on up to 99 characters for code.
  • Break a long line inside brackets, and put the break before an operator, as PEP 8 suggests for new code, so that each line starts with the operator that applies to it.
  • Leave two blank lines around top-level functions and classes, and one between the methods of a class.
  • Put imports at the top of the file, one module per line.
  • Put one space on each side of =, += and the comparisons, and one after each comma, but none just inside brackets, and none around = in a keyword argument or in the default of a parameter without a type hint: round(cost, 2), express=False. (A parameter with a type hint, a topic of a later module, does get spaces: express: bool = False.) Around arithmetic operators PEP 8 leaves the spacing to your judgment.
  • Write one statement per line: no ; between statements, and, as PEP 8 prefers, the body of an if on a line of its own.

Here is one function before and after a clean-up. Both files print the same numbers:

A function before and after a PEP 8 clean-up

Python (before_cleanup.py) · before_cleanup.py

import math,sys
def ShippingCost( weightKg,Distance_km ,express = False ):
  #calculate the cost
  Base=40;perKg=12.5
  cost=Base+perKg*math.ceil(weightKg)+0.8*Distance_km
  if express==True: cost=cost*1.5
  return round(cost,2)
print(ShippingCost(2.3,120),ShippingCost(0.4,15,express = True))

Output

173.5 96.75

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 before_cleanup.py

Python (after_cleanup.py) · after_cleanup.py

import math

BASE_FEE = 40
FEE_PER_KG = 12.5
FEE_PER_KM = 0.8
EXPRESS_FACTOR = 1.5


def shipping_cost(weight_kg, distance_km, express=False):
    """Return the cost of sending a parcel, rounded to two decimals.

    Every started kilogram is charged in full, so 2.3 kg costs as much
    as 3 kg.
    """
    cost = (BASE_FEE + FEE_PER_KG * math.ceil(weight_kg)
            + FEE_PER_KM * distance_km)
    if express:
        cost *= EXPRESS_FACTOR
    return round(cost, 2)


print(shipping_cost(2.3, 120), shipping_cost(0.4, 15, express=True))

Output

173.5 96.75

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 after_cleanup.py

What the clean-up of the shipping function changed
Criterion BeforeAfter
Names ShippingCost, weightKg, Distance_kmshipping_cost, weight_kg, distance_km
Numbers 40, 12.5, 0.8 and 1.5 in the codeBASE_FEE, FEE_PER_KG, FEE_PER_KM, EXPRESS_FACTOR
Indentation Two spacesFour spaces
Statements Two joined with ;, and an if with its bodyOne per line
Imports import math,sys, with sys unusedimport math
The test if express==True:if express:
The calculation One line without spacesWrapped before an operator
Explanation #calculate the costA docstring with the rule for kilograms

Every row is a rule of this lesson, and one more comes from PEP 8’s programming recommendations: it advises against comparing with True and False, so if express == True: became if express:. The comment that repeated the code became a docstring that explains a rule: every started kilogram is charged in full.

A comment should say why, not what

A comment is worth its line when it tells the reader something the code cannot: why a value was chosen, why the obvious way is wrong here, what outside rule the code follows. This short program has four comments:

Four comments: one wrong, one useless, one useful, one a name Python · comments.py
LOAN_DAYS = 21  # books are lent for 14 days


def days_left(days_borrowed):
    # subtract days_borrowed from LOAN_DAYS
    left = LOAN_DAYS - days_borrowed
    # Late books are counted by the fines report, so never show a
    # negative number here.
    return max(left, 0)


d = 7  # days since the book was borrowed
print(days_left(d), days_left(30))

Output

14 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 comments.py

The four comments of comments.py
Criterion What it doesWhat to do
# books are lent for 14 days Contradicts the code, which says 21Fix it, or delete it
# subtract days_borrowed from LOAN_DAYS Repeats the line below itDelete it
# Late books are counted by the fines report … Explains why max() is thereKeep it
# days since the book was borrowed Explains what the name d meansRename d to days_borrowed

The first comment is wrong: someone changed the code and not the comment. PEP 8 puts it plainly: a comment that contradicts the code is worse than no comment at all. The second repeats the line below it; delete such comments, they are one more thing to keep up to date. The third explains why the function hides negative numbers. Nobody could guess that from the code, and someone who removes max() without knowing it would break the report.

The comment on d = 7 explains a name, and a better name, days_borrowed = 7, says the same thing and stays correct wherever the name is used. Write a comment on a line of its own before the code it explains; an inline comment, after code on the same line, needs at least two spaces before the # and should be rare. Code that is no longer needed is deleted, not commented out: version control keeps the old lines.

Key idea

Write a comment to say why; delete one that only says what the line does, fix one that contradicts the code, and replace one that explains a name with a better name.

Checkpoint

Check: PEP 8 layout · Comments that say why

  1. Question 1 of 2 A calculation in new code is too long for one line. Which way of breaking it does PEP 8 suggest?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Inside brackets, with the break before the operator, so the next line starts with +

    after_cleanup.py wraps its cost calculation inside brackets and starts the second line with +, the style PEP 8 suggests for long lines in new code.

  2. Question 2 of 2 Which of these comments is worth keeping?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: # Late books are counted by the fines report, so never show a negative number here.

    A comment earns its place by saying why: a reason, an outside rule, a trap. Comments that repeat the code, explain a name or contradict the code should be deleted, replaced by a better name, or fixed.

Formatters and linters keep the layout for you

Nobody needs to check spaces and line lengths by hand. A formatter: A tool that rewrites the layout of code into one consistent style, such as Black or Ruff's formatter. It changes spaces and line breaks, never what the code does. rewrites a file’s layout into one consistent style: Black, “the uncompromising code formatter”, and Ruff’s formatter, which is designed as a drop-in replacement for Black, both run with one command. A linter: A tool that reads code and reports problems that a formatter does not fix, such as an unused import or a name that breaks the naming conventions. reports what a formatter does not change, such as names that break the conventions or an unused import; Ruff’s linter can also fix some of those problems when it is asked to. The module on testing and code quality sets both up.

A formatter and a linter
Criterion A formatterA linter
What it handles Layout: spaces, line breaks, quotes, indentationProblems: unused imports, names, likely bugs
What it does Rewrites the file in one styleReports each problem, and fixes some when asked
Examples Black, ruff formatruff check
When to choose Mixed tabs and spaces, long lines, inconsistent quotesAn unused import or a name in the wrong style

Tools cannot write a docstring or decide what a comment should say, and PEP 8 itself warns against following it blindly: consistency within one project matters more than consistency with the guide, and a rule that makes a piece of code harder to read is a rule to break there.

Text Diff Paste the two versions of a function to see every change that a clean-up made.

Checkpoint

Check: Formatters and linters

  1. One question One file mixes tabs and spaces and has 120-character lines; another imports sys and never uses it. Which tools handle which?

    Choose one answer.

    Show the answer: One question

    Answer: A formatter fixes the layout of the first; a linter reports the unused import in the second

    Let a formatter, such as Black or ruff format, own the layout, and a linter, such as ruff check, report the rest. Neither writes a docstring or decides what a comment should say.

Practise: tidy a module to PEP 8 and PEP 257

marks.py works out percentages and grades, and it works, but it ignores Python’s style conventions. The exercise asks you to clean it up without changing what any function returns; its sample tests check the names, the docstrings and the layout, and call the renamed functions:

Exercise · Easy · Python

Tidy a module to PEP 8 and PEP 257 without changing it

marks.py works out percentages and grades, and it works, but it ignores Python's style conventions. Clean it up so that the style checks pass, without changing what any function returns.

Rename to the PEP 8 styles: the constant passMark becomes PASS_MARK, and the functions become percentage(), grade_for() and needs_to_pass(), with parameters and local variables in snake_case (obtained, total, required). The checks then look for:

  • a module docstring, and a docstring in every function whose first line is one sentence ending with a full stop;
  • snake_case names for every function, parameter and local variable, and UPPER_CASE for module-level constants;
  • indentation of four spaces per level, no line longer than 79 characters and one statement per line, so no ; and no if with its body on the same line;
  • two blank lines before every function.

The behaviour tests call the renamed functions with the same values as before.

Starter code · marks.py

import math
passMark=40
def Percentage(Obtained,Total):
  return round(Obtained*100/Total,1)
def gradeFor(percent):
  if percent>=75:return "distinction"
  elif percent>=60:return "first class"
  elif percent>=passMark:return "pass"
  else:return "fail"
def needsToPass(Obtained,Total):
  Required=math.ceil(Total*passMark/100);return max(Required-Obtained,0)
The sample tests · test_marks.py
import ast
import re

import marks

SNAKE = re.compile(r"^_?[a-z][a-z0-9_]*$")
UPPER = re.compile(r"^_?[A-Z][A-Z0-9_]*$")


def source_lines():
    with open(marks.__file__, encoding="utf-8") as source:
        return source.read().splitlines()


def tree():
    return ast.parse("\n".join(source_lines()))


def functions():
    return [n for n in ast.walk(tree()) if isinstance(n, ast.FunctionDef)]


def test_behaviour():
    """the renamed functions return what the old ones did"""
    assert marks.PASS_MARK == 40
    assert marks.percentage(45, 60) == 75.0
    assert marks.percentage(33, 120) == 27.5
    assert marks.grade_for(80) == "distinction"
    assert marks.grade_for(75) == "distinction"
    assert marks.grade_for(74.9) == "first class"
    assert marks.grade_for(60) == "first class"
    assert marks.grade_for(40) == "pass"
    assert marks.grade_for(39.9) == "fail"
    assert marks.needs_to_pass(30, 100) == 10
    assert marks.needs_to_pass(20, 75) == 10
    assert marks.needs_to_pass(50, 100) == 0


def test_module_docstring():
    """the module starts with a docstring"""
    assert ast.get_docstring(tree()), "marks.py has no module docstring"


def test_function_docstrings():
    """every function has a docstring whose first line ends with a full stop"""
    for node in functions():
        doc = ast.get_docstring(node)
        assert doc, f"{node.name}() has no docstring"
        first = doc.splitlines()[0].strip()
        assert first.endswith("."), (
            f"the first line of the docstring of {node.name}() "
            "does not end with a full stop")


def test_snake_case_names():
    """functions, parameters and local variables use snake_case"""
    for node in functions():
        assert SNAKE.match(node.name), f"{node.name} is not snake_case"
        for arg in node.args.args:
            assert SNAKE.match(arg.arg), (
                f"the parameter {arg.arg} of {node.name}() is not snake_case")
        for inner in ast.walk(node):
            stored = isinstance(inner, ast.Name) and isinstance(
                inner.ctx, ast.Store)
            if stored:
                assert SNAKE.match(inner.id), (
                    f"the variable {inner.id} in {node.name}() "
                    "is not snake_case")


def test_constants_upper_case():
    """module-level constants use UPPER_CASE"""
    for node in tree().body:
        constant = isinstance(node, ast.Assign) and isinstance(
            node.value, ast.Constant)
        if constant:
            for target in node.targets:
                if isinstance(target, ast.Name):
                    assert UPPER.match(target.id), (
                        f"the constant {target.id} is not UPPER_CASE")


def test_layout():
    """four-space indents, short lines and one statement per line"""
    for number, line in enumerate(source_lines(), start=1):
        assert "\t" not in line, f"line {number} contains a tab"
        assert len(line) <= 79, f"line {number} has {len(line)} characters"
    seen = set()
    for node in ast.walk(tree()):
        if isinstance(node, ast.stmt):
            # Every statement starts at a multiple of four spaces; a
            # continuation line inside brackets may line up as PEP 8 allows.
            assert node.col_offset % 4 == 0, (
                f"line {node.lineno} is indented by {node.col_offset} spaces")
            assert node.lineno not in seen, (
                f"line {node.lineno} holds more than one statement")
            seen.add(node.lineno)


def test_blank_lines():
    """two blank lines come before every function"""
    lines = source_lines()
    for node in tree().body:
        if isinstance(node, ast.FunctionDef):
            before = lines[max(node.lineno - 3, 0):node.lineno - 1]
            assert before == ["", ""], (
                f"{node.name}() at line {node.lineno} needs two blank lines "
                "before it")
A hint

Work through the checks one at a time and run the tests after each change. Re-indent each body with four spaces, move every return that follows an if ...: onto its own line, and split a=1;b=2 into two lines. A docstring such as """Return the percentage of total that obtained is, to one decimal.""" goes on the first line of the function's body.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

To finish, a short round of questions on docstrings, doctest, names, layout, comments and tools. They come one at a time, starting where you are least sure, and every answer explains itself:

Practice that adapts to you

Practise this lesson

14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 Where does Python keep the docstring of a function?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: In the function's __doc__ attribute

    A string written as the first statement of a function, a class or a module is kept as its __doc__ attribute, which help() and editors show.

  2. Question 2 of 14 Which value does price.__doc__ hold?

    Read the code, then choose one answer.

    def price(amount):
        # Add the delivery fee.
        "Return amount plus the delivery fee."
        return amount + 40
    Show the answer: Question 2 of 14

    Answer: 'Return amount plus the delivery fee.'

    The comment is discarded when Python reads the file, so the string is the first statement of the body, which makes it the docstring. Triple quotes are the convention of PEP 257, not a requirement.

  3. Question 3 of 14 Which first line of a docstring follows PEP 257 best?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: """Return the average of the marks, or None if there are none."""

    PEP 257 wants a phrase in command form that ends with a full stop and says what is returned. The others describe instead of command, repeat the signature that help() already shows, or have no full stop.

  4. Question 4 of 14 What does doc_indent.py print?

    What does this program print? Choose one answer.

    def f():
        """Summary line.
    
        Details, indented like the code around them.
        """
    
    
    print(repr(f.__doc__))
    Show the answer: Question 4 of 14

    Answer: it prints

    'Summary line.\n\nDetails, indented like the code around them.\n'

    The details are indented like the code in the source, and since Python 3.13 the compiler strips that common indentation from __doc__. repr() shows the line breaks as \n.

  5. Question 5 of 14 What does doctest compare for each example in a docstring?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: What the >>> line gives when it runs, with the expected text on the line below it

    doctest finds lines that start with >>>, runs them and compares what they give with the line or lines below. A difference is reported as a failed example.

  6. Question 6 of 14 What does doctest_demo.py print?

    What does this program print? Choose one answer.

    import doctest
    
    
    def word_count(text):
        """Return the number of words in text.
    
        >>> word_count("Pack my box with five dozen liquor jugs")
        8
        >>> word_count("   ")
        0
        """
        return len(text.split())
    
    
    # Run the examples in word_count's docstring and report each one.
    doctest.run_docstring_examples(word_count, globals(), verbose=True,
                                   name="word_count")
    Show the answer: Question 6 of 14

    Answer: it prints

    Finding tests in word_count
    Trying:
        word_count("Pack my box with five dozen liquor jugs")
    Expecting:
        8
    ok
    Trying:
        word_count("   ")
    Expecting:
        0
    ok

    doctest finds both >>> examples in the docstring, runs each one and compares what it gives with the line below it. With verbose=True it reports every example, and both match, so both say ok.

  7. Question 7 of 14 Which names follow PEP 8? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 7 of 14

    Answer:

    • ShoppingCart for a class
    • shipping_cost for a function
    • MAX_RETRIES for a module-level constant

    Functions and variables use snake_case, classes CapWords and constants UPPER_CASE. camelCase is common in other languages, but PEP 8 keeps it only for old code that already uses it.

  8. Question 8 of 14 def CalcTotal(itemPrices): is a function. What does PEP 8 make of its line?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: def calc_total(item_prices):

    PEP 8 names functions, variables and parameters in snake_case, so the line becomes def calc_total(item_prices):, as the clean-up of shipping_cost() did.

  9. Question 9 of 14 At most how many characters long should a line of code be, by PEP 8's default rule?

    Type a number.

    Show the answer: Question 9 of 14

    Answer: 79 characters

    PEP 8 limits lines to 79 characters, and comments and docstrings to 72. A team may agree on up to 99 for code.

  10. Question 10 of 14 Which def line follows PEP 8's spacing rules?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: def shipping_cost(weight_kg, distance_km, express=False):

    A space goes after every comma, never just inside brackets, and not around = in a keyword argument or in the default of a parameter without a type hint.

  11. Question 11 of 14 What should a comment tell the reader?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: Something the code cannot say for itself, such as why a value was chosen

    Docstrings are for the users of a function, comments for whoever changes the code. A good comment explains a reason, an outside rule or a trap that nobody could guess from the code.

  12. Question 12 of 14 The code says LOAN_DAYS = 21 # books are lent for 14 days. What is the right fix?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: Correct the comment, or delete it, since the name already says what the value is

    A comment that contradicts the code misleads every reader, which is why PEP 8 calls it worse than none. Whether the code or the comment is right has to be checked, but the value in the code is what the program uses.

  13. Question 13 of 14 What does a formatter such as Black or Ruff's formatter change in a file?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: Only the layout, such as spaces, line breaks and quotes, never what the code does

    A formatter owns the layout and leaves the meaning alone. Names, unused imports and other problems are a linter's business, and docstrings and comments are yours.

  14. Question 14 of 14 After a formatter has run over a file, which of these problems can still be there?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: An import that is never used

    A formatter rewrites spaces, line breaks, indentation and quotes. Whether an import is used is a question about what the code does, which is why a linter checks it.

Interview questions

Fresher · What is the difference between a docstring and a comment in Python?

A docstring is a string literal written as the first statement of a module, class or function. Python stores it in the object’s __doc__ attribute, so help(), editors, documentation generators and doctest can read it while the program runs. It describes how to use the object: what it does, its arguments, what it returns and what it raises. A comment starts with # and is discarded when Python reads the file, so only people reading the source see it. It explains why the code does what it does, such as a business rule or the reason for an unusual choice. In short, a docstring is for the users of a function and a comment for whoever maintains it. PEP 257 sets the conventions for docstrings, and PEP 8 says comments must be kept up to date, because one that contradicts the code is worse than none.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A public function's docstring reads: "This function takes a list and a number and returns stuff. It loops over the list." What would you change so that help(), an editor tooltip and a newcomer all get what they need?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your team inherits a 40,000-line Python codebase with tabs and spaces mixed, camelCase and snake_case names, and 150-character lines. How would you bring it to PEP 8 without breaking anything or flooding code review?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A docstring is the first statement of a module, function or class. Python keeps it as __doc__, and help() and editors show it.
  • Start with a one-line summary in command form that ends with a full stop (“Return …”). Add a blank line and the details: arguments, return value, errors and side effects.
  • doctest runs the >>> examples in docstrings and checks their output.
  • PEP 8: snake_case for functions and variables, CapWords for classes, UPPER_CASE for constants; four-space indents, lines of at most 79 characters, two blank lines around top-level definitions.
  • Comments explain why. Delete comments that repeat the code, fix comments that contradict it, and prefer a better name to a comment that explains a name.
  • Let a formatter such as Black or Ruff handle the layout, and a linter report the rest; keep your attention for names, docstrings and comments.

Terms in this lesson

docstring
A string written as the first statement of a module, class or function. Python keeps it as the object's __doc__ attribute, which help(), editors and doctest read.
doctest
The standard library module that finds examples written after >>> in docstrings, runs them and reports each one whose output differs from the line below it.
formatter
A tool that rewrites the layout of code into one consistent style, such as Black or Ruff's formatter. It changes spaces and line breaks, never what the code does.
linter
A tool that reads code and reports problems that a formatter does not fix, such as an unused import or a name that breaks the naming conventions.
PEP 8
Python's style guide: the naming, layout and comment conventions most Python code follows, such as snake_case names for functions, four-space indents and lines of at most 79 characters.
snake_case
Writing a name in lower case with underscores between its words, such as shipping_cost. PEP 8 uses it for functions, variables and parameters.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.