Quick summary
Summarize this blog with AI
Debugging should explain why an operation failed without turning failure into a success-shaped result. This guide follows one small input-processing task: validate a requested record count, preserve the original cause of malformed data, and close a resource even when processing stops.
Use the syntax and exception guide for basic exception categories. This page focuses on diagnosing a failure, validating external input, and proving cleanup. Examples use the standard library and were tested with Python 3.12.14 in normal and optimized execution.
Validate input with explicit exceptions
Python can remove assert statements when running with -O. An assertion is therefore unsuitable for a required check on user input, a file, or an API response. Use an explicit condition and raise an exception that identifies the invalid input contract.
def parse_count(text):
if not isinstance(text, str):
raise TypeError('count must be text')
if not text or not text.isascii() or not text.isdecimal():
raise ValueError('count must contain ASCII digits only')
count = int(text)
if not 1 <= count <= 1000:
raise ValueError('count must be between 1 and 1000')
return count
print(parse_count('12'))
for value in ('0', '1001', 'two'):
try:
parse_count(value)
except ValueError as exc:
print(str(exc))
Expected output:
12
count must be between 1 and 1000
count must be between 1 and 1000
count must contain ASCII digits only
This teaching contract rejects spaces, signs, decimals, and non-ASCII digits instead of guessing how to interpret them. If your application intentionally accepts any of those, change the contract and tests together. Assertions remain useful for internal development invariants; they should not be required for correct program behavior or contain essential side effects.
Add context while preserving the cause
class RecordFormatError(ValueError):
pass
def parse_record_count(text, row_number):
try:
return parse_count(text)
except ValueError as exc:
raise RecordFormatError(f'row {row_number}: invalid count') from exc
try:
parse_record_count('two', row_number=3)
except RecordFormatError as exc:
print(str(exc))
print(type(exc.__cause__).__name__)
print(str(exc.__cause__))
Expected output:
row 3: invalid count
ValueError
count must contain ASCII digits only
The new exception adds a row number while retaining the underlying parsing error as its cause. The caller can present the short message and keep a full traceback for debugging. A non-string is deliberately not caught here: it violates the caller's programming contract and should not be disguised as an ordinary malformed count.
Prove resource cleanup on failure
A context manager defines cleanup at the resource boundary. StringIO gives this demonstration a real close operation without requiring a local input file. The same with-statement structure is useful when opening a text file.
from io import StringIO
stream = StringIO('2\ninvalid\n5\n')
accepted = []
try:
with stream:
for row_number, line in enumerate(stream, start=1):
accepted.append(parse_record_count(line.rstrip('\n'), row_number))
except RecordFormatError as exc:
print(str(exc))
print(stream.closed)
print(accepted)
Expected output:
row 2: invalid count
True
[2]
The stream closes, but cleanup does not make processing successful. The accepted list contains only the first row. This example stops the batch on the first invalid record; do not publish that partial list as a complete result. A different policy may retain both accepted and rejected rows, with explicit counts and status.
Reduce the failure to a minimal reproduction
Start with the final traceback line and the nearest application frame. Write down the expected input type, actual type, and operation. Reduce the failing input to a harmless representative fixture. In an interactive debugger, inspect type(text) and the row number just before parsing. A breakpoint is useful in a development session but can hang an unattended job.
import traceback
try:
parse_record_count('0', row_number=8)
except RecordFormatError as exc:
frames = traceback.extract_tb(exc.__traceback__)
print(frames[-1].name)
print(type(exc).__name__)
print(type(exc.__cause__).__name__)
Expected output:
parse_record_count
RecordFormatError
ValueError
Read chained tracebacks from the original cause through the added context. Avoid a blanket except Exception: pass, and do not return an empty list or zero merely to keep the pipeline running. Those replacements can look like legitimate business data.
Log context once, with a clear ownership boundary
When a top-level job handler owns the failure, it can log an exception with its traceback and mark the job failed. Avoid logging the same exception at every layer. Include a job ID, stage, and row number when useful, but do not put credentials or full private records into error messages. A traceback can itself include a sensitive exception message, so design the message and logging policy together.
Turn the reproduction into a regression check
Test valid boundaries 1 and 1000, invalid boundaries 0 and 1001, malformed text, a non-string, and resource closure after an exception. Run input-validation tests under normal Python and python -O; the same explicit validation should remain active. If the problem is slow execution rather than a wrong result, use a profiler after establishing a correct fixture.
For a complete numerical-cleaning exercise, see loops, functions, and validation. Primary references: the assert statement, exception chaining and cleanup, and Python debugger.