Storing coordinate based grids, planes, maps or levels inside a Python dictionary has a few benefits (and a few negative tradeoffs) compared to lists of lists.

Benefit 1: neighbour check

For example, if we have a grid of

1 4 9
2 3 7
8 5 6

we could store it as

[
 [1,4,9],
 [2,3,7],
 [8,5,6]
]

but if we ever need to check any neighbouring cells, we need to manually make sure we don’t cross any index boundaries. For example, in (0, 0), we need to remember to check that a neighbour at (-1, 0) cannot be accessed.

If we instead store this as a dictionary with (x, y) keys:

{
 (0, 0): 1,
 (1, 0): 4,
 (2, 0): 9,
 (0, 1): 2,
 (1, 1): 3,
 (2, 1): 7,
 (0, 2): 8,
 (1, 2): 5,
 (2, 2): 6
}

we can always access any index, regardless of it doesn’t exist:

grid.get((-1, 0)) # None

and if there’s a default value that’s not None, we can define that as well:

grid.get((-1, 0), '#')

Benefit 2: sparse grids

Sometimes in Advent of Code puzzles, we don’t need to store the state of every coordinate. For example in the following:

X . . X
. . . X
X . . .
. . X .

where . is considered “empty”, we could only store the coordinates where there is an X and treat every missing coordinate as . when accessing them with .get() as above.

Downside: iterating over

One downside of a dictionary approach is if we need to do a lot of iterating over the individual cells, especially if we have a sparse grid.

We need to keep track of the smallest and largest x and y and do nested for loops:

for y in range(min_y, max_y+1):
	for x in range(min_x, max_x+1):
	  do_something_with(grid[y][x])

It’s also very easy to make one-off errors with this. Ask me how I know.

If we have a full grid with coordinates filled in right order we can usually loop over the keys:

for (x, y), value in grid.items():
  do_something_else_with(x, y, value)