text: #!/usr/bin/env python3
  # -*- coding: utf-8 -*-
  
  # Ref: http://doc.qt.io/qt-5/modelview.html#2-1-a-read-only-table
  
  import sys
  from PyQt5.QtCore import Qt, QAbstractTableModel, QVariant
  from PyQt5.QtWidgets import QApplication, QTableView
  
  HEADER_LIST = ["String column", "Int column", "Float column", "Boolean column"]
  
  class MyData:
      def __init__(self):
          self._num_rows = 3
          self._num_columns = 4
  
          self._data = [["0", 0, 0.0, True] for i in range(self._num_rows)]   # <- the type of the data here define the delegate that is used : line edit for string, spinbox for int, doublespinbox for float, combobox for boolean
  
      def get_num_rows(self):
          return self._num_rows
  
      def get_num_columns(self):
          return self._num_columns
  
      def get_data(self, row_index, column_index):
          value = self._data[row_index][column_index]
          print("read ({},{}): {}".format(row_index, column_index, value))
          return value
  
      def set_data(self, row_index, column_index, value):
          print("write ({},{}): {}".format(row_index, column_index, value))
          self._data[row_index][column_index] = value
  
  ###############################################################################
  
  class MyModel(QAbstractTableModel):
  
      def __init__(self, data, parent=None):
          super().__init__(parent)
          self._data = data               # DON'T CALL THIS ATTRIBUTE "data", A QAbstractItemModel METHOD ALREADY HAVE THIS NAME (model.data(index, role)) !!!
  
      def rowCount(self, parent):
          return self._data.get_num_rows()
  
      def columnCount(self, parent):
          return self._data.get_num_columns()
  
      def data(self, index, role):
          if role == Qt.DisplayRole or role == Qt.EditRole:           # <--
              # See https://stackoverflow.com/a/8480223
              return self._data.get_data(index.row(), index.column())
          return QVariant()
  
      def headerData(self, index, orientation, role):
          if role == Qt.DisplayRole:
              if orientation == Qt.Horizontal:
                  return HEADER_LIST[index]
              elif orientation == Qt.Vertical:
                  return "{}".format(index)
          return None
  
      def setData(self, index, value, role):
          if role == Qt.EditRole:
  
              try:
                  self._data.set_data(index.row(), index.column(), value)
  
                  # The following line are necessary e.g. to dynamically update the QSortFilterProxyModel
                  self.dataChanged.emit(index, index, [Qt.EditRole])
              except Exception as e:
                  print(e)
                  return False
  
          return True
  
      def flags(self, index):
          return Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled
  
  if __name__ == '__main__':
      app = QApplication(sys.argv)
  
      data = MyData()
  
      table_view = QTableView()
      my_model = MyModel(data)
      table_view.setModel(my_model)
      table_view.show()
  
      # The mainloop of the application. The event handling starts from this point.
      # The exec_() method has an underscore. It is because the exec is a Python keyword. And thus, exec_() was used instead.
      exit_code = app.exec_()
  
      # The sys.exit() method ensures a clean exit.
      # The environment will be informed, how the application ended.
      sys.exit(exit_code)
  