I am making a to-do tree application (Earlybird) in Python that will show checkable tasks/subtasks in a tree view. It includes higher-level groups of tasks grouped into blocks (e.g., Work block, Home block) that do not have checkboxes.
It looks like this:

This is is my first application using Python, and object-oriented programming in general. I am usually a Matlab programmer. Hence, feedback on any level of what I've done would be very helpful. I'm about to put the application on Github, and add a ton of features, so now is the perfect time to throw some dynamite at it.
The application has two main classes:
- EarlybirdTree: the core tree view. This is subclassed from- QTreeView, and defines basic methods (loading and saving files, adding edited items to the undo stack, etc)..
- EarlybirdMain: a simple wrapper for- EarlybirdTreeobjects. This is subclassed from- QMainWindow, and allows the user to interact with the tree's methods using menus and toolbars.
The following code includes the above two py files, as well as an earlybird data file (testFile.eb). I store the data in json format. Each main function also adds a separate view of the undostack, just for convenience. Note to keep this from blowing up, I haven't included the functionality for adding/removing items and many other bells and whistles.
earlybirdTree.py
# -*- coding: utf-8 -*-
"""
earlybirdTree.py
    Defines the EarlyBirdTree class, a QTreeView subclass that displays a 
    custom QStandardItemModel as a simple to-do tree. The data is saved
    as a custom json file.
"""
import sys
import os
import json
from PySide import QtGui, QtCore
class StandardItemModel(QtGui.QStandardItemModel):
    '''Items will emit this signal when edited'''
    itemDataChanged = QtCore.Signal(object, object, object, object)
class StandardItem(QtGui.QStandardItem):
    ''''Subclass QStandardItem to reimplement setData to emit itemDataChanged'''
    def setData(self, newValue, role=QtCore.Qt.UserRole + 1):
        #print "setData called with role ", role  #for debugging
        if role == QtCore.Qt.EditRole:
            oldValue = self.data(role)
            QtGui.QStandardItem.setData(self, newValue, role)
            model = self.model()
            if model is not None and oldValue != newValue:
                model.itemDataChanged.emit(self, oldValue, newValue, role)
            return True
        if role == QtCore.Qt.CheckStateRole:
            oldValue = self.data(role)
            QtGui.QStandardItem.setData(self, newValue, role)            
            model = self.model()
            if model is not None and oldValue != newValue:                             
                model.itemDataChanged.emit(self, oldValue, newValue, role)
            return True
        QtGui.QStandardItem.setData(self, newValue, role)
class EarlybirdTree(QtGui.QTreeView):
    '''The earlyBird to do tree view, the core class for the application.'''
    def __init__(self, parent=None, filename = None):
        QtGui.QTreeView.__init__(self, parent=None)
        self.parent = parent
        self.filename = filename
        self.model = StandardItemModel()
        self.rootItem = self.model.invisibleRootItem()        
        self.setModel(self.model)
        self.makeConnections()
        self.undoStack = QtGui.QUndoStack(self)
        self.setStyleSheet("QTreeView::item:hover{background-color:#999966;}")  
        self.headerLabels = ["Task"]
        self.model.setHorizontalHeaderLabels(self.headerLabels) 
        if self.filename:
            self.loadEarlybirdFile(self.filename)
    def makeConnections(self):
        '''Connect all the signals-slots needed.'''
        self.model.itemDataChanged.connect(self.itemDataChangedSlot)
    def itemDataChangedSlot(self, item, oldValue, newValue, role):
        '''Slot used to push changes of existing items onto undoStack'''
        if role == QtCore.Qt.EditRole:
            command = CommandTextEdit(self, item, oldValue, newValue,
                "Text changed from '{0}' to '{1}'".format(oldValue, newValue))
            self.undoStack.push(command)
            return True
        if role == QtCore.Qt.CheckStateRole:
            command = CommandCheckStateChange(self, item, oldValue, newValue, 
                "CheckState changed from '{0}' to '{1}'".format(oldValue, newValue))
            self.undoStack.push(command)
            return True  
    def clearModel(self):
        '''Clears data from model,clearing the view, but repopulates headers/root.
        Used whenever an .eb file is loaded, or newFile method instantiated'''
        self.model.clear()
        self.model.setHorizontalHeaderLabels(self.headerLabels)
        self.rootItem = self.model.invisibleRootItem()   
    def newFile(self):
        '''Creates blank tree'''
        if not self.undoStack.isClean() and self.saveCheck():
            self.saveTodoData()
        self.filename = None
        self.clearModel()
        self.undoStack.clear()
    def closeEvent(self, event):
        '''Typically closeevent is called by a QMainWindow wrapper, 
        but sometimes we do view these guys standalone'''
        if not self.undoStack.isClean() and self.saveCheck():
            self.fileSave()
        self.close()     
    '''
    ***
    Next five methods are part of mechanics for loading .eb files
    ***
    '''
    def loadEarlybirdFile(self, filename = None):
        '''Opens todo tree file (.eb) and populates model with data.'''
        if not self.undoStack.isClean() and self.saveCheck():
            self.saveTodoData() 
        directoryName = os.path.dirname(filename) if filename else "."
        if not filename:
            filename, foo = QtGui.QFileDialog.getOpenFileName(None,
                    "Load earlybird file", directoryName, 
                    "(*.eb)")          
        if filename:
            with open(filename) as f:
                fileData = json.load(f)
            if self.populateModel(fileData, filename):
                self.expandAll()
                self.filename = filename
                self.undoStack.clear()
                return True        
        return False   
    def populateModel(self, fileData, filename):
        '''Verify that top-level items are blocks, and call methods to load data.'''
        if "taskblocks" not in fileData:
            print "Warning: Cannot load {0}.\n"\
                  "Top level must contain taskblocks.".format(filename)
            return False 
        if "tasks" in fileData:
            print "Warning: only reads taskblocks from top level.\n"\
                  "Igorning top-level tasks in {0}.".format(filename)
        taskblockList = fileData["taskblocks"]
        self.clearModel()
        return self.loadTaskblocks(taskblockList)
    def loadTaskblocks(self, taskblockList):  
        '''Load task blocks into the model'''
        for (blockNum, taskblock) in enumerate(taskblockList): 
            blockNameItem = StandardItem(taskblock["blockname"])       
            self.rootItem.appendRow(blockNameItem)
            if "tasks" in taskblock:
                taskList = taskblock["tasks"]
                self.loadTasks(taskList, blockNameItem) 
        return True      
    def loadTasks(self, taskList, parentItem):
        '''Recursively load tasks until we hit a base task (a task w/o any subtasks).'''
        for (taskNum, task) in enumerate(taskList):
            taskNameItem = StandardItem(task["name"])
            taskNameItem.setCheckable(True)
            #print "task and done", task["name"], task["done"]
            if task["done"]:
                taskNameItem.setCheckState(QtCore.Qt.Checked)           
            else:
                taskNameItem.setCheckState(QtCore.Qt.Unchecked)
            parentItem.appendRow(taskNameItem) #add children only to column 0  
            if "tasks" in task:
                subtaskList = task["tasks"]
                return self.loadTasks(subtaskList, taskNameItem) 
    '''
    ****
    Next seven methods are part of the saving mechanics
    ***
    '''
    def saveCheck(self):
        '''If the document has been changed since last clean state, ask if the user
        wants to save the changes.'''
        if QtGui.QMessageBox.question(self,
                "Earlybird save check",
                "Save unsaved changes first?",
                QtGui.QMessageBox.Yes|QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
            return True
        else:
            return False
    def saveTodoData(self): 
        '''Save data from the tree in json format'''
        if self.filename:
            dictModel = self.modelToDict()
            with open(self.filename, 'w') as fileToWrite:
                json.dump(dictModel, fileToWrite, indent=2)
        else:
            self.saveTodoDataAs()
        self.undoStack.clear()
    def saveTodoDataAs(self):
        '''Save data in model as...x'''
        dir = os.path.dirname(self.filename) if self.filename is not None else "."
        self.filename, flt = QtGui.QFileDialog.getSaveFileName(None,
                "EarlyBird: Load data file", dir, "EarlyBird data (*.eb)")           
        if self.filename:
            print "Saving: ", self.filename #for debugging
            dictModel = self.modelToDict()
            with open(self.filename, 'w') as fileToWrite:
                json.dump(dictModel, fileToWrite, indent=2)
        self.undoStack.clear()
    def modelToDict(self):  #def modelToDict(self, parentItem = self.rootItem):
        '''Takes model presently in view, and saves all data as dictionary.
        Called by self.saveTodoData() and self.saveTodoDataAs()'''
        dictModel = {}       
        if self.rootItem.rowCount():           
            dictModel["taskblocks"]= self.createTaskblockList(self.rootItem)
            return dictModel
    def createTaskblockList(self, parentItem):
        '''Creates list of task blocks, and their tasks (latter using createTasklist).
        Called by modelToDict which is used to save the model as a dictionary'''
        numChildren = parentItem.rowCount()
        if numChildren:
            taskblockList = [None] * numChildren
            childList = self.getChildren(parentItem)
            for childNum in range(numChildren):
                childItem = childList[childNum]
                childTaskblockData = {}
                childTaskblockData["blockname"]=childItem.text()               
                #now see if the block has children (tasks)
                if childItem.rowCount():
                    childTaskblockData["tasks"] = self.createTaskList(childItem)
                taskblockList[childNum] = childTaskblockData
            return taskblockList
        else:
            return None
    def createTaskList(self, parentItem):
        '''Recursively traverses model creating list of tasks to
        be saved as json'''
        numChildren = parentItem.rowCount()
        if numChildren:
            taskList = [None] * numChildren
            childList = self.getChildren(parentItem)
            for childNum in range(numChildren):
                childItem = childList[childNum]
                childTaskData = {}
                childTaskData["name"] = childItem.text()
                childTaskData["done"] = True if childItem.checkState() else False
                #now see if the present child has children
                if childItem.rowCount():
                    childTaskData["tasks"] = self.createTaskList(childItem)
                taskList[childNum] = childTaskData
            return taskList
        else:
            return None
    def getChildren(self, parentItem):
        '''Returns list of child items of parentItem. Used when converting
        model to dictionary for saving as json'''
        numChildren = parentItem.rowCount()
        if numChildren > 0:
            childItemList = [None] * numChildren
            for childNum in range(numChildren):
                childItemList[childNum] = parentItem.child(childNum, 0)
        else:
            childItemList = None
        return childItemList
class CommandTextEdit(QtGui.QUndoCommand):
    '''Command for undoing/redoing text edit changes, to be placed in undostack'''
    def __init__(self, earlybirdTree, item, oldText, newText, description):
        QtGui.QUndoCommand.__init__(self, description)
        self.item = item
        self.tree = earlybirdTree
        self.oldText = oldText
        self.newText = newText
    def redo(self):      
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setText(self.newText)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 
    def undo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setText(self.oldText)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 
class CommandCheckStateChange(QtGui.QUndoCommand):
    '''Command for undoing/redoing check state changes, to be placed in undostack'''
    def __init__(self, earlybirdTree, item, oldCheckState, newCheckState, description):
        QtGui.QUndoCommand.__init__(self, description)
        self.item = item
        self.tree = earlybirdTree
        self.oldCheckState = QtCore.Qt.Unchecked if oldCheckState == 0 else QtCore.Qt.Checked
        self.newCheckState = QtCore.Qt.Checked if oldCheckState == 0 else QtCore.Qt.Unchecked
    def redo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setCheckState(self.newCheckState)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 
    def undo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot)
        self.item.setCheckState(self.oldCheckState)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 
def main():
    ebApp = QtGui.QApplication(sys.argv)
    firstEb = EarlybirdTree(filename = "testFile.eb")
    firstEb.show()
    undoView = QtGui.QUndoView(firstEb.undoStack)
    undoView.show()
    sys.exit(ebApp.exec_())
if __name__ == "__main__":
    main()
earlybirdMain.py
# -*- coding: utf-8 -*-
"""
earlybirdMain.py: 
    A wrapper for the EarlybirdTree class (defined in earlybirdTree.py).
    The wrapper allows for simple menu/toolbar-based user interaction
    with an earlybird to do tree. Includes undo/redo functionality.
"""
import sys
import os
from PySide import QtGui, QtCore
from earlybirdTree import EarlybirdTree
class EarlybirdMain(QtGui.QMainWindow):
    '''Main window to wrap an EarlybirdTree'''
    def __init__(self, filename = None):
        QtGui.QMainWindow.__init__(self)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 
        self.view = EarlybirdTree(self, filename) 
        self.model = self.view.model
        self.windowTitleSet()
        self.setCentralWidget(self.view)
        self.createStatusBar()
        self.createActions()
        self.createToolbars()
        self.createMenus()
    def createToolbars(self):
        '''Create toolbars for actions on files and items'''
        self.fileToolbar = self.addToolBar("File actions")
        self.fileToolbar.addAction(self.fileNewAction)
        self.fileToolbar.addAction(self.fileOpenAction)
        self.fileToolbar.addAction(self.fileSaveAction)
        self.fileToolbar.addAction(self.fileSaveAsAction)
        self.itemToolbar = self.addToolBar("Item actions")
        self.itemToolbar.addAction(self.undoAction)
        self.itemToolbar.addAction(self.redoAction)
    def closeEvent(self, event):
        '''If data has been changed, ask user if they want to save it'''
        if not self.view.undoStack.isClean() and self.view.saveCheck():
            self.view.fileSave()
        self.close()
    def createMenus(self):
        '''Create menu for actions on files'''
        self.fileMenu = self.menuBar().addMenu("&File")
        self.fileMenu.addAction(self.fileOpenAction)    
        self.fileMenu.addAction(self.fileNewAction)
        self.fileMenu.addAction(self.fileSaveAction)
        self.fileMenu.addAction(self.fileSaveAsAction)
    def createActions(self):
        '''Create all actions to be used in toolbars/menus: calls createAction()'''
        #File actions
        self.fileNewAction = self.createAction("&New", slot = self.newFile,
                shortcut = QtGui.QKeySequence.New, tip = "New file",
                status = "Create a new file")
        self.fileOpenAction = self.createAction("&Open...", slot = self.fileOpen,
                shortcut = QtGui.QKeySequence.Open, tip = "Open file",
                status = "Open an existing earlybird tree")
        self.fileSaveAction = self.createAction("&Save", slot = self.fileSave,
                shortcut = QtGui.QKeySequence.Save, tip = "Save file",
                status = "Save file")
        self.fileSaveAsAction = self.createAction("Save &As", slot = self.fileSaveAs,
                shortcut = QtGui.QKeySequence.SaveAs, tip = "Save file as", status = "Save file as")
        #Item actions
        self.undoAction = self.createAction("Undo", slot = self.view.undoStack.undo,
               shortcut = QtGui.QKeySequence.Undo, tip = "Undo",
               status = "Undo changes")    
        self.redoAction = self.createAction("Redo", slot = self.view.undoStack.redo,
               shortcut = QtGui.QKeySequence.Redo, tip = "Redo",
               status = "Redo changes")
    def createAction(self, text, slot=None, shortcut=None, 
                     tip=None, status = None):
        '''Function called to create each individual action'''
        action = QtGui.QAction(text, self)
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
        if status is not None:
            action.setStatusTip(status)
        if slot is not None:
            action.triggered.connect(slot)
        return action 
    def createStatusBar(self):                          
        self.status = self.statusBar()
        self.status.setSizeGripEnabled(False)
        self.status.showMessage("Ready")        
    def fileSaveAs(self):
        self.view.saveTodoDataAs()
        self.windowTitleSet()
    def fileSave(self):
        if self.view.filename:        
            self.view.saveTodoData()
        else:
            self.view.saveTodoDataAs()
            self.windowTitleSet()
    def fileOpen(self):
        '''Load earlybird file from memory.'''
        if self.view.loadEarlybirdFile():
            self.model = self.view.model  
            self.windowTitleSet()
            if self.view.filename:
                filenameNopath = QtCore.QFileInfo(self.view.filename).fileName()
                self.status.showMessage("Opened file: {0}".format(filenameNopath))
    def newFile(self):
        '''Opens new blank earlybird file'''
        self.view.newFile()
        self.windowTitleSet()
    def windowTitleSet(self):
        '''Displays filename as window title, if it exists.'''
        if self.view.filename:
            self.setWindowTitle("Earlybird - {}[*]".format(os.path.basename(self.view.filename)))
        else:
            self.setWindowTitle("Earlybird - <untitled>")
def main():   
    ebApp = QtGui.QApplication(sys.argv)
    mainEb = EarlybirdMain(filename = None)#"simpleTodo.eb"
    mainEb.show()
    undoView = QtGui.QUndoView(mainEb.view.undoStack)
    undoView.show()
    sys.exit(ebApp.exec_())
if __name__ == "__main__":
    main()
testFile.eb
{
  "taskblocks": [
    {
      "tasks": [
        {
          "done": false, 
          "name": "Rake leaves"
        }, 
        {
          "done": true, 
          "name": "Eat dinner"
        }
      ], 
      "blockname": "Home"
    }, 
    {
      "tasks": [
        {
          "done": false, 
          "name": "Analysis"
        }, 
        {
          "tasks": [
            {
              "done": false, 
              "name": "Start Github project"
            }, 
            {
              "done": true, 
              "name": "Write readme.md"
            }, 
            {
              "done": false, 
              "name": "Implement functions"
            }
          ], 
          "done": false, 
          "name": "Graphing project"
        }
      ], 
      "blockname": "Work"
    }
  ]
}
The toughest part of the application to figure out was the undo/redo functionality. I have asked three questions about this at SO before finally settling on the strategy above.

