Tic-Tac-Toe in Python3 I am new to Python and would like to get helpful tips and suggestions on this code. Would be awesome if you provide info if anything is fundamentally incorrect, improve the algorithm, and any room for further improvements.

class Game:
  def __init__(self):
    self.board = ["0", "1", "2", "3", "4", "5", "6", "7", "8"]
    self.ai = "X"
    self.human = "O"

  def start_game(self):
    print(" %s | %s | %s \n=========\n %s | %s | %s \n=========\n %s | %s | %s \n" % \
        (self.board[0], self.board[1], self.board[2],
             self.board[3], self.board[4], self.board[5],
             self.board[6], self.board[7], self.board[8]))
    print("Enter number between [0-8]:")
    while not self.game_over(self.board) and not self.draw(self.board):
      self.get_human_position()
      if not self.game_over(self.board) and not self.draw(self.board):
        self.eval_board()

      print(" %s | %s | %s \n=========\n %s | %s | %s \n=========\n %s | %s | %s \n" % \
          (self.board[0], self.board[1], self.board[2],
               self.board[3], self.board[4], self.board[5],
               self.board[6], self.board[7], self.board[8]))

    print("Game over") # have to think of way to print Draw when it's a draw

  def get_human_position(self):
    spot = None
    while spot is None:
      spot = int(input())
      if self.board[spot] != "X" and self.board[spot] != "O":
        self.board[spot] = self.human
      else:
        spot = None

  def eval_board(self):
    spot = None
    while spot is None:
      if self.board[4] == "4":
        spot = 4
        self.board[spot] = self.ai
      else:
        spot = self.get_optimal_move(self.board, self.ai)
        if self.board[spot] != "X" and self.board[spot] != "O":
          self.board[spot] = self.ai
        else:
          spot = None

  def get_optimal_move(self, board, next_player, depth = 0, highest_score = {}):
    available_spaces = [s for s in board if s != "X" and s != "O"]
    optimal_move = None

    for available in available_spaces:
      board[int(available)] = self.ai
      if self.game_over(board):
        optimal_move = int(available)
        board[int(available)] = available
        return optimal_move
      else:
        board[int(available)] = self.human
        if self.game_over(board):
          optimal_move = int(available)
          board[int(available)] = available
          return optimal_move
        else:
          board[int(available)] = available

    if optimal_move:
      return optimal_move
    else:
      return int(available_spaces[0])

  def three_in_row(self, *args):
    return args[0] == args[1] == args[2] == "X" or \
        args[0] == args[1] == args[2] == "O"

  def game_over(self, b):
    return self.three_in_row(b[0], b[1], b[2]) == 1 or \
        self.three_in_row(b[3], b[4], b[5]) == 1 or \
        self.three_in_row(b[6], b[7], b[8]) == 1 or \
        self.three_in_row(b[0], b[3], b[6]) == 1 or \
        self.three_in_row(b[1], b[4], b[7]) == 1 or \
        self.three_in_row(b[2], b[5], b[8]) == 1 or \
        self.three_in_row(b[0], b[4], b[8]) == 1 or \
        self.three_in_row(b[2], b[4], b[6]) == 1

  def draw(self, b):
    return len([s for s in b if s == "X" or s == "O"]) == 9

if __name__ == '__main__':
  game = Game()
  game.start_game()


Currently I do want to fix the Draw portion where I would print Draw instead of game-over when it is a draw.
Design

I'm guessing you're coming from some sort of Object-Oriented Programming Language (OOPL), like Java. Python is an interpreted language, and while treating it like an OOPL isn't bad in many large scale scenarios, something small like this doesn't need that design. If this is for some kind of scholastic exercise or you're just training OOPL design in Python, you can stick to it, but personally I'd avoid it here.

Printing &amp; Strings

Pasting out each array index isn't necessary. Instead of using the modulo operator to format strings, you can either use .format() or f-strings. For the board prompt I'd personally use .format(), like so:

print(' {} | {} | {} \n===+===+===\n {} | {} | {} \n===+===+===\n {} | {} | {} \n'.format(*list(n for n in self.board)))


You use variadic assignment in three_in_row, so I'm assuming you know the four use cases of the asterisk.

I notice you use double quotes a lot in your main code but not in the actual main function, so maybe that was pregenerated by an editor like VSCode? Regardless, you can use single quotes more throughout your code.

Game &amp; Logic

None checks and usage seem to be quite frequent throughout this program. Again, this indicates to me some beginner OOP ideology. We all start somewhere, but as it is in good OOP design to not explicitly use null where possible, the same can apply to None here.

In your eval_board function, you prioritize the fourth, or central, spot for the AI.  Why not just check this outside of the loop, and then start it if it fails? This function appears to be used exclusively by the AI as well, so why isn't it named something like ai_move or ai_eval to reflect this? Other functions could benefit from this naming as well. Also, using while loops and checking for a None spot seems like a bad substitution for Boolean functions and checks in my mind.

Conveniently, I've also put up a Tic Tac Toe question, which is here: Back to Basics - Tic Tac Toe

Any comments I'd have on optimizing your game logic would just be incorporating some of the techniques used in my implementation. Granted, I don't have an "AI," but the imagination shouldn't need to flex over the available possibilities.