Python model for a "snake"-like game
1. Answers to your questions


Callbacks. Why not use a return value intead?

def step(self):
    """Advance the game one step.
    Return a pair of Booleans (game over, ate food).

    """
    game_over = self.head in self.tail
    ate_food = self.head == self.food
    if ate_food:
        self.tail.insert(0, self.head)
        self.food = self._new_food_location()
    self._move_snake(*self._dir)
    return game_over, ate_food


(But see below for two bugs in this function.)
Getters are normally used in three scenarios. First, to provide a simple interface to a complex data structure. Second, to give the implementer freedom to change the implementation in future. Third, in combination with setters when writing a public API whose internal data structures have an invariant that needs to be preserved, so as to protect the internal data structures from corruption by a well-meaning but incompetent client.

None of these scenarios applies here. The internal data structures are straightforward, so the first scenario does not apply. And this is all your own code (not a public API) so the second and third scenarios do not apply.
Redundancy in _move_snake. It doesn't seem like a big deal either way. No opinion here.


2. Other comments on your code


There's a bug in step: you move the snake even if the game is over. Surely the snake should stop moving when it's dead?
Another bug in step, this time in the "eat food" logic. You insert the head into the tail:

self.tail.insert(0, self.head)


but then you call _move_snake which does the same thing. So the snake ends up containing two copies of the head location.

The usual way that "snake" games work is that when the snake eats some food, it does not grow a new tail segment immediately. Instead, it waits until the next time it moves and grows a new tail segment in the position where its old tail used to be. This is easily implemented by incrementing a counter each time the snake eats food:

self.growth_pending += 1


and then decrementing the counter instead of deleting the tail segment:

if self.growth_pending:
    self.growth_pending -= 1
else:
    self.tail.pop()


Note that these bugs would have been easy for you to spot had you actually finished writing the game and tried to run it.
Inserting an item at the beginning of a list:

self.tail.insert(0, (x, y))


takes time proportional to the length of the list (see the TimeComplexity page on the Python wiki). It would be better to use a collections.deque and call the appendleft method.
This line of code:

game_field = set([(x, y) for x in range(self.width)
                  for y in range(self.height)])


doesn't need the list comprehension, as you can pass a generator expression directly to set:

game_field = set((x, y) for x in range(self.width)
                 for y in range(self.height))


This avoids constructing a list and then immediately throwing it away again.
You can further simplify that line of code using itertools.product:

from itertools import product
game_field = set(product(range(self.width), range(self.height)))

In _new_food_location you convert possible into a list so that you can call random.choice on it. But if you used random.sample instead, you wouldn't have to do that.
In _new_food_location you build a set containing every location in the game not occupied by the snake. It would be better to build this set just once per game, and keep it up to date as the snake moves.

In detail, in reset you'd write something like:

self.growth_pending = 0
self.empty = set(product(range(self.width), range(self.height)))
self.empty.remove(self.head)
self.empty.difference_update(self.tail)


(Note the use of difference_update to avoid having to convert self.tail to a set.)

In _move_snake you'd write something like:

self.tail.appendleft(self.head)
self.head = newX, newY
self.empty.remove(self.head)
if self.growth_pending:
    self.growth_pending -= 1
else:
    self.empty.add(self.tail.pop())


And finally, in _new_food_location you'd write:

new_position = random.sample(self.empty, 1)[0]