"Tic Tac Toe" game in Python
I see a few areas where you can improve.


You don't need parenthesis around the expressions you and or return:

def is_valid_move(x, y, board):
    return 0 &lt;= x &lt;= 2 and 0 &lt;= y &lt;= 2 and board[x][y] == '_'


Sometimes you do need parenthesis, but only if the operator precedence is wrong otherwise. I.e A and B or C is different from A and (B or C).
Your check_win function is very hard to debug. How sure are you that you did not mess up any of the indices while copy&amp;pasting, and how long would it take you to check that you didn't?

Instead divide the function into sub-responsibilities, which you can name and reason about individually:

def all_equal(it):
    it = iter(it)
    first = next(it)
    return all(x == first for x in it)

def check_rows(board):
    for row in board:
        if row[0] != "_" and all_equal(row):
            return True
    return False

def check_cols(board):
    for col in zip(*board):  # transpose the board
        if col[0] != "_" and all_equal(col):
            return True
    return False

def check_diagonals(board):
    if board[0][0] != "_" and all_equal(board[i][i] for i in range(len(board))):
        return True
    if board[0][2] != "_" and all_equal([board[0][2], board[1][1], board[2][0]]):
        return True
    return False

def check_win(board):
    return check_rows(board) or check_cols(board) or check_diagonals(board)


While this code is longer, it should also be more readable. Also, this way only the diagonal check is still complicated to check for correctness, but at least it is localized.
Your get_input_from_user function can be shortened a bit, at the cost of some readability (at least until you get used to functional programming):

def get_input_from_user(message: str):
    return tuple(map(int, input(message).split()))

Don't iterate over the indices, if what you really want to do is iterate over the elements:

def print_board(board):
    for row in board:
        print(row)

Your main loop and the make_move function. The make_move function has two almost identical branches, the only difference is the player mark. So why not pass that instead of the index? In your main loop you can also change that to just swap between the two players, which lets you use a for loop which is automatically restricted to n iterations:

def make_move(player, board):
    x, y = get_input_from_user(f"Player {player}, please enter a move:")
    while not is_valid_move(x, y, board):
        x,y = get_input_from_user("Invalid move, please try again:")
    board[x][y] = player

if __name__ == '__main__':
    players = "X", "O"
    board = [['_'] * 3 for _ in range(3)]
    for _ in range(9):
        make_move(players[0], board)
        print_board(board)
        if check_win(board):
            print(f"Player {players[0]} is the Winner!")
            break
        players = players[::-1]  # equivalent to players[0], players[1] = players[1], players[0]
    else:    # only runs if no `break` broke the loop
        print("Its a Draw!")


I also used some f-strings for formatting, multiplication of lists, and the else clause of loops to detect a draw.
Python has an official style-guide, PEP8. It recommends always puting a space after commas in parameter lists, etc.