Read Python Tracebacks and Fix the Failing Layer
Use traceback frames and exception causes to locate a bug and add useful context at the correct layer.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Read Python Tracebacks and Fix the Failing Layer
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A traceback can be intimidating because it shows several lines of files, functions, and calls. The key is to treat it as a route to the failing expression, not as a wall of text. Our example starts with a configuration dictionary containing a perfectly valid port string. A parser should return the number 8080, yet the current code raises TypeError. We will reproduce that failure, inspect the final exception and the innermost frame, and fix the expression that passed the wrong object to int. Only after that will we handle an expected input error: a string such as "abc" that cannot be converted. That second case deserves a contextual message and a preserved cause.
- 00:43
The lesson's browser task also checks valid boundary ports and a missing required key. When debugging, the first question is not "Which exception can I catch?" It is "What expression failed, and what value reached it?" The config dictionary contains a suitable value, but the function passes the entire dictionary to int. Python does not interpret its "port" key automatically. The TypeError message mentions dict, which is a clue about the actual object received at the failing call. The return line is wrong even before we think about malformed user input. It would be tempting to catch TypeError and return zero, but that would convert a coding defect into a misleading value.
- 01:28
Another part of the program might then try to open port zero or report an invalid range, sending us away from the real cause. Instead, inspect the line that failed. The intended expression is int(config["port"]), where the key lookup obtains the numeric text. We should make that direct correction, not wrap the whole function in a broad handler. The final exception type tells us what failed. The frames show the path through load and then parse_port. In a full traceback there is also a top-level script frame, but the last two named functions identify our route to the problem. The innermost frame is parse_port, and its expression int(config) is where the wrong type is supplied.
- 02:11
This is how to read a long traceback: begin at the final error, then climb through the frames to understand how execution arrived there. A frame may be in library code, in which case look upward for the nearest call from your own code that supplied bad data. Inspect actual values at that point. Here config is a mapping, and config["port"] is the string we need. Tracebacks do not automatically prescribe a catch block. They provide evidence about the failing line. Once that line is fixed, new and more meaningful input failures may appear. Now the valid port succeeds because the conversion receives the string under the required key.
- 02:53
If the string cannot be parsed, int raises ValueError. We catch only that expected conversion failure and raise a new ValueError that names the problematic port text. The from clause preserves the original conversion failure in __cause__, making the diagnostic chain visible. We then check the allowed range for successfully parsed numbers. A value of zero or 65536 should fail with a range message, while one and 65535 should pass. The missing-key case is separate again: config["port"] raises KeyError if there is no required field.
- 03:33
We intentionally do not turn that absence into a default zero. A caller needs to know that the configuration is incomplete. Notice how each failure path tells a different truth about the data, instead of one catch-all handler erasing the distinctions. One and 65535 are the inclusive valid endpoints. Zero and 65536 must fail because they are outside that interval. The alphabetic string tests the conversion path and its preserved cause. The empty dictionary tests the missing required key. These are not arbitrary examples; each exercises a distinct promise of parse_port.
- 04:17
A correct success result alone would not detect a broad catch, a silent default, or a missing range check. In a real traceback, file paths and line numbers can be noisy, especially when library functions appear between your own calls. Look for the last frame you control before the exception, then compare the actual type and value at that call site with what the called function expects. If the traceback contains an explicit cause, read both final exception messages: the outer one explains the application context and the inner one explains the low-level operation. Do not log secrets merely to inspect a frame; a type name or redacted sample is often enough.
- 05:00
When you encounter a traceback in a larger project, use the same order: identify the exception type and message, find the innermost relevant frame, inspect the failing expression and its input, repair a code mistake there, and only then translate expected input failures at a meaningful boundary. The companion website task asks you to do that repair yourself. After it passes, try changing the port text to whitespace, a negative value, and a word, and predict which path each takes before running it.
Understand the concept
A traceback records the chain of calls that led to an unhandled exception. Read the final line first for the exception type and message, then move upward to find the failing line in your own code. The outer frames show how execution reached that line. A long traceback is not necessarily several independent failures; it is usually one failure traveling through a series of calls.
In a configuration loader, parse_port(config) should read config['port'], convert that value, and check that it lies between 1 and 65535. Calling int(config) by mistake produces TypeError because a dictionary is not integer text. The traceback identifies the bad expression, and printing or inspecting the value at that frame reveals the wrong type. Fix the expression before adding a broad catch; catching TypeError around the whole function would conceal the original bug.
Once the correct value reaches int, a value such as 'abc' raises ValueError. At this boundary, the function can raise a new ValueError that names the invalid port while preserving the conversion error with from exc. The resulting traceback says one exception was the direct cause of another. That is more useful than returning zero, which would make invalid input look like a legitimate value.
The task implements parse_port(config) for a mapping containing 'port'. Missing keys should still raise KeyError, so do not silently default them. Valid numeric text becomes an integer. Conversion failures get contextual ValueError with a cause; out-of-range numbers get ValueError with a clear range message. Tests distinguish those paths and check both range boundaries.
- The final traceback line names the exception
- Earlier frames show the call path
- Inspect the value at the failing frame
- Explicit chaining preserves an underlying cause
See it step by step
Read the code, predict the output, then compare it with the result.
01. Locate a failing expression
def parse_number(text):
return int(text)
def load(settings):
return parse_number(settings['count'])
try:
load({'count':'many'})
except ValueError as exc:
print(type(exc).__name__)
print('many' in str(exc))ValueError
TrueThe call path is load then parse_number, but int('many') is the actual failing expression.
02. Display the final traceback line
import traceback
def convert(text):
return int(text)
try:
convert('abc')
except ValueError:
print(traceback.format_exc().strip().splitlines()[-1])ValueError: invalid literal for int() with base 10: 'abc'The final line names the exception; the full formatted traceback also includes source frames.
03. Add context without hiding the cause
def parse_port(text):
try:
return int(text)
except ValueError as exc:
raise ValueError(f'Invalid port: {text!r}') from exc
try:
parse_port('many')
except ValueError as exc:
print(str(exc))
print(type(exc.__cause__).__name__)Invalid port: 'many'
ValueErrorThe outer message identifies the configuration field; the inner cause remains available for diagnosis.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Read the final error and then follow frames
A traceback shows a path through function calls to the expression that failed. The final line names the exception and usually gives the most immediate clue. Work upward from there to the last frame in your own code, then inspect the exact expression and the values it received. Do not start by adding a broad handler around the entire call path; that can hide the faulty expression.
In this example, load calls parse_number with a value from a settings dictionary. The string many reaches int, which rejects it. We catch the error only to print a compact observation. An unhandled run would show both function frames. The important point is that the error occurs at conversion, not at the outer function call.
def parse_number(text):
return int(text)
def load(settings):
return parse_number(settings['count'])
try:
load({'count': 'many'})
except ValueError as exc:
print(type(exc).__name__)
print('many' in str(exc))ValueError
True- load retrieves the count field.
- parse_number passes its text to int.
- The conversion raises ValueError for nonnumeric text.
Fix a wrong expression before catching errors
A related bug happens if the function passes the entire configuration mapping to int instead of its port value. That raises TypeError even when the port string is perfectly valid. The traceback points to int(config), so the repair is to index the mapping first. Catching TypeError and returning zero would conceal a programming mistake and turn a correct setting into an invalid-looking result.
The corrected version below extracts config['port'] and converts it. Required-key indexing also makes absence explicit: a missing port raises KeyError. The practice task later adds user-facing context for bad numeric text, but that is a different failure from passing the wrong object type to int. Separate code defects from expected invalid input.
def broken(config):
return int(config)
try:
broken({'port': '8080'})
except TypeError as exc:
print(type(exc).__name__)
def parse_port(config):
return int(config['port'])
print(parse_port({'port': '8080'}))TypeError
8080- The broken line passes a dictionary to int.
- Indexing the required key produces the intended text.
- Converting that text gives the numeric port.
Add context while preserving the cause
After fixing the wrong expression, malformed numeric text becomes an expected boundary failure. Catch ValueError only around int(text), then raise a new ValueError that names the invalid port and chain it from the conversion error. The cause remains inspectable. A numeric value outside the allowed range is a different path and receives a range-specific message.
The checker tests both endpoints, malformed text, out-of-range text, and a missing key. Each path should tell a useful truth about the failure. The function must not manufacture a default for a missing required field. Read its contract, run the examples, and check the exception type and message rather than merely seeing that something failed.
def parse_port(config):
text = config['port']
try:
number = int(text)
except ValueError as exc:
raise ValueError(f'Invalid port: {text!r}') from exc
if not 1 <= number <= 65535:
raise ValueError('port must be from 1 to 65535')
return number
print(parse_port({'port': '8080'}))
try:
parse_port({'port': 'abc'})
except ValueError as exc:
print(str(exc), type(exc.__cause__).__name__)8080
Invalid port: 'abc' ValueError- The required key is read before conversion.
- A malformed number gets a contextual error with a cause.
- Range validation is applied only to successfully parsed integers.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Use text = config['port']. Catch ValueError only around int(text), then raise ValueError(f'Invalid port: {text!r}') from exc. Check 1 <= number <= 65535 afterward.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Catching a bug instead of fixing its line
try:
return int(config)
except TypeError:
return 0text = config['port']
number = int(text)What happens: Passing a dictionary to int is hidden and an invalid port looks successful.
Use the traceback to repair the wrong expression before handling expected input failures.
Defaulting a missing required key
text = config.get('port', 0)text = config['port']What happens: A missing port becomes a misleading out-of-range value instead of identifying the absent key.
Required fields should fail clearly when absent; KeyError identifies the missing key.