If you’re feeling ill and visit a doctor’s office, they typically won’t start your appointment by drawing your blood or ordering an X-ray–they’ll start by asking you questions about your symptoms. What feels unusual? When did you first notice it?
Only then will they try to identify the cause of the symptoms.
Similarly, we’ll start the debugging process by first describing the symptoms, then continue the investigation by trying to identify the proximate cause, what specifically is going wrong in the line of code where the symptoms made themselves known.
This stage may be the most complicated part of the OILER method. That’s because it requires identifying differences between your exepectations and reality. It may be that your code doesn’t actually do what you intended. Or your data may have properties you were unaware of, leading to surprising outcomes!
Much of the work here is getting clear on your own thinking, both about your expectations and what the code says.
3.1 Strategy
3.1.1 Describe the Symptom
First, take a moment to clarify: what has happened that is unexpected? Perhaps you expected the program to run all the way to completion, but it’s thrown an error during execution. Or perhaps you expected the program to write a dictionary to a file, but the file remains empty after the program has run.
When executing a program results in something different than you expected, we call it a symptom. A symptom can look like:
a runtime error
an output that is not what you wanted
cell output in a notebook, resulting from a call to print() or from evaluating the last expression in a cell
image content, resulting from invocation of a plotting function
file contents, resulting from a file writing operation
3.1.2 Identify the Proximate Cause
Next, you will try to identify something called the proximate cause- the line of code whose execution lead to the symptom you identified. Note that this may not be exactly the same as the root cause, the part of your program where things first started going unexpectedly!
Identifying the proximate cause involves three steps:
Finding the proximate location (a specific line in your code),
Forming a hypothesis about the cause of the unexpected behavior at that location, and
Verifying that hypothesis.
Tip 3.1: Turn on line numbering!
If you are coding in a Jupyter notebook, you may not see line numbers for the code in your cells. We recommend that you always turn on line numbering: explore the command menus to find this option. It will be helpful in figuring out what lines of code the error messages are referring to. It will also be helpful in any communications with other people, because you can refer to the line numbers.
3.1.2.1 Find the Location
We have to start every investigation somewhere. There are two strategies for beginning, depending on the nature of the symptom you’ve encountered:
If there’s an error message… parse the message to for the line number at which your program’s execution halted (not sure how? See Tip 3.2 for details). Then, parse the error message for critical keywords (Tip 3.3).
If there’s no error message… find the line where the unexpected output happens. This might look like:
If you were surprised by a result printed to output, look for the line of code where printing happens. This might be an explicit print() invocation. It could also be a cell in a Jupyter notebook that immediately displays the result of a computation.
If a plot doesn’t look right, find the line of code that displays the figure. Even better, if you can, find the line of code that defines the plot element that looks unusual (such as the origin of a variable plotted on the x or y axis).
If you were surprised by the contents of a file created by your program, find the line of code that writes to the file.
Either way, you should end up with your your “cursor in the code” somewhere (see Tip 3.4).
Tip 3.2: How to parse an error message for a line number
When you get an error message (formally known as a “stack traceback” in Python), the first piece of information you’ll want to read for is the location in the code where execution stopped.
We recommend reading from the bottom up. When code is executing, one function may call another, which may call another. Each function invocation becomes one frame in the stack traceback. The innermost function call will be at the bottom of the stack trace. That’s why you’ll want to read from the bottom up.
Why don’t we say to just look at the bottom? Well, the code that you are debugging may invoke a library or module, and during its execution, it may invoke some other functions internal to that library. Those function calls may appear at the bottom of the stack trace. Information about the internal implementation of these libraries can get pretty confusing. So, you will need to read upwards in the stack trace. You generally won’t even consider making any changes to the software libraries you’ve invoked, but there is some code that you wrote or that you inherited that you will consider changing. So, look upward in the stack trace until you find the first reference to a line of code in your codebase, the codebase that you would consider making changes to.
Tip 3.3: How to parse an error message for a line number
Keywords in an error message are essential clues about the nature of the bug. You’ll want to keep your eyes out for a few types of information:
Look for an error type keyword- like SyntaxError or NameError.
Look for mentions of variable or function names from your code.
Look for mentions of data types that will be operated on by the code. If the message mentions a list, which variable in your code is the list it is referring to?
Often, the error message will say something about an operation that couldn’t be performed on a particular data type, without naming the particular variable. You will have to figure out which object has that type. For example, suppose I write the following code:
x = None
x.append(3)
When this executes, the following error message appears:
AttributeError: 'NoneType' object has no attribute 'append'
The error type here is AttributeError. It mentions a datatype, NoneType, which is the datatype for the value None. The append method is available for lists, but for NoneType.
The error message doesn’t explicitly tell me that x is the offending variable. I have to form a hypothesis that x has the value None when I was expecting it to be a list. That’s pretty obvious in this case, since I just set it to be None on the previous line, but imagine the common situation that several lines earlier I had set x to be the result of a function call. Based on this error message, I would form the hypothesis that x has the value None at this point in the execution.
As you get familiar with common error types, you will be able to build on your understanding of previously encountered bugs to quickly resolve new ones. Logging bugs in a Bug Book will help you build up a personal reference, an idea we will return to in the Reflection step (see ?sec-bug-book). Additionally, resources like official documentation, user forums (such as Stack Overflow), or LLMs can help you leverage other programmers’ experience.
Tip 3.4: Get your cursor in the code.
You are trying to identify a very specific location. You’ll know it’s specific enough if you’ve identified a particular line number. So we’ve coined the slogan, “Get your cursor in the code”. If you’re reading code in print, that won’t be possible, but if you’re on a screen, click your mouse to put your cursor on the line of code that is the proximate location.
3.1.2.2 Hypothesize a Cause
Next, you should try to form a hypothesis about the cause, and verify that hypothesis.
There are three potential kinds of causes:
Code vs. Intention Mismatch
Computation Misunderstanding
Data Fault
3.1.2.2.1 Code vs. Intention Mismatch
You should check first for a potential mismatch between what your code actually says and what you intended it to say. Python is a “formal language”, meaning that it is very precise and picky. The interpreter will do what the code says to do, not what the programmer meant it to do. You may think that you asked for a slice containing the first two elements of a list. But if you wrote lst[1:2] you actually get a list with just the second element, because indexing begins with 0 and the formal rules for interpreting slices say to exclude the last element.
By contrast, English allows for lots of ambiguity. Listeners can often figure out what speakers meant, even if their words were not precise (though misinterpretations and multiple interpretations are the source of both comedy and tragedy).
Code vs. intention mismatches are worth checking for first, because they are the most common cause of errors, especially for novice programmers. More experienced programmers make these errors as well, but they often catch them while they’re writing code, so they come up less frequently in debugging sessions.
Some common causes of code vs. intention mismatch include:
syntax errors (e.g., missing colon or mismatched parentheses)
indentation errors
grouping errors (additional parentheses often correct these)
wrong function or variable name
incorrectly accessing elements of a collection, like a list or dictionary
It may be useful in this step to explicitly describe the requirements Python imposes at this line. If you are using a function, how many arguments does it accept, and what data types should they have? Is a colon needed at the end of a line? If you are accessing values from a dictionary, what keys are acceptable?
Slow down!
To verify a hypothesis about a code-intention mismatch, you might be tempted to immediately begin experimenting with fixes. If you think there’s something amiss about how you indented parts of a function, you could begin adding and subtracting indentations until the bug goes away.
We caution, however, that the fixes you try should be hypothesis driven. Add parentheses for grouping only if you have a hypothesis that things are getting grouped incorrectly and therefore operations are being performed in the wrong order. If you just try all kinds of syntactic changes to your program, you may resolve the particular symptom without fixing the real problem. And you won’t improve over time at avoiding code-intention mismatches.
3.1.2.2.2 Computation Misunderstanding
A second cause of bugs is misunderstanding what a computation does. By computation, we are speaking broadly about any operation, function, or method–any of the “plot points” in the play that is your program. This typically happens two ways:
Misunderstanding inputs: You may have an incorrect understanding of how many arguments a function accepts, their data types, or what order they should be passed in.
Misunderstanding outputs: You may not truly understand how a function transforms inputs into outputs or what form those outputs will take.
An (very common) example of misunderstanding a base Python function involves the sum() function: to many beginners, sum(1,2) looks like it’s going to add the integers 1 and 2. But this expression will throw an error! That happens because sum() expects a sequence, like a list or tuple, of values to total up (for example, sum([1,2])). This kind of knowledge takes time and experience to build.
Even if you aren’t thoroughly versed in Python, you can develop intuition for this kind of misunderstanding. If you are looking at a particular computation- are you confident you could explain what is required of the inputs and what is expected of the outputs? If you don’t feel sure, you may be in a danger zone! There are a few ways to check your understanding:
visit a function’s official documentation page
look up discussions about the function on user forums like Stack Overflow
ask a large language model (LLM) to explain how to use the function and generate a simple code example
try running some simple, isolated invocations of the function
If your investigation brings you to a new understanding of how a computation works, and that new understanding is incompatible with the code as written, you have successfully identified the proximate cause.
Using LLMs safely
Many programmers find LLMs to be helpful debugging tools. That said, it’s critical to remember they don’t perfectly reproduce the official documentation for programming libraries- they’re a kind of statistical model, not a search engine or a calculator. That means they perform best in coding tasks that involve popular, widely-used functions. They’re also most reliable when you give them specific prompts without too many steps at once.
LLMs may sometimes create code examples that don’t really work, or don’t do exactly what you asked for. They may also give different answers if you attempt the same prompt several times. We recommendending you use LLMs with the awareness that they are not perfect oracles, but a fuzzy, probabilistic window into a sea of code patterns (that some people find very useful).
3.1.2.2.3 Data Fault
A third possible cause is misunderstanding the state of the data objects at some point in the program execution. We’ll call this a data fault for short.
For example, you may think that the variable x is bound to a list of strings, but it actually has the value None. Or you may think that a dictionary includes a particular key, but it doesn’t.
The best way to verify a hypothesis about a data fault is to have your Python interpreter report on the state of the data. If you are concerned about understanding the state of a variable right before you pass it to a function at line 10, add a print statement in line 9 that prints the contents of the variable and its data type. Or add an assert statement that encodes your expectations about the value and type of a variable. If you are using an interactive debugger, you can set a breakpoint at the line of code in question and manually inspect the contents of the data object.
Are we done investigating?
If your hypothesis is confirmed, go on to the next step! If not, make another hypothesis and try again.
3.2 Practice
Let’s continue on with our supermarket checkout code example. If we run this example, we’ll get an error. Let’s run the code and then step through the Investigate strategies we introduced above.
Code
import csvdef read_two_column_csv(file_name):withopen(file_name, "r") asfile: reader = csv.reader(file)next(reader) # skip header row data = {}for item, value in reader: data[item] = valuereturn datadef create_combined_inventory(quantities, prices): combined_data = {}for item in quantities: combined_data[item] = {"quantity": int(quantities[item])}for item in prices: price =float(prices[item].replace("$", ""))if item in combined_data: combined_data[item]["price"] = priceelse: combined_data[item] = {"price": price}return combined_data## Process a shopping requestdef check_one_product(inventory, product, request_qty): message ="" available_qty = inventory[product]["quantity"] price = inventory[product]["price"]if request_qty > available_qty: qty = available_qty apology ="Sorry, we only have "else: qty = request_qty apology ="" subtotal = qty * price message = (f"${qty * price:.2f}: {apology}"f"{qty}{'unit'if qty ==1else'units'} of {product}; "f"${price:.2f} per unit.\n" )return subtotal, messagedef process_shopping_request(shopping_request, inventory): response ="" total =0for product, request_qty in shopping_request.items(): subtotal, message = check_one_product(inventory, product, request_qty) total += subtotal response += message response +=f"----\n${total:.2f} Total\n"return responsequantities = read_two_column_csv("data/quantities.csv")prices = read_two_column_csv("data/prices.csv")inventory = create_combined_inventory(quantities, prices)print(process_shopping_request({"apples": 500, "bananas": 3, "pears": 1}, inventory))print( process_shopping_request( {"apples": 500, "bananas": 3, "oranges": 4, "pears": 1}, inventory ))
$10.00: Sorry, we only have 10 units of apples; $1.00 per unit.
$6.00: 3 units of bananas; $2.00 per unit.
$1.50: 1 unit of pears; $1.50 per unit.
----
$17.50 Total
This step is easy: we expected the program to run to completion and print out our shopping cart total, but it didn’t. We got this lovely error message instead.
3.2.2 Identify the Proximate Cause
3.2.2.1 Find the Location
Where should we start investigating? Well, we have an error message, so let’s parse it for a line number.
In this case, the bottom of the stack trace is referring to code we wrote, not something inside the implementation of a library, so it is line 32.
Sometimes things can get a little confusing if your code is split among several cells in a Jupyter notebook. When you get a line number 32, you have to make sure you’re getting line 32 in the correct cell. In our case, we put the whole program in one cell, so that’s not an issue.
3.2.2.2 Hypothesize a Cause
Since we have an error message, continue parsing the error message for keywords. In this case, it says that the exception was KeyError. What does that mean? Giving this a quick web search, it turns out that the official Python docs has a helpful resource about built-in exceptions. It says:
That’s useful- this keyword indicates that our program tried to access a dictionary with a key that doesn’t actually exist. Which dictionary, and which key, though?
Now, let’s look at line 32. Can we see how the error message corresponds to elements of this line? We know the error involves a dictionary and a key. What are those in the code?
The error message says "price" is the key that is missing. Is "price" being used as a key to inventory? Not quite- it’s being used as key to whatever inventory[product] is. Tricky!
3.2.2.2.1 Code vs. Intention Mismatch?
We do not have a syntax error here.
But we should check for a code-intention mismatch. Did we intend to look up the price in a dictionary that we access as inventory[product]?
What was supposed to happen here? Well, line 32 is part of the function check_one_product(). We know this is a helper function that is called by the top-level function process_shopping_request().
It looks like this line takes uses the product argument passed into check_one_product() to access the inventory dictionary for that particular product from the inventory dictionary. Our plot diagram from the orientation stage suggests that we think, at this point in the execution, inventory is a dictionary. The plot diagram shows that inventory is a nested object- a dictionary whose keys are product names like 'apples' and whose values are also compound objects, with information about price and quantity available.
We may have been a little fuzzy about exactly what that inner compound object is. But it makes sense that it should be a dictionary, with keys for ‘quantity’ (see code line 31) and ‘price’.
Let’s follow the strategy of describing the requirements Python imposes at this line- we can do this by looking at the line and brainstorming any requirements that come to mind, no matter how obvious they seem:
We’re inside a function, so Python needs this line to be properly indented.
The object inventory should be a nested dictionary for this syntax to work.
Dictionary values can only be accessed via their keys.
Dictionary keys can only be certain types in Python (strings, numbers, or tuples).
Getting clear about these requirements is nice, but it’s not obvious any of them were violated in the design of the program.
Overall, it seems like there is a good match here between code and our intention. The code says to look up the price in the inner dictionary inventory[product], and that’s exactly what we wanted to happen at this point in the code.
So let’s keep going!
3.2.2.2.2 Computation Misunderstanding?
The only computation that’s involved here is the lookup of a key in a dictionary. We think the dictionary should include 'price' as a key, and the expression should yield the value associated with that key. If the lookup were successful, we’d store it in a variable called price.
It could be that there is some problem with our understanding of how dictionary lookup works, but it seems more likely that the problem is that price just isn’t in the dictionary.
3.2.2.2.3 Data Fault?
Perhaps we misunderstood something about the contents of the inventory dictionary. We think that the inner dictionary associated with a product name should have 'price' as a key but the error says it doesn’t. Perhaps the key has a slightly different name, like capitalized 'Price', or perhaps it’s just missing for this product.
That seems like a very plausible hypothesis. Let’s try to validate it.
How do we test this? There are many ways, some more elegant than others. Here’s one: let’s make our program print out the keys for every product’s inventory dictionary before we try to use them.
Notice the inserted line 32 in the code below.
Code
import csvdef read_two_column_csv(file_name):withopen(file_name, "r") asfile: reader = csv.reader(file)next(reader) # skip header row data = {}for item, value in reader: data[item] = valuereturn datadef create_combined_inventory(quantities, prices): combined_data = {}for item in quantities: combined_data[item] = {"quantity": int(quantities[item])}for item in prices: price =float(prices[item].replace("$", ""))if item in combined_data: combined_data[item]["price"] = priceelse: combined_data[item] = {"price": price}return combined_data## Process a shopping requestdef check_one_product(inventory, product, request_qty): message ="" available_qty = inventory[product]["quantity"]print(f"For {product} inventory, keys are {list(inventory[product].keys())}") price = inventory[product]["price"]if request_qty > available_qty: qty = available_qty apology ="Sorry, we only have "else: qty = request_qty apology ="" subtotal = qty * price message = (f"${qty * price:.2f}: {apology}"f"{qty}{'unit'if qty ==1else'units'} of {product}; "f"${price:.2f} per unit.\n" )return subtotal, messagedef process_shopping_request(shopping_request, inventory): response ="" total =0for product, request_qty in shopping_request.items(): subtotal, message = check_one_product(inventory, product, request_qty) total += subtotal response += message response +=f"----\n${total:.2f} Total\n"return responsequantities = read_two_column_csv("data/quantities.csv")prices = read_two_column_csv("data/prices.csv")inventory = create_combined_inventory(quantities, prices)print(process_shopping_request({"apples": 500, "bananas": 3, "pears": 1}, inventory))print( process_shopping_request( {"apples": 500, "bananas": 3, "oranges": 4, "pears": 1}, inventory ))
For apples inventory, keys are ['quantity', 'price']
For bananas inventory, keys are ['quantity', 'price']
For pears inventory, keys are ['quantity', 'price']
$10.00: Sorry, we only have 10 units of apples; $1.00 per unit.
$6.00: 3 units of bananas; $2.00 per unit.
$1.50: 1 unit of pears; $1.50 per unit.
----
$17.50 Total
For apples inventory, keys are ['quantity', 'price']
For bananas inventory, keys are ['quantity', 'price']
For oranges inventory, keys are ['quantity']
---------------------------------------------------------------------------KeyError Traceback (most recent call last)
Cell In[3], line 64 62 inventory = create_combined_inventory(quantities, prices)
63print(process_shopping_request({"apples": 500, "bananas": 3, "pears": 1}, inventory))
---> 64print(process_shopping_request({"apples":500,"bananas":3,"oranges":4,"pears":1},inventory))
Cell In[3], line 53, in process_shopping_request(shopping_request, inventory) 51 total =0 52for product, request_qty in shopping_request.items():
---> 53 subtotal, message =check_one_product(inventory,product,request_qty) 54 total += subtotal
55 response += message
Cell In[3], line 33, in check_one_product(inventory, product, request_qty) 31 available_qty = inventory[product]["quantity"]
32print(f"For {product} inventory, keys are {list(inventory[product].keys())}")
---> 33 price =inventory[product]["price"] 35if request_qty > available_qty:
36 qty = available_qty
KeyError: 'price'
Huh, it looks like we are indeed missing a "price" key in the inventory dictionary for oranges, even though it’s there for the other fruits! Our hypothesis of a data fault is confirmed.