Tic-tac-toe in python with OOP I'm a beginner to intermediate in programming, and created a Tic-tac-toe game in python. I plan to add an AI player and a GUI using pygame.
Any improvements I could make to the code, especially with the logic, or just formatting in general?
my_board = [
    [&quot;1&quot;, &quot;2&quot;, &quot;X&quot;],
    [&quot;4&quot;, &quot;5&quot;, &quot;6&quot;],
    [&quot;7&quot;, &quot;8&quot;, &quot;9&quot;]
]


class Tictactoe():
  def __init__(self, board):
    self.board = board
  
  def print_board(self):
    print(&quot;|-------------|&quot;)
    print(&quot;| Tic Tac Toe |&quot;)
    print(&quot;|-------------|&quot;)
    print(&quot;|             |&quot;)
    print(&quot;|    &quot; + self.board[0][0] + &quot; &quot; + self.board[0][1] + &quot; &quot; + self.board[0][2] + &quot;    |&quot;)
    print(&quot;|    &quot; + self.board[1][0] + &quot; &quot; + self.board[1][1] + &quot; &quot; + self.board[1][2] + &quot;    |&quot;)
    print(&quot;|    &quot; + self.board[2][0] + &quot; &quot; + self.board[2][1] + &quot; &quot; + self.board[2][2] + &quot;    |&quot;)
    print(&quot;|             |&quot;)
    print(&quot;|-------------|&quot;)
    print()

  def available_moves(self):
    moves = []
    for row in self.board:
      for col in row:
        if col != &quot;X&quot; and col != &quot;O&quot;:
          moves.append(int(col))
    return moves

  def select_space(self, move, turn):
    row = (move-1)//3
    col = (move-1)%3
    self.board[row][col] = &quot;{}&quot;.format(turn)

  def has_won(self, player):
    for row in self.board:
        if row.count(player) == 3:
            return True
    for i in range(3):
        if self.board[0][i] == player and self.board[1][i] == player and self.board[2][i] == player:
            return True
    if self.board[0][0] == player and self.board[1][1] == player and self.board[2][2] == player:
        return True
    if self.board[0][2] == player and self.board[1][1] == player and board[2][0] == player:
        return True
    return False
  
  def game_over(self):
    self.available_moves()
    if self.has_won(&quot;X&quot;) or self.has_won(&quot;O&quot;) or len(self.available_moves()) ==0:
      return True
    else:
      return False

  def get_player_move(self, turn):
    move = int(input(&quot;Player {player} make your turn: &quot;.format(player=turn)))
    self.available_moves()
    while move not in range (1,10) or move not in self.available_moves():
      move = int(input(&quot;Invalid move, try again: &quot;))
    return move
      

     
def game(board):
  tictactoe = Tictactoe(my_board)
  while not tictactoe.game_over():
    tictactoe.print_board()
    x_move = tictactoe.get_player_move(&quot;X&quot;)
    tictactoe.select_space(x_move,&quot;X&quot;)
    tictactoe.print_board()
    if tictactoe.game_over():
      break

    o_move = tictactoe.get_player_move(&quot;O&quot;)
    tictactoe.select_space(o_move,&quot;O&quot;)
  if tictactoe.has_won(&quot;X&quot;):
    print(&quot;X wins&quot;)
  elif tictactoe.has_won(&quot;O&quot;):
    print(&quot;O wins&quot;)
  else:
    print(&quot;tie&quot;)



game(my_board)
Indents
PEP 8: Use 4 spaces per indentation level.

Main game loop
The way your game loop is set up now is a bit unelegant. You already implemented all the methods to be able to handle both players, there's only one piece missing:
from itertools import cycle

players = cycle(['X', 'O'])

cycle will provide a generator that infinitely repeats the iterable you pass it. This makes the loop quite a bit cleaner:
while not tictactoe.game_over():
    tictactoe.print_board()

    player = next(players)
    move = tictactoe.get_player_move(player)
    tictactoe.select_space(move, player)

I'd also suggest renaming select_space to something like do_move. Makes it a bit clearer what's going on.

Redundant calls to Tictactoe.available_moves()
In game_over and get_player_move you call self.available_moves() once without using the return value in any way. You can simply delete those lines.

game_over
The pattern
if self.has_won(&quot;X&quot;) or self.has_won(&quot;O&quot;) or len(self.available_moves()) ==0:
    return True
else:
    return False

can be simplified to
return self.has_won(&quot;X&quot;) or self.has_won(&quot;O&quot;) or len(self.available_moves()) == 0

What you were doing is basically manually returning the resulting bool value of a condition. We can instead simply return the condition directly.

print_board
Python lets you easily avoid an unelegant pattern
print(&quot;|    &quot; + self.board[0][0] + &quot; &quot; + self.board[0][1] + &quot; &quot; + self.board[0][2] + &quot;    |&quot;)

by using the * operator for unpacking an iterable
print(&quot;|   &quot;, *self.board[0], &quot;   |&quot;)

Since we need to do this for all rows in self.board we should let Python handle it for us as well:
for row in self.board:
    print(&quot;|   &quot;, *row, &quot;   |&quot;)


has_won
For a cleaner implementation of this method I would suggest implementing helper functions that provide lists of the rows, columns and diagonals of the board. They might also be useful for future feature additions.
def rows(self):
    return self.board[:]  # creates a mutable copy of the rows

def columns(self):
    return [[row[index] for row in self.board] for index in range(3)]

def diagonals(self):
    first = [self.board[index][index] for index in range(3)]
    second = [self.board[index][len(self.board[index]) - index - 1] for index in range(3)]
    return [first, second]

These methods are also ready to be used for boards of different sizes. Simply replace the 3s by a variable.
has_won is now a lot simpler and more readable:
def has_won(self, player):
    for line in self.rows() + self.columns() + self.diagonals():
        symbols = set(line)
        if player in symbols and len(symbols) == 1:
            return True

    return False

We can take this a step further (as pointed out in the comments) and change has_won to get_winner and return the winning symbol. This will also affect some other parts of your code:
class Tictactoe:
def get_winner(self):
    for line in self.rows() + self.columns() + self.diagonals():
        symbols = set(line)
        if len(symbols) == 1:
            return symbols.pop()

    return None


def game_over(self):
    return self.get_winner() is not None or len(self.available_moves()) == 0

Printing our winner in game:
def game(...):
    
    # main gameplay here
    
    if (winner := self.get_winner()) is not None:
        print(f&quot;{winner} wins&quot;)
    else:
        print(&quot;tie&quot;)