Exercise 3: Explaining Why Negative Coordinates Corrupt Instead of Crash — Possible Solution ==================================================================== THE TEST ------------------------------ canvas_a = Canvas(10, 10, background=(255, 255, 255)) fill_rect_naive(canvas_a, Rect(-2, 0, 4, 1), (255, 0, 0)) canvas_a.pixels[0][8], canvas_a.pixels[0][9] canvas_b = Canvas(10, 10) fill_rect_naive(canvas_b, Rect(8, 8, 5, 5), (0, 0, 255)) RESULT ------------------------------ canvas_a.pixels[0][8] -> (255, 0, 0) -- red, silently corrupted canvas_a.pixels[0][9] -> (255, 0, 0) -- red, silently corrupted fill_rect_naive(canvas_b, Rect(8, 8, 5, 5), ...) -> raises IndexError WHY THE NEGATIVE CASE PRODUCES SILENT CORRUPTION, NOT A CRASH ------------------------------ fill_rect_naive computes x0 = int(rect.x) = int(-2) = -2, and loops `for x in range(x0, x1)`, i.e. range(-2, 2) = [-2, -1, 0, 1]. Each of these becomes canvas.pixels[y][x] for x in that list. Python list indexing treats a negative index as "count from the end of the list" -- this is a DELIBERATE, documented, and normally very useful Python feature (my_list[-1] is the last element, my_list[-2] is the second- to-last, and so on). For a row of length 10, index -2 refers to the SAME element as index 8, and index -1 refers to the same element as index 9. So canvas.pixels[0][-2] = color and canvas.pixels[0][-1] = color are not errors at all -- they are completely valid, successful assignments, just to a different (and unintended) pair of positions than the ones the rect was actually supposed to occupy. The bug isn't that Python does something wrong; it's that -2 and -1 are simultaneously "off the left edge of the intended rect" AND "valid indices pointing at the right edge of the row," and fill_rect_naive has no code that distinguishes those two meanings from each other. WHY THE POSITIVE CASE PRODUCES A REAL CRASH INSTEAD ------------------------------ fill_rect_naive computes y0 = 8, y1 = int(8 + 5) = 13, and loops `for y in range(y0, y1)`, i.e. y takes the values 8, 9, 10, 11, 12. canvas.pixels only has 10 rows, valid indices 0 through 9. There is no Python indexing convention that makes index 10 refer to anything at all for a length-10 list -- unlike negative indices, which wrap around to a specific, well-defined position, an index that's too LARGE simply has nothing to wrap to. Python's list indexing only defines a wraparound behavior in the negative direction (counting backward from the end); it has no analogous "wrap forward past the end" behavior in the positive direction. So canvas.pixels[10] raises IndexError: list index (or list assignment index) out of range, immediately, the first time y reaches 10. WHY THIS WORKS AS AN ANSWER ------------------------------ The asymmetry comes entirely from how Python's own indexing rules are defined, not from anything specific to this rasterizer's own code: negative indices have a well-defined, valid meaning (count from the end), so an out-of-bounds-to-the-left coordinate never triggers an error at all -- it just silently means something different than intended. Positive indices have no such fallback meaning once they exceed the list's own length, so an out-of-bounds-to-the-right (or -bottom) coordinate has nowhere valid to resolve to, and Python is forced to raise an error. The exact same missing bounds check (there isn't one, in either direction) produces two totally different visible symptoms purely because of this asymmetry in what Python's own indexing considers "valid" in each direction.