diff --git a/qt/python/mantidqt/widgets/codeeditor/execution.py b/qt/python/mantidqt/widgets/codeeditor/execution.py
index 7367bcaa75f66d3940ad913e292b6bf6da386d95..e276ed73d1ecff8ec25e1bac79ae845d3cbcc06e 100644
--- a/qt/python/mantidqt/widgets/codeeditor/execution.py
+++ b/qt/python/mantidqt/widgets/codeeditor/execution.py
@@ -35,11 +35,10 @@ class PythonCodeExecution(QObject):
     sig_exec_error = Signal(object)
     sig_exec_progress = Signal(int)
 
-    def __init__(self, filename=None):
+    def __init__(self):
         """Initialize the object"""
         super(PythonCodeExecution, self).__init__()
 
-        self._filename = '<string>' if filename is None else filename
         self._globals_ns, self._locals_ns = None, None
         self.reset_context()
 
@@ -63,31 +62,33 @@ class PythonCodeExecution(QObject):
         # create new context for execution
         self._globals_ns, self._locals_ns = {}, {}
 
-    def execute_async(self, code_str):
+    def execute_async(self, code_str, filename=None):
         """
         Execute the given code string on a separate thread. This function
         returns as soon as the new thread starts
 
         :param code_str: A string containing code to execute
-        :param user_globals: A mutable mapping type to store global variables
-        :param user_locals: A mutable mapping type to store local variables
+        :param filename: See PythonCodeExecution.execute()
         :returns: The created async task
         """
         # Stack is chopped on error to avoid the  AsyncTask.run->self.execute calls appearing
         # as these are not useful for the user in this context
-        t = AsyncTask(self.execute, args=(code_str,),
+        t = AsyncTask(self.execute, args=(code_str,filename),
                       stack_chop=2,
                       success_cb=self._on_success, error_cb=self._on_error)
         t.start()
         return t
 
-    def execute(self, code_str):
+    def execute(self, code_str, filename=None):
         """Execute the given code on the calling thread
         within the provided context.
 
         :param code_str: A string containing code to execute
+        :param filename: An optional identifier specifying the file source of the code. If None then '<string>'
+        is used
         :raises: Any error that the code generates
         """
+        filename = '<string>' if filename is None else filename
         compile(code_str, self.filename, mode='exec')
 
         sig_progress = self.sig_exec_progress
diff --git a/qt/python/mantidqt/widgets/codeeditor/interpreter.py b/qt/python/mantidqt/widgets/codeeditor/interpreter.py
index 4601c8673e66eae59ebf2d2846a42ccc13e95529..8e0fbbfaa2a6dbc057d8a36d92efb44d6de58fc2 100644
--- a/qt/python/mantidqt/widgets/codeeditor/interpreter.py
+++ b/qt/python/mantidqt/widgets/codeeditor/interpreter.py
@@ -22,7 +22,7 @@ import sys
 # 3rd party imports
 from qtpy.QtCore import QObject, Signal
 from qtpy.QtGui import QColor, QFontMetrics
-from qtpy.QtWidgets import QStatusBar, QVBoxLayout, QWidget
+from qtpy.QtWidgets import QFileDialog, QMessageBox, QStatusBar, QVBoxLayout, QWidget
 
 # local imports
 from mantidqt.widgets.codeeditor.editor import CodeEditor
@@ -39,9 +39,61 @@ CURRENTLINE_BKGD_COLOR = QColor(247, 236, 248)
 TAB_WIDTH = 4
 
 
+class EditorIO(object):
+
+    def __init__(self, editor):
+        self.editor = editor
+
+    def ask_for_filename(self):
+        filename, _ = QFileDialog.getSaveFileName(self.editor, "Choose filename...")
+        return filename
+
+    def save_if_required(self, confirm=True):
+        """Asks the user if the contents should be saved.
+
+        :param confirm: If True then show a confirmation dialog first to check we should save
+        :returns: True if either saving was successful or no save was requested. Returns False if
+        the operation should be cancelled
+        """
+        if confirm:
+            button = QMessageBox.question(self.editor, "",
+                                          "Save changes to document before closing?",
+                                          buttons=(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel),
+                                          defaultButton=QMessageBox.Cancel)
+            if button == QMessageBox.Yes:
+                return self.write()
+            elif button == QMessageBox.No:
+                return True
+            else:
+                # Cancelled
+                return False
+        else:
+            return self.write()
+
+    def write(self):
+        filename = self.editor.fileName()
+        if not filename:
+            filename = self.ask_for_filename()
+            if not filename:
+                return False
+            self.editor.setFileName(filename)
+
+        try:
+            with open(filename, 'w') as f:
+                f.write(self.editor.text())
+            self.editor.setModified(False)
+        except IOError as exc:
+            QMessageBox.warning(self.editor, "",
+                                "Error while saving '{}': {}".format(filename, str(exc)))
+            return False
+        else:
+            return True
+
+
 class PythonFileInterpreter(QWidget):
 
     sig_editor_modified = Signal(bool)
+    sig_filename_modified = Signal(str)
 
     def __init__(self, content=None, filename=None,
                  parent=None):
@@ -60,26 +112,43 @@ class PythonFileInterpreter(QWidget):
         layout.addWidget(self.status)
         self.setLayout(layout)
         layout.setContentsMargins(0, 0, 0, 0)
-        self._setup_editor(content)
+        self._setup_editor(content, filename)
 
-        self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(filename))
+        self._presenter = PythonFileInterpreterPresenter(self,
+                                                         PythonCodeExecution())
 
         self.editor.modificationChanged.connect(self.sig_editor_modified)
+        self.editor.fileNameChanged.connect(self.sig_filename_modified)
 
     @property
     def filename(self):
-        return self._presenter.model.filename
+        return self.editor.fileName()
+
+    def confirm_close(self):
+        """Confirm the widget can be closed. If the editor contents are modified then
+        a user can interject and cancel closing.
+
+        :return: True if closing was considered successful, false otherwise
+        """
+        return self.save(confirm=True)
 
     def execute_async(self):
         self._presenter.req_execute_async()
 
+    def save(self, confirm=False):
+        if self.editor.isModified():
+            io = EditorIO(self.editor)
+            return io.save_if_required(confirm)
+        else:
+            return True
+
     def set_editor_readonly(self, ro):
         self.editor.setReadOnly(ro)
 
     def set_status_message(self, msg):
         self.status.showMessage(msg)
 
-    def _setup_editor(self, default_content):
+    def _setup_editor(self, default_content, filename):
         editor = self.editor
 
         # use tabs not spaces for indentation
@@ -95,9 +164,14 @@ class PythonFileInterpreter(QWidget):
         font_metrics = QFontMetrics(self.font())
         editor.setMarginWidth(1, font_metrics.averageCharWidth()*3 + 12)
 
-        # fill with content if supplied
+        # fill with content if supplied and set source filename
         if default_content is not None:
             editor.setText(default_content)
+        if filename is not None:
+            editor.setFileName(filename)
+            editor.setModified(False)
+        else:
+            editor.setModified(True)
 
 
 class PythonFileInterpreterPresenter(QObject):
@@ -138,7 +212,7 @@ class PythonFileInterpreterPresenter(QObject):
             return
         self.view.set_editor_readonly(True)
         self.view.set_status_message(RUNNING_STATUS_MSG)
-        return self.model.execute_async(code_str)
+        return self.model.execute_async(code_str, self.view.filename)
 
     def _get_code_for_execution(self):
         editor = self.view.editor
diff --git a/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py b/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py
index 765e4eeed0c64568e21d770daca714826754558b..77624eb813af003824cb3e4dc2cbe0b36bbebdc9 100644
--- a/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py
+++ b/qt/python/mantidqt/widgets/codeeditor/multifileinterpreter.py
@@ -16,16 +16,26 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import (absolute_import, unicode_literals)
 
+# std imports
+import os.path as osp
+
 # 3rd party imports
 from qtpy.QtWidgets import QTabWidget, QVBoxLayout, QWidget
 
 # local imports
 from mantidqt.widgets.codeeditor.interpreter import PythonFileInterpreter
 
-NEW_TAB_TITLE = 'temp.py'
+NEW_TAB_TITLE = 'New'
 MODIFIED_MARKER = '*'
 
 
+def _tab_title_and_toolip(filename):
+    if filename is None:
+        return NEW_TAB_TITLE, NEW_TAB_TITLE
+    else:
+        return osp.basename(filename), filename
+
+
 class MultiPythonFileInterpreter(QWidget):
     """Provides a tabbed widget for editing multiple files"""
 
@@ -35,41 +45,85 @@ class MultiPythonFileInterpreter(QWidget):
         # attributes
         self.default_content = default_content
 
-        # layout
+        # widget setup
         self._tabs = QTabWidget(self)
         self._tabs.setMovable(True)
+        self._tabs.setTabsClosable(True)
+        self._tabs.tabCloseRequested.connect(self.close_tab)
         layout = QVBoxLayout()
         layout.addWidget(self._tabs)
         self.setLayout(layout)
         layout.setContentsMargins(0, 0, 0, 0)
 
         # add a single editor by default
-        self.append_new_editor()
+        self.append_new_editor(default_content)
 
     @property
     def editor_count(self):
         return self._tabs.count()
 
-    def append_new_editor(self):
-        interpreter = PythonFileInterpreter(self.default_content,
+    def append_new_editor(self, content=None, filename=None):
+        interpreter = PythonFileInterpreter(content, filename=filename,
                                             parent=self._tabs)
-        interpreter.sig_editor_modified.connect(self.update_tab_title)
-        self._tabs.addTab(interpreter, NEW_TAB_TITLE)
-        self.update_tab_title(modified=True)
+        # monitor future modifications
+        interpreter.sig_editor_modified.connect(self.mark_current_tab_modified)
+        interpreter.sig_filename_modified.connect(self.on_filename_modified)
+
+        tab_title, tab_toolip = _tab_title_and_toolip(filename)
+        tab_idx = self._tabs.addTab(interpreter, tab_title)
+        self.mark_tab_modified(tab_idx, (filename is None))
+        self._tabs.setTabToolTip(tab_idx, tab_toolip)
+        return tab_idx
+
+    def close_tab(self, idx):
+        """Close the tab at the given index."""
+        if idx >= self.editor_count:
+            return
+        editor = self.editor_at(idx)
+        if editor.confirm_close():
+            self._tabs.removeTab(idx)
+
+        # we never want an empty widget
+        if self.editor_count == 0:
+            self.append_new_editor(content=self.default_content)
 
     def current_editor(self):
         return self._tabs.currentWidget()
 
+    def editor_at(self, idx):
+        """Return the editor at the given index. Must be in range"""
+        return self._tabs.widget(idx)
+
     def execute_current(self):
         """Execute content of the current file. If a selection is active
         then only this portion of code is executed"""
         self.current_editor().execute_async()
 
-    def update_tab_title(self, modified):
+    def on_filename_modified(self, filename):
+        title, tooltip = _tab_title_and_toolip(filename)
+        idx_cur = self._tabs.currentIndex()
+        self._tabs.setTabText(idx_cur, title)
+        self._tabs.setTabToolTip(idx_cur, tooltip)
+
+    def open_file_in_new_tab(self, filepath):
+        """Open the existing file in a new tab in the editor
+
+        :param filepath: A path to an existing file
+        """
+        with open(filepath, 'r') as code_file:
+            content = code_file.read()
+        self._tabs.setCurrentIndex(self.append_new_editor(content=content,
+                                                          filename=filepath))
+
+    def mark_current_tab_modified(self, modified):
         """Update the current tab title to indicate that the
         content has been modified"""
-        idx_cur = self._tabs.currentIndex()
-        title_cur = self._tabs.tabText(idx_cur)
+        self.mark_tab_modified(self._tabs.currentIndex(), modified)
+
+    def mark_tab_modified(self, idx, modified):
+        """Update the tab title to indicate that the
+        content has been modified or not"""
+        title_cur = self._tabs.tabText(idx)
         if modified:
             if not title_cur.endswith(MODIFIED_MARKER):
                 title_new = title_cur + MODIFIED_MARKER
@@ -80,4 +134,8 @@ class MultiPythonFileInterpreter(QWidget):
                 title_new = title_cur.rstrip('*')
             else:
                 title_new = title_cur
-        self._tabs.setTabText(idx_cur, title_new)
+        self._tabs.setTabText(idx, title_new)
+
+    def save_current_file(self):
+        """Save the current file"""
+        self.current_editor().save()
diff --git a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py
index 28535d09b573c6fabdbe425b7e4d6238a7076f2a..b8f2c4bb13da35e4184544542bad74a1a3baf426 100644
--- a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py
+++ b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py
@@ -195,9 +195,10 @@ squared = sum*sum
     # -------------------------------------------------------------------------
     # Filename checks
     # -------------------------------------------------------------------------
-    def test_filename_included_in_traceback_if_defined(self):
+    def test_filename_included_in_traceback_if_supplied(self):
         code = """raise RuntimeError"""
-        executor, recv = self._run_async_code(code, filename='test.py')
+        filename = 'test.py'
+        executor, recv = self._run_async_code(code)
         self.assertTrue(recv.error_cb_called)
         self.assertEqual('test.py', recv.error_stack[0][0])
 
@@ -219,10 +220,8 @@ squared = sum*sum
         executor = PythonCodeExecution()
         self.assertRaises(expected_exc_type, executor.execute, code)
 
-    def _run_async_code(self, code, with_progress=False, filename=None):
+    def _run_async_code(self, code, with_progress=False):
         executor = PythonCodeExecution()
-        if filename is not None:
-            executor.filename = filename
         if with_progress:
             recv = ReceiverWithProgress()
             executor.sig_exec_progress.connect(recv.on_progess_update)
diff --git a/qt/python/mantidqt/widgets/src/_widgetscore.sip b/qt/python/mantidqt/widgets/src/_widgetscore.sip
index 7df091cbfb161c9cb3791931457899bc0f013a50..be14405d4f48b7e6aebe3ab59e1ee2076c87cb19 100644
--- a/qt/python/mantidqt/widgets/src/_widgetscore.sip
+++ b/qt/python/mantidqt/widgets/src/_widgetscore.sip
@@ -80,6 +80,7 @@ public:
                           lineTo, indexTo);
 %End
   bool hasSelectedText() const;
+  bool isModified() const;
   bool isReadOnly() const;
   QString selectedText() const;
   QString text() const;
@@ -89,6 +90,7 @@ public:
   void setIndentationsUseTabs(bool tabs);
   void setFileName(const QString &filename);
   void setMarginWidth(int margin, int width);
+  void setModified(bool m);
   void setReadOnly(bool ro);
   void setSelection(int lineFrom, int indexFrom, int lineTo, int indexTo);
   void setTabWidth(int width);
@@ -98,6 +100,7 @@ public slots:
   void updateProgressMarker(int lineno, bool error);
 
 signals:
+  void fileNameChanged(const QString &fileName);
   void modificationChanged(bool m);
 
 private:
diff --git a/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h b/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h
index 24b1fa5839782453a331c982e1af4a5199598fc9..54f39ef8f18b77eced40b10330eac6548505ef15 100644
--- a/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h
+++ b/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h
@@ -102,11 +102,8 @@ public:
   void keyPressEvent(QKeyEvent *event) override;
   /// The current filename
   inline QString fileName() const { return m_filename; }
-  /**
-   * Set a new file name
-   * @param filename :: The new filename
-   */
-  inline void setFileName(const QString &filename) { m_filename = filename; }
+  /// Set a new file name
+  void setFileName(const QString &filename);
 
   /// Override so that ctrl + mouse wheel will zoom in and out
   void wheelEvent(QWheelEvent *e) override;
@@ -127,7 +124,7 @@ public slots:
   /// Set the marker state
   void setMarkerState(bool enabled);
   /// Update the progress marker
-  void updateProgressMarker(int lineno, bool error=false);
+  void updateProgressMarker(int lineno, bool error = false);
   /// Mark the progress arrow as an error
   void markExecutingLineAsError();
   /// Refresh the autocomplete information base on a new set of keywords
@@ -149,6 +146,8 @@ signals:
   void textZoomedIn();
   /// Emitted when a zoom out is requested
   void textZoomedOut();
+  /// Notify that the filename has been modified
+  void fileNameChanged(const QString &fileName);
 
 protected:
   /// Write to the given device
diff --git a/qt/widgets/common/src/ScriptEditor.cpp b/qt/widgets/common/src/ScriptEditor.cpp
index 8c9423ae98ec2aee2f9eee49846cf52036277611..39d16e8317e7d13cd7b1f2f85e7961efdebdc290 100644
--- a/qt/widgets/common/src/ScriptEditor.cpp
+++ b/qt/widgets/common/src/ScriptEditor.cpp
@@ -205,8 +205,8 @@ QSize ScriptEditor::sizeHint() const { return QSize(600, 500); }
 void ScriptEditor::saveAs() {
   QString selectedFilter;
   QString filter = "Scripts (*.py *.PY);;All Files (*)";
-  QString filename = QFileDialog::getSaveFileName(nullptr, "MantidPlot - Save",
-                                                  "", filter, &selectedFilter);
+  QString filename = QFileDialog::getSaveFileName(nullptr, "Save file...", "",
+                                                  filter, &selectedFilter);
 
   if (filename.isEmpty()) {
     throw SaveCancelledException();
@@ -279,6 +279,14 @@ void ScriptEditor::keyPressEvent(QKeyEvent *event) {
   forwardKeyPressToBase(event);
 }
 
+/*
+ * @param filename The new filename
+ */
+void ScriptEditor::setFileName(const QString &filename) {
+  m_filename = filename;
+  emit fileNameChanged(filename);
+}
+
 /** Ctrl + Rotating the mouse wheel will increase/decrease the font size
  *
  */