Many Advent of Code problems involve the manipulation of points within a 2D space. Thus, my solutions typically include some variation of the same move() and neighbor() functions:

    "^": ( 0, -1),
    "v": ( 0,  1),
    ">": ( 1,  0),
    "<": (-1,  0),

def move(point, direction):
    x, y = point
    dx, dy = DIRECTION_TO_DELTA[direction]
    return (x + dx, y + dy)

def neighbors(point):
    for direction in DIRECTION_TO_DELTA:
        yield move(point, direction)

print(list(neighbors(1, 2)))

Here, points are represented by 2-tuples, and I heavily rely on unpacking to extract the point's x and y values whenever I need to do any kind of computation on them. This is especially visible in the implementation shown for move(). Also worth noting, the x and y values can be extracted using indexing too:

point = (2, 6)

# tuple indexing
x = point[0]
y = point[1]

# tuple unpacking
x, y = point

But there's another way to represent these (x, y) points. Instead of using a tuple, we can take advantage of Python's built-in support for complex numbers! Real parts store the x values while imaginary ones store the y values. A point such as (3, 7) is then represented as 3 + 7j instead. With this new way of representing points, extra steps for unpacking are no longer necessary, and the first code snippet above simplifies to this:

    "^": -1j,
    "v":  1j,
    ">":  1,
    "<": -1,

def move(point, direction):
    return point + DIRECTION_TO_DELTA[direction]

def neighbors(point):
    for direction in DIRECTION_TO_DELTA:
        yield move(point, direction)

print(list(neighbors(1 + 2j)))

Whenever the individual x or y values need to be retrieved, they can be accessed using the built-in attributes for complex numbers:

point = 2 + 6j

x = int(point.real)
y = int(point.imag)

The only thing to note is that number.real and number.imag are both float values, so it's essential to cast them to integers if that's desired.

The use of complex numbers to represent 2D systems is elegant, so I will use them more in the future, but they're unfortunately limited to 2D spaces. For 3D spaces, 3-tuples are still the way to go. Or if you're feeling fancy, a dataclass:

from dataclasses import dataclass

class Point:
    x: int
    y: int
    z: int
    def move(self, vector):
        return Point(
            self.x + vector.x,
            self.y + vector.y,
            self.z + vector.z,

    def neighbors(self):
        vectors = [
            Point(x, y, z)
            for x in range(-1, 2)
            for y in range(-1, 2)
            for z in range(-1, 2)
            if x != y != z != 0
        for vector in vectors:
            yield self.move(vector)

print(list(Point(1, 2, 3).neighbors()))

This implementation is object-oriented, but it's easy to switch to the functional style used in the earlier code snippets. Either way, I don't know a built-in way to represent 3D spaces in Python instead of constructing my own representation. Do you?