Embercode
✕
Learning with AILesson 9 of 16

40 min read

Writing a good bug report

Why this matters

A bug report is a short written message that describes a problem in a program so that another person can see the same problem on their own computer. The person who reads it might be a teammate, the author of a library you use, a tutor, or an AI assistant. In every case they can only work with what you wrote.

Compare two messages sent to a teacher:

My average function is broken.

and

average([2, 4, 6]) returns 3.3333333333333335. I expected 4.0. Here is a six-line program that shows it.

The first message cannot be acted on. The reader has to ask five questions before they can even start. The second message can be checked in ten seconds. The reader copies the six lines, runs them, and sees the same wrong number.

Writing a good report is a skill separate from fixing the bug. You can be a strong programmer and still get slow help because your reports are vague. You can also be a beginner and get fast help because your reports are exact. This lesson teaches the parts a report needs and shows three complete examples. It does not teach how to find the bug itself. The method for that is in the lesson on debugging basics in the VS Code module, and the lesson on reading error messages covers how to read the text a crash prints.

What a report is for

A report has one job: to let someone else reproduce the problem. Reproduce means make the problem happen again on demand. If the reader can reproduce it, they can see what the program does. If they can see it, they can test a fix. If they cannot reproduce it, they can only guess.

This changes what you put in. A report is not a story about your afternoon. It is not a list of everything you suspect. It is the smallest set of facts that lets the reader make the same thing happen.

The parts of a report

Most good reports have the same parts, in this order.

PartWhat to write
TitleOne line that names the symptom, such as "average skips the first number"
Minimal exampleThe smallest code that shows the problem, which you ran yourself
StepsThe exact input and the exact command, if there is more than one step
Expected outputWhat you thought the program would print, and why
Actual outputWhat it really printed, copied exactly, including any error text
EnvironmentThe language version and the operating system, if they might matter
What you triedWhat you checked already, and what it showed

You do not need a heading for each part in a short message. The parts are a checklist. Before you send a report, check that each part is present, even if it takes one line.

Worked example 1: a wrong answer

Suppose you wrote a function that should return the average of a list of numbers. It runs without error, but the number is wrong.

The vague version is this:

My average function gives the wrong answer. Please help.

The reader now has no idea what "wrong" means, what input you used, or which function you mean. Here is the same problem written as a minimal example. Save the code as repro.py.

def average(numbers):
    total = 0
    for n in numbers[1:]:
        total += n
    return total / len(numbers)
 
print("average of [2, 4, 6] is", average([2, 4, 6]))

Run it with python3 repro.py. The real output on the machine used for this lesson is:

average of [2, 4, 6] is 3.3333333333333335

The average of 2, 4 and 6 is 4, because 2 plus 4 plus 6 is 12 and 12 divided by 3 is 4. The program printed 3.3333333333333335. That gap is the bug. The report now says exactly that:

average([2, 4, 6]) returns 3.3333333333333335. I expected 4.0, because (2 + 4 + 6) / 3 is 4. Python 3.14.4. The example above is the full program.

Look at what the report does not say. It does not say "I think the loop is wrong." It does not name a line. The reader can see the loop for themselves once the output is shown. Describing the symptom is more useful than guessing the cause, because a wrong guess sends the reader in the wrong direction.

How to shrink an example

The first version of your code is almost never the minimal one. It may have 200 lines, several files and a database call. The minimal example is found by removing things and checking that the bug still happens. The steps are these.

  1. Copy the full program into a new file.
  2. Remove one piece, such as a function you think is not involved.
  3. Run it. If the bug still happens, keep the removal and go on. If it does not, put the piece back.
  4. Repeat until removing anything else makes the bug disappear.

What is left is the minimal example. It is often much smaller than you expected, and the act of shrinking it often shows you the bug. If the shrinking shows the bug, you may not need to send the report at all.

Use simple input. A list of three small numbers is easier to read than a list of 40 random ones. Use the smallest input that still shows the bug.

Worked example 2: a crash

A crash is easier to report because the computer prints an error. The error text is part of the report, and it must be copied exactly. Here is a crash from the same average function, called with an empty list. Save the code as empty.py.

def average(numbers):
    total = 0
    for n in numbers:
        total += n
    return total / len(numbers)
 
print(average([]))

Run it with python3 empty.py. The real output is:

Traceback (most recent call last):
  File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons-06/bug/empty.py", line 7, in <module>
    print(average([]))
          ~~~~~~~^^^^
  File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons-06/bug/empty.py", line 5, in average
    return total / len(numbers)
           ~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero

The folder part of each file path depends on where you saved the file, so your lines will show a different path. The rest of the text is what the report needs. Copy all of it, not only the last line. The last line tells the reader the kind of error, ZeroDivisionError. The lines above it tell them which call failed, and the line numbers tell them where in the file to look.

A report that says "it crashes with a division error" loses the line numbers and the exact name. A report that pastes the full text lets the reader go straight to line 5. The report for this crash looks like this:

average([]) crashes with the traceback below. I expected average to return something for an empty list, or to fail with a message that says the list is empty. Python 3.14.4.

(paste the full traceback here)

Two details matter here. The report says what should happen, so the reader knows the crash is a bug and not a feature. It also says which version of Python was used, because error text can change between versions.

Worked example 3: several cases and what you already tried

The third example is a wrong answer that shows up in only some cases. This is the situation where the "what you already tried" part earns its place.

A leap year is a year with an extra day, February 29. The rules are that a year divisible by 4 is a leap year, except years divisible by 100, unless they are also divisible by 400. Save this as leap.py:

def is_leap_year(year):
    return year % 4 == 0
 
cases = [(2024, True), (1900, False), (2000, True), (2023, False)]
for year, expected in cases:
    actual = is_leap_year(year)
    status = "ok" if actual == expected else "FAIL"
    print(f"{status}: is_leap_year({year}) = {actual}, expected {expected}")

Run it with python3 leap.py. The real output is:

ok: is_leap_year(2024) = True, expected True
FAIL: is_leap_year(1900) = True, expected False
ok: is_leap_year(2000) = True, expected True
ok: is_leap_year(2023) = False, expected False

The program gets three of the four answers right. A report that says "leap years are wrong" would send the reader hunting. This report names the case that fails and the case that passes, and it shows the output for each:

is_leap_year(1900) returns True. Expected False, because 1900 is divisible by 100 and not by 400. The four-case example above shows this: 2024, 2000 and 2023 pass, and 1900 fails.

What I tried: I checked 2024 by hand and it is correct. I also looked at the rule for years divisible by 100 and I think the function does not handle it, but I have not changed the code yet.

The "what you tried" part does three things. It shows the reader you did some work, so they do not repeat the same checks. It narrows the problem, because the reader now knows the first failing case. It also keeps the reader from giving advice you have already followed.

Notice what this report does not claim. It says "I think the function does not handle it." That is a guess, and it is marked as one. A good report separates what you observed from what you believe.

What to write in "what you already tried"

The section can be short. Use the same rule as the rest of the report: say what you did and what came out. Some useful forms:

  • "I ran the example with Python 3.14.4 and got the same output."
  • "I tried the input [] and it crashed with the traceback below."
  • "I checked the value 2024 by hand and it matches the expected answer."
  • "I printed the value of total before the return and compared it with my own sum on paper."
  • "I searched the error text and found no match that fits this case."

Two things to avoid. Do not write "I tried everything." The reader cannot check it, and it hides the real steps. Do not leave the section out because you think the work did not count. A failed check is useful: it tells the reader what not to repeat.

Environment information

Some bugs depend on the version of the tool. A program that works on one version of Python can fail on another. Add the version when you think it might matter, and always when the error comes from inside a library.

To get the version of Python, run this in the terminal:

python3 --version

The output on the machine used for this lesson is:

Python 3.14.4

For Node.js, the JavaScript runtime, the command is:

node --version

The output here is:

v24.20.0

Write the version exactly as the command prints it. Do not round it, and do not write "the latest version", because the reader cannot tell what that means on the day they read it.

Common mistakes

These are the mistakes that cost the most time. Each one is shown with a real error where that helps.

Pasting a summary instead of the error text

A report that says "it gives an error about a variable" is much weaker than one that pastes the message. Here is a typo in the average function, totl written where total was meant. Save it as typo.py:

def average(numbers):
    total = 0
    for n in numbers:
        totl += n
    return total / len(numbers)
 
print(average([2, 4, 6]))

The real output is:

Traceback (most recent call last):
  File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons-06/bug/typo.py", line 7, in <module>
    print(average([2, 4, 6]))
          ~~~~~~~^^^^^^^^^^^
  File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons-06/bug/typo.py", line 4, in average
    totl += n
    ^^^^
UnboundLocalError: cannot access local variable 'totl' where it is not associated with a value

The message names totl. A reader who sees the name can fix the typo in a few seconds. A reader who sees only "a variable error" has to ask for the text. Copy the text, and do not retype it from memory, because retyping changes small details.

Describing the cause instead of the symptom

"The loop starts at the wrong index" is a guess. "average([2, 4, 6]) returns 3.3333333333333335" is a fact that the reader can check. Put facts first. Put guesses last and mark them as guesses.

Leaving out the input

A report that says "the string version crashes" does not say what the string was. Here is a call with a string where a list was expected. Save it as strs.py:

def average(numbers):
    total = 0
    for n in numbers:
        total += n
    return total / len(numbers)
 
print(average("2 4 6"))

The real output is:

Traceback (most recent call last):
  File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons-06/bug/strs.py", line 7, in <module>
    print(average("2 4 6"))
          ~~~~~~~^^^^^^^^^
  File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons-06/bug/strs.py", line 4, in average
    total += n
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

The report must include the input "2 4 6", because that input is the cause. Without it, the reader might test with a list, see no crash, and close the report.

Sending broken code and leaving out the error

When the code does not run at all, the error is a syntax error, and the caret points to the spot. Here is a list with a missing bracket. Save it as syntax.py:

def average(numbers):
    total = 0
    for n in numbers:
        total += n
    return total / len(numbers)
 
print(average([2, 4, 6)

The real output is:

  File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons-06/bug/syntax.py", line 7
    print(average([2, 4, 6)
                          ^
SyntaxError: closing parenthesis ')' does not match opening parenthesis '['

A report that says "my code does not run" makes the reader guess. A report with the full text, including the caret, shows the spot. The caret is part of the message. Do not drop it when you copy.

Sending a screenshot of text

A screenshot cannot be searched, copied into a test, or read by a screen reader. It also costs the reader more effort to read than plain text. Paste the text. Use a screenshot only for things text cannot show, such as a layout problem in a web page, and add a text description with it.

Several bugs in one report

Two bugs in one message get two half answers. Send one report for each problem. Each report then has its own minimal example, its own expected output, and its own status. The reader can close one without touching the other.

A title that is only a feeling

"Broken", "does not work" and "urgent" tell the reader nothing when they scan a list of reports. Name the symptom: "average skips the first number" or "leap year check accepts 1900". A good title is one line that another person could search for.

Practice

Exercise 1: rewrite a vague report

Rewrite this message as a good report. You do not have a real program, so write the parts you would need and say what is missing.

My leap year code is broken. It worked for some years. Please help.

Show the answer

A good version names the failing input, the expected output and the actual output, and says what is unknown.

Title: is_leap_year returns True for 1900.

Minimal example: the four-case program from worked example 3.

Actual output: FAIL: is_leap_year(1900) = True, expected False.

Expected output: False, because 1900 is divisible by 100 and not by 400.

Environment: Python 3.14.4.

What I tried: the years 2024, 2000 and 2023 give the right answers. I have not changed the code.

The text "it worked for some years" becomes the exact list of years that work and the one that does not.

Exercise 2: find the line for a report

Save the typo program from the mistakes section as typo.py and run it. Write the one line of the report that gives the reader the location and the name of the error.

Show the answer

The line is:

UnboundLocalError: cannot access local variable 'totl' where it is not associated with a value

The location is line 4 of average. The line numbers in the traceback point there, and the message names totl, which is the misspelled name.

Exercise 3: predict before running

Predict the output of the wrong average function when it is called with [2, 4, 6, 8, 10]. Write your prediction first, then run average with that input. The function skips the first number and divides by the full count.

Show the answer

The loop adds 4, 6, 8 and 10, which makes 28. The function divides by 5, the length of the full list. So the result is 5.6. The expected average is 6.0. Running the program prints:

5.6

The report for this case has the same three parts as the report for [2, 4, 6]: the input, the expected output and the actual output.

Exercise 4: shrink a program

This program prints a wrong answer. Shrink it to the smallest program that still prints a wrong answer. You may remove the list of cases, the loop and the print statements, as long as a single call still gives the wrong output.

def average(numbers):
    total = 0
    for n in numbers[1:]:
        total += n
    return total / len(numbers)
 
scores = [90, 80, 70]
print("class average:", average(scores))
print("done")

Show the answer

The smallest version keeps the function and one call with a list of numbers:

def average(numbers):
    total = 0
    for n in numbers[1:]:
        total += n
    return total / len(numbers)
 
print(average([90, 80, 70]))

The print("done") line and the label text are not part of the bug. The function always skips the first number, so even [90] is wrong: it prints 0.0, where the expected value is 90.0. With [90, 80, 70] it prints 50.0, where the expected value is 80.0. A reader may shrink further to [2, 4, 6], which shows the same bug with smaller numbers.

Exercise 5: write the environment line

Run python3 --version on your own computer. Write the line of the report that gives the version.

Show the answer

Copy the output exactly. On the machine used for this lesson the output was Python 3.14.4, so the line reads:

Environment: Python 3.14.4

Your number will differ if your computer has a different version. Use your own output, not the one in this lesson.

Summary

  • A bug report lets another person see the problem on their own computer, so it must contain facts they can check.
  • The minimal example is the smallest program that still shows the bug. Shrink your code by removing pieces until the bug stops, then keep the last version that failed.
  • State the expected output, the actual output and the input. Paste error text in full, including the file, line numbers and the caret.
  • Give the version of the tool, and write "what you already tried" with what each check showed.
  • Describe the symptom first. Mark guesses as guesses, and send one report per bug.
Check yourself: Bug reports (8 questions) →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Reading official docs well