Why this matters
Most of your work as a software engineer will be in code that someone else wrote. Some of it comes from teammates. Some comes from people who left the company years ago. Some comes from you, months earlier, before you knew what you know now. Job descriptions that ask for "experience with an existing codebase" mean this skill.
Reading code is a different skill from writing it. When you write, you already know what the program should do. When you read, you open a file with no idea of its purpose. A routine gives you a way in. This lesson teaches three moves: read from the top, follow one input through the program, and draw the calls on paper. Together they answer three questions every time you open unfamiliar code. Where does it start? What happens to one piece of data? What depends on what?
The example is a small shop program. It has five files and prints a receipt for each order. The examples were run with Python 3.14, the version on the machine where this lesson was written. You do not need to know Python well to follow them. Each technical word is defined the first time it appears.
Words used in this lesson
- Codebase: all the code that makes up one project.
- Entry point: the place where a program starts running.
- Function: a named block of code that does one job. You run it by name, such as
price_for. - Call: running a function by writing its name and brackets. A function that calls another function is the caller.
- Argument: a value passed into a function inside its brackets, such as the
2inprice_for("coffee", 2). - Import: a line that loads code from another file so this file can use it.
- Dictionary: a table of key and value pairs in Python.
PRICES["tea"]looks up the price stored under the key"tea". - Trace: the list of steps a program takes for one specific input.
- Call diagram: a tree that shows which function calls which.
Step 1: Start at the entry point
Open main.py. It is the entry point, the file that starts the program.
from cart import Cart
from receipt import format_receipt
ORDERS = [
[("coffee", 2), ("muffin", 1)],
[("tea", 1), ("cake", 3)],
]
def run():
for number, order in enumerate(ORDERS, start=1):
cart = Cart()
for name, quantity in order:
cart.add_item(name, quantity)
print(format_receipt(number, cart))
if __name__ == "__main__":
run()Read it in this order.
- The import lines at the top list the other files this file uses:
cart.pyandreceipt.py. Any file not named here is not needed to understand this one. ORDERSis data, the information the program works on. There are two orders. Each order is a list of pairs: an item name and a quantity.runis a function. It loops over the orders. For each order it creates aCart, adds the items, and prints a receipt.- The last two lines start the program. The
if __name__ == "__main__":line means "run this only when this file is started directly". Python sets__name__to"__main__"for the file you start. If another file importsmain.py, that check is false andrundoes not start.
When you read a file for the first time, follow these rules.
- Read the imports to learn which other files matter.
- Read the names of the functions and variables before their bodies. A name like
format_receipttells you the job before you read a line of it. - Read the bottom of the file to see what starts it.
- Skip the detail you cannot use yet. You can come back to it.
Next, list the folder so you know what else is there.
ls -1 --ignore=__pycache__ shopcart.py
main.py
pricing.py
receipt.py
tax.pyThe --ignore=__pycache__ part hides a folder that Python creates on its own to store compiled copies of your files. You can ignore it when reading.
Step 2: Learn the job of each file
Reading one file is not enough. You need to know what each file is for, and which files it depends on. Make a short table on paper or in a text file. Three columns are enough: the file, its job, and the files it imports.
| File | Job | Imports |
|---|---|---|
| main.py | Starts the program and loops over the orders | cart.py, receipt.py |
| cart.py | Holds the items of one order and adds up their prices | pricing.py |
| pricing.py | Stores the price list and applies the bulk discount | nothing |
| receipt.py | Builds the text of one receipt | tax.py |
| tax.py | Stores the tax rate and adds tax to an amount | nothing |
Now read the table from the bottom. pricing.py and tax.py import nothing, so they are the base of the program. If you change a price rule in pricing.py, the change reaches cart.py, then receipt.py, then main.py. The table tells you that before you run anything.
File names are a guess, not a fact. tax.py sounds like it might show the tax on a receipt. Open it and you find this:
TAX_RATE = 0.08
def add_tax(amount):
return round(amount * (1 + TAX_RATE), 2)It holds the rate and one function that returns the amount with tax added, not the tax alone. You only learn that by reading the file. Keep the file name as a hint, then check it.
Step 3: Trace one input
A trace follows the program one step at a time for one specific input. Pick a single input and follow it all the way through. Do not try to follow every input at once.
Take the first order: two coffees and one muffin. Here is the trace, written by hand.
runcreates an emptyCart.runadds("coffee", 2)and("muffin", 1)to the cart. The cart stores the pairs. Nothing is priced yet.runcallsformat_receiptwith the number1and the cart.format_receiptcallscart.subtotal().subtotalcallsprice_for("coffee", 2). The unit price is 2.50. The quantity 2 is below 3, so there is no discount. The result is 5.00.subtotalcallsprice_for("muffin", 1). The unit price is 3.00 and the result is 3.00.subtotaladds the two results. The subtotal is 8.00.format_receiptcallsadd_tax(8.00). The rate is 0.08, so the total is 8.00 times 1.08, which is 8.64.format_receiptbuilds the lines of text and returns them.runprints them.
Writing the trace by hand is slow the first time. A faster way is to add print lines to a copy of the code. A print line shows a value while the program runs. Make a copy in a second folder so that the original stays clean. In the copy, pricing.py gets two extra lines inside price_for:
print(f"[trace] price_for: {name}, quantity {quantity}, unit {unit}, before discount {total}")
# ... the discount check goes here ...
print(f"[trace] price_for: returns {round(total, 2)}")The tax.py file in the copy gets one line that prints the amount it received and the amount it returns. Then run the copy.
cd shop_trace && python3 main.py | head -8[trace] price_for: coffee, quantity 2, unit 2.5, before discount 5.0
[trace] price_for: returns 5.0
[trace] price_for: muffin, quantity 1, unit 3.0, before discount 3.0
[trace] price_for: returns 3.0
[trace] add_tax: 8.0 becomes 8.64
Order 1
2 x coffee
1 x muffinThe printed steps match the hand trace. The head -8 part shows only the first eight lines of output. You can leave it out to see everything.
The traced copy taught you something the hand trace did not. Python shows 2.5 for the coffee price. The number is the same as 2.50, and Python drops the trailing zero when it prints a float. Floats are numbers stored in binary with a fixed size. Some decimal values cannot be stored exactly, which matters later in this lesson.
When the trace is done, delete the copy with the print lines once you no longer need it. Never add trace lines to the only copy of the code you are reading.
Step 4: Draw the call diagram
A call diagram turns the trace into a picture. Each line is a function call. A call that is indented under another call was made by that function. Here is the diagram for the first order.
run (main.py)
Cart() (cart.py)
cart.add_item (cart.py), once for each item line
format_receipt (receipt.py)
cart.subtotal (cart.py)
price_for (pricing.py), once for each item line
add_tax (tax.py)Read it from the top. run makes the calls under it, in order. format_receipt makes two calls: subtotal, which makes its own call to price_for, and add_tax.
You can draw this on paper in about two minutes. It answers the question you will ask most often when you change code: if I change this function, what else could break? For example, if you change price_for, the diagram shows that subtotal, format_receipt and run all sit above it. Their output may change too.
A call diagram made from one trace shows only the calls that one input made. Another input could take a different path. For a full picture, read the code as well. Use the diagram to remember the path you traced, and update it when you learn more.
Example 2: an error, read from the bottom
The first example worked, so nothing went wrong. Now give the program an item that is not on the price list. Ask for bagel, which the shop does not sell. Run the command from the shop folder.
python3 -c "from cart import Cart; from receipt import format_receipt; c = Cart(); c.add_item('bagel', 1); print(format_receipt(1, c))"Traceback (most recent call last):
File "<string>", line 1, in <module>
from cart import Cart; from receipt import format_receipt; c = Cart(); c.add_item('bagel', 1); print(format_receipt(1, c))
~~~~~~~~~~~~~~^^^^^^
File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons/shop/receipt.py", line 8, in format_receipt
subtotal = cart.subtotal()
File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons/shop/cart.py", line 12, in subtotal
total = sum(price_for(name, quantity) for name, quantity in self.lines)
File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons/shop/cart.py", line 12, in <genexpr>
total = sum(price_for(name, quantity) for name, quantity in self.lines)
~~~~~~~~~^^^^^^^^^^^^^^^^
File "/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons/shop/pricing.py", line 13, in price_for
unit = PRICES[name]
~~~~~~^^^^^^
KeyError: 'bagel'A traceback is the error report Python prints. It is a call diagram of the moment the program failed. Read it in this order.
- Read the last line first.
KeyError: 'bagel'names the kind of error and the value involved. AKeyErrormeans you asked a dictionary for a key it does not have. The dictionary isPRICES, and the key is'bagel'. - Look at the line just above the last one.
pricing.py, line 13, readsunit = PRICES[name]. That is the lookup that failed. The^^^^marks under the code show the part of the line that failed. - Go up the traceback.
price_forwas called by a line incart.py, line 12, insidesubtotal. The<genexpr>frame between them is the loop insidesumthat makes that call, and it is also line 12.subtotalwas called byreceipt.py, line 8, insideformat_receipt. Each step up is a caller. - The top of the traceback is where the program started. Here it is
<string>, which means the code came from the command line rather than from a file. The'bagel'name came from thec.add_item('bagel', 1)call you typed.
From this traceback you know the failing line, the path that led to it, and the bad value. The fix might belong in main.py, where orders are written, or in pricing.py, where a missing item could give a message in plain words. The traceback does not choose for you. It shows where to look, and you decide.
Two things to notice. First, the traceback shows the path with the caller at the top and the failing line at the bottom. Second, the file paths in the output are long because they show the folder on the machine where this was run. On your machine they will look different.
Example 3: a result that looks wrong
The third example has no error. The program prints a number that might be wrong. Look at the second order: three cakes at 4.25 each, with the bulk discount of 10 percent for three or more of one item.
The arithmetic is 4.25 times 3 is 12.75, and 10 percent off gives 11.475. Rounded to cents, the expected answer is 11.48. The program printed this:
Order 2
1 x tea
3 x cake
Subtotal: 13.27
Total with tax: 14.33The subtotal is 1.80 for the tea and 11.47 for the cakes. Was the program wrong? Check each step with a single line of Python.
python3 -c "print(4.25*3*0.9, round(4.25*3*0.9, 2))"11.475 11.47Python prints 11.475, but it stores the number slightly below 11.475. When round looks at the stored value, it rounds down to 11.47. A person doing the sum on paper would get 11.48. Neither result is a bug in Python. The question is which rule the shop wants: round the discounted price for each item, or round only the final total. That is a question about the shop's rules, and the code cannot answer it for you.
This is the logic error kind of problem, where the program runs and gives a result that might be wrong. The method is the same as before. Trace the value through each step, find the step where the number changes, and then decide whether the rule in that step is what you want.
Common mistakes
Reading every file line by line before tracing. Beginners often open every file and read it all. This takes an hour and you forget the early lines by the end. Read the entry point first, then trace one input. Open other files only when the trace reaches them.
Starting in the middle. If you open pricing.py first, you see a price rule with no idea where the prices came from. Start at the entry point, so every function you read has a caller you already know.
Trusting a name. The name add_tax sounds like it returns the tax. It returns the amount with tax added. Read the return line of every function you rely on.
Thinking a file is broken when it prints nothing. Running receipt.py directly shows nothing:
python3 receipt.py; echo "exit code: $?"exit code: 0The program exits with code 0, which means it finished without an error. The file defines functions and has no code at the bottom that starts them, so nothing happens. Check the bottom of the file before you decide something is wrong.
Misspelling a file name. Python reports the file it could not open:
python3 mian.pypython3: can't open file '/home/chiranjeet/dev/anisha/scratch-offtmp/claude-lessons/shop/mian.py': [Errno 2] No such file or directory[Errno 2] is the system code for "no such file". Run ls on the folder and copy the exact name.
Editing the only copy while tracing. Adding print lines to the original code makes it hard to return to the clean version. Make a copy in another folder, as in Step 3, and keep the original unchanged.
Practice
Exercise 1. You want to know where the tax rate is set. Without reading more than needed, which file do you open, and how do you know?
Show the answer
Open tax.py. The table in Step 2 shows that receipt.py imports from tax.py, and tax.py defines TAX_RATE = 0.08. main.py does not import the tax file, so it is not where the rate lives.
Exercise 2. The trace in Step 3 shows the steps for the first order. What is the final total, and which two numbers does the subtotal come from?
Show the answer
The final total is 8.64. The subtotal is 8.00, which is 5.00 for the coffees plus 3.00 for the muffin. Then 8.00 times 1.08 is 8.64.
Exercise 3. Without running anything, count how many times price_for is called for the first order, and name the function that calls it.
Show the answer
It is called twice, once for coffee and once for muffin. Both calls come from subtotal in cart.py, because subtotal loops over the two item lines and calls price_for for each one.
Exercise 4. Predict what this command prints. Then run it to check.
python3 -c "from pricing import price_for; print(price_for('muffin', 3))"Show the answer
It prints 8.1. Three muffins at 3.00 are 9.00. The quantity 3 is at least BULK_DISCOUNT_FROM, so the total is multiplied by 0.9, which gives 8.1. The output of the run is 8.1.
Exercise 5. The traceback in Example 2 ends with KeyError: 'bagel'. Which line of your own code do you check first, and what do you check after that?
Show the answer
Check pricing.py, line 13, unit = PRICES[name], because that is the lookup that failed. Then go up the traceback to find where 'bagel' entered the cart. In this case it came from the c.add_item('bagel', 1) call. In a real project you would look for the place where the order was created, such as a file that reads orders from a form or a list.
Summary
- Start at the entry point and read its imports first, so you know which other files matter.
- Make a table of files with each file's job and the files it imports. Read the table from the base up.
- Trace one input from start to finish, first by hand, then with
printlines in a copy of the code. - Draw a call diagram as an indented tree, where each line calls the lines indented under it.
- Read a traceback from the bottom to find the error, then go up to find where the bad value came from.
- When a result looks wrong, print each step and decide whether the rule at the step that changed the number is the one you want.