Debugging and Defensive Error Handling

EE1021 Programming for Engineers · Lecture 6 · 2026–2027

Basri Erdogan

Lecture Goals

  1. Classify syntax errors, runtime exceptions, and logic errors
  2. Read a Python traceback from the final line upward
  3. Reproduce and isolate a defect with controlled input
  4. Use narrow try blocks and specific except clauses
  5. Distinguish validation from exception handling
  6. Raise ValueError when an input violates a function contract
  7. Verify a repair with normal, boundary, and invalid inputs

Retrieval: A Sensor Parser

def parse_reading(text):
    sensor, value = text.split(",")
    return sensor, float(value)

print(parse_reading("T2,error"))

Before running the code, predict:

  1. Which line fails?
  2. What exception type appears?
  3. Is the program’s grammar wrong, or is the input unsuitable?

float("error") raises ValueError. The program is syntactically valid, but this input cannot be converted.

Planning Still Matters

Pseudocode helps define expected behavior before debugging:

INPUT one record
SPLIT record into sensor ID and value text
CONVERT value text to a number
IF conversion is impossible
    REPORT an invalid reading
OTHERWISE
    RETURN sensor ID and numeric value

Debugging asks: where does actual behavior first differ from this plan?

Part 1: Understand the Failure

Three Kinds of Bugs

Category When detected Typical evidence
Syntax error Before execution SyntaxError, invalid Python grammar
Runtime exception During execution Traceback and exception type
Logic error After or during execution Wrong result without an exception

An exception is an event at runtime. If no matching handler deals with it, normal execution stops and the exception propagates to the caller.

Syntax Error: Python Cannot Parse It

if temperature > 80
    print("alarm")
SyntaxError: expected ':'
  • The program cannot begin normal execution.
  • Inspect the reported location and the line immediately before it.
  • Fix the grammar before investigating runtime behavior.

Runtime Exception: This Input Fails

temperature = float("sensor offline")
ValueError: could not convert string to float: 'sensor offline'

The statement is valid Python. Execution fails because the value does not satisfy the conversion operation.

Logic Error: The Program Runs but Lies

readings = [20.0, 22.0, 24.0]
average = sum(readings) / (len(readings) - 1)
print(average)  # 33.0, but expected 22.0

No exception occurs. We need an expected result or an invariant to expose the defect.

Classify the Evidence

Classify each as syntax, runtime, or logic:

  1. print(readings[5]) for a three-item list
  2. if voltage > 5.0 print("high")
  3. A Celsius conversion returns 64.4 instead of 20.0
  4. registry["S9"] when S9 is absent

Check: runtime (IndexError), syntax, logic, runtime (KeyError).

Part 2: Read the Evidence

A Traceback Is a Route to the Failure

Traceback (most recent call last):
  File "monitor.py", line 9, in <module>
    print(convert(record))
  File "monitor.py", line 5, in convert
    return float(value_text)
ValueError: could not convert string to float: 'error'

Read from the final line upward:

  1. exception type;
  2. message and bad value;
  3. innermost source line;
  4. caller path.

Traceback Anatomy

Evidence Question it answers
ValueError What kind of failure occurred?
conversion message What operation and value failed?
monitor.py, line 5 Where did it fail?
convert(record) How did execution reach that line?

A traceback is evidence—not the complete diagnosis. Inspect the data and the intended contract next.

Read This Traceback

Traceback (most recent call last):
  File "summary.py", line 12, in <module>
    print(average_reading([]))
  File "summary.py", line 7, in average_reading
    return sum(values) / len(values)
ZeroDivisionError: division by zero
  1. Which function contains the failing operation?
  2. Which input made the denominator zero?
  3. What contract decision must be made before repairing it?

average_reading; the empty list; decide whether empty input is invalid, has a sentinel result, or must be prevented by the caller.

Part 3: Debug Systematically

Use a Repeatable Debugging Loop

  1. Reproduce with a known input.
  2. Reduce to the smallest failing example.
  3. Locate the first incorrect state or failing line.
  4. Hypothesize one cause.
  5. Repair one focused part.
  6. Retest the failure and nearby cases.

Changing several things at once destroys useful evidence.

Write Expected and Actual Behavior

values = [20.0, 22.0, 24.0]
expected = 22.0
actual = sum(values) / (len(values) - 1)

print("expected:", expected)
print("actual:  ", actual)

The first useful debugging question is often: what should this specific input produce?

Inspect the Smallest Useful State

total = 0
for value in [20.0, 22.0, 24.0]:
    total += value
    print("value:", value, "total:", total)

Temporary output should test a hypothesis. Remove or convert it to a test after the repair.

For larger programs, a debugger can pause execution and inspect the same state without adding prints.

Test Boundaries, Not Only Typical Values

For an average function, test:

Case Why it matters
[20.0, 22.0, 24.0] ordinary behavior
[20.0] smallest valid collection
[] empty boundary
[20.0, "bad"] invalid element

A repair is incomplete until the original failure and nearby cases have been retested.

Choose the Next Action

A report is correct for three sensors but wrong when only one sensor is supplied. What is the most useful next action?

  1. Add a broad except Exception
  2. Change several formulas
  3. Reproduce with one sensor and inspect numerator and denominator
  4. Restart Python repeatedly

Check: 3. It isolates the boundary and exposes the first incorrect state.

Part 4: Handle Expected Failures

try and except Change Control Flow

try:
    value = float(value_text)
except ValueError:
    print("Reading is not numeric")
  • Python runs the try block.
  • A matching exception skips the rest of that block.
  • The matching handler runs.
  • Execution then continues after the entire statement.

Use exception handling for failures that are expected and recoverable here.

Keep the try Block Narrow

Too broad

try:
    sensor, text = record.split(",")
    value = float(text)
    registry[sensor].append(value)
    print_report(registry)
except ValueError:
    print("bad record")

Focused conversion

sensor, text = record.split(",")
try:
    value = float(text)
except ValueError:
    print("value is not numeric")

A narrow block makes the handled operation clear and avoids hiding unrelated defects.

Catch the Exception You Can Handle

try:
    sensor, value_text = record.split(",")
    value = float(value_text)
except ValueError as error:
    print("Invalid record:", error)

Both unpacking and conversion can raise ValueError. If the responses should differ, separate the operations and their handlers.

Avoid a bare except: or except Exception: in beginner programs: it can conceal unexpected defects and interrupts.

Matching and Nonmatching Exceptions

y Operation Handler Outcome
1 5 / y not needed result is 5.0
0 5 / y except ZeroDivisionError handler runs
"abc" 5 / y no matching TypeError handler exception propagates

An except clause handles only compatible exception types.

Predict the Control Flow

print("A")
try:
    print("B")
    value = int("4.5")
    print("C")
except ValueError:
    print("D")
print("E")

Write the printed letters in order.

Check: A, B, D, E. The exception skips the remaining statements in try.

Multiple Handlers Give Specific Responses

try:
    sensor, value_text = record.split(",")
    value = float(value_text)
    limit = limits[sensor]
except ValueError as error:
    print("Malformed record:", error)
except KeyError as error:
    print("Unknown sensor:", error)
else:
    print(sensor, value, limit)

The else block runs only when the try block completes without an exception.

Validation and Exceptions Answer Different Questions

if sensor not in limits:
    print("Unknown sensor")

Validation asks whether a known condition is acceptable before an operation.

try:
    value = float(value_text)
except ValueError:
    print("Value is not numeric")

Exception handling responds when an operation cannot complete.

Enforce a Function Contract with raise

def average_reading(values):
    """Return the average of a non-empty sequence."""
    if not values:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

raise makes a violated contract explicit. The caller can handle the failure where recovery makes sense.

Where Should the Failure Be Handled?

def average_reading(values):
    if not values:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

Should this function print an error and ask for input?

Usually no. The function should report the contract violation with ValueError; the user-interface layer decides how to inform the user or request new input.

Integration: Repair and Verify

A Defensive Sensor Parser

def parse_reading(record):
    parts = record.split(",")
    if len(parts) != 2:
        raise ValueError("expected SENSOR,VALUE")

    sensor, value_text = parts
    try:
        value = float(value_text)
    except ValueError as error:
        raise ValueError("value must be numeric") from error

    return sensor, value

The function validates structure, handles conversion evidence, and returns one predictable tuple.

Exit Ticket

For parse_reading, state the expected result for:

  1. "T1,23.5"
  2. "T2,error"
  3. "missing-comma"

Then write one assertion for the valid case and name the exception expected for the invalid cases.

Check: ("T1", 23.5); both invalid records raise ValueError with different messages.

A Good Repair Leaves Evidence

  • Classify the failure before changing code.
  • Read the final traceback line first.
  • Reproduce with a small, controlled input.
  • Change one focused cause.
  • Catch only exceptions you can handle meaningfully.
  • Raise an exception when a function contract is violated.
  • Retest normal, boundary, and invalid cases.

In the lab, you will use this workflow to repair a sensor-data pipeline.