diff --git a/gdb/NEWS b/gdb/NEWS
index 4cf91053c95..225ce15df16 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -238,6 +238,11 @@ qExecAndArgs
      a tuple of pairs each representing a single range.  Contiguous blocks
      have only one range.
 
+  ** New event registry gdb.events.corefile_changed, which emits a
+     CorefileChangedEvent whenever the core file associated with an
+     inferior changes.  The event has an 'inferior' attribute which is
+     the gdb.Inferior in which the core file has changed.
+
 * Guile API
 
   ** Procedures 'memory-port-read-buffer-size',
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index e1e983726e8..6c0b3310c7f 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -4147,6 +4147,17 @@ Events In Python
 filename will have changed, but the symbol filename might still hold
 its previous value.
 
+@item events.corefile_changed
+Emits @code{gdb.CorefileChangedEvent} which indicates that the core
+file associated with a @code{gdb.Inferior} has changed, either a new
+core file has been loaded, or the existing core file has been
+unloaded (@pxref{Core Files In Python}).
+
+@defvar CorefileChangedEvent.inferior
+The @code{gdb.Inferior} in which the corefile has changed
+(@pxref{Inferiors In Python}).
+@end defvar
+
 @item events.new_progspace
 This is emitted when @value{GDBN} adds a new program space
 (@pxref{Progspaces In Python,,Program Spaces In Python}).  The event
diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def
index 24724038562..3711cf29287 100644
--- a/gdb/python/py-all-events.def
+++ b/gdb/python/py-all-events.def
@@ -47,3 +47,4 @@ GDB_PY_DEFINE_EVENT(new_progspace)
 GDB_PY_DEFINE_EVENT(free_progspace)
 GDB_PY_DEFINE_EVENT(tui_enabled)
 GDB_PY_DEFINE_EVENT(selected_context)
+GDB_PY_DEFINE_EVENT(corefile_changed)
diff --git a/gdb/python/py-corefile.c b/gdb/python/py-corefile.c
index d35838c7523..4da70d631a4 100644
--- a/gdb/python/py-corefile.c
+++ b/gdb/python/py-corefile.c
@@ -23,6 +23,7 @@
 #include "inferior.h"
 #include "gdbcore.h"
 #include "gdbsupport/rsp-low.h"
+#include "py-event.h"
 
 /* A gdb.Corefile object.  */
 
@@ -320,13 +321,48 @@ cfpy_mapped_files (PyObject *self, PyObject *args)
   return obj->mapped_files;
 }
 
+/* Emit a CorefileChangedEvent event to REGISTRY.  Return 0 on success,
+   or a negative value on error.  INF is the inferior in which the core
+   file changed.  */
+
+static int
+emit_corefile_changed_event (eventregistry_object *registry, inferior *inf)
+{
+  /* If there are no listeners then we are done.  */
+  if (evregpy_no_listeners_p (gdb_py_events.corefile_changed))
+    return 0;
+
+  gdbpy_ref<> event_obj
+    = create_event_object (&corefile_changed_event_object_type);
+  if (event_obj == nullptr)
+    return -1;
+
+  gdbpy_ref<inferior_object> inf_obj = inferior_to_inferior_object (inf);
+  if (inf_obj == nullptr
+      || evpy_add_attribute (event_obj.get (), "inferior",
+			     inf_obj.get ()) < 0)
+    return -1;
+
+  return evpy_emit_event (event_obj.get (), registry);
+}
+
 /* Callback from gdb::observers::core_file_changed.  The core file in
    PSPACE has been changed.  */
 
 static void
 cfpy_corefile_changed (inferior *inf)
 {
+  /* It's safe to do this even if Python is not initialized, but there
+     should be nothing to clear in that case.  */
   cfpy_inferior_corefile_data_key.clear (inf);
+
+  if (!gdb_python_initialized)
+    return;
+
+  gdbpy_enter enter_py;
+
+  if (emit_corefile_changed_event (gdb_py_events.corefile_changed, inf) < 0)
+    gdbpy_print_stack ();
 }
 
 /* Called when a gdb.Corefile is destroyed.  */
diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def
index fe3e0978a55..7dddcc156ec 100644
--- a/gdb/python/py-event-types.def
+++ b/gdb/python/py-event-types.def
@@ -150,3 +150,8 @@ GDB_PY_DEFINE_EVENT_TYPE (selected_context,
 			  "SelectedContextEvent",
 			  "GDB user selected context event object",
 			  event_object_type);
+
+GDB_PY_DEFINE_EVENT_TYPE (corefile_changed,
+			  "CorefileChangedEvent",
+			  "GDB corefile changed event",
+			  event_object_type);
diff --git a/gdb/testsuite/gdb.python/py-corefile.exp b/gdb/testsuite/gdb.python/py-corefile.exp
index ecbff30cf7c..1b7a6458133 100644
--- a/gdb/testsuite/gdb.python/py-corefile.exp
+++ b/gdb/testsuite/gdb.python/py-corefile.exp
@@ -17,6 +17,7 @@
 # support in Python.
 
 require isnative
+require {!is_remote host}
 
 load_lib gdb-python.exp
 
@@ -28,17 +29,93 @@ if {[build_executable "build executable" $testfile $srcfile] == -1} {
     return
 }
 
+set remote_python_file \
+    [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
 set corefile [core_find $binfile]
 if {$corefile == ""} {
     unsupported "couldn't create or find corefile"
     return
 }
 
+# Helper proc to run the 'core-file' command.  Takes optional arguments:
+#
+#   -corefile FILENAME : Load FILENAME as the new core file.  If this
+#                        argument is not given then the current core
+#                        file will be unloaded.
+#
+#   -inferior NUM : The inferior in which the corefile is being changed.
+#                   This is used to match the corefile_changed events
+#                   that will be emitted.
+#
+#   -prefix STRING : A test prefix, to make test names unique.
+proc core_file_cmd { args } {
+    parse_some_args {
+	{corefile ""}
+	{inferior 1}
+	{prefix ""}
+    }
+
+    if { $prefix eq "" } {
+	if { $corefile eq "" } {
+	    set prefix "unload corefile"
+	} else {
+	    set prefix "load corefile"
+	}
+    }
+
+    with_test_prefix $prefix {
+	gdb_test "events corefile_changed check" \
+	    "^No corefile_changed event has been seen\\." \
+	    "no corefile event has been seen"
+
+	gdb_test "events exited check" \
+	    "^No exited event has been seen\\." \
+	    "no exited event has been seen"
+
+	if { $corefile eq "" } {
+	    gdb_test "core-file" "^No core file now\\." "unload current core file"
+
+	    gdb_test "events corefile_changed check" \
+		"Event 1/1, Inferior $inferior, Corefile None" \
+		"expected corefile event has been seen"
+
+	    gdb_test "events exited check" \
+		"Event 1/1, Inferior $inferior, Exit Code None" \
+		"expected exited event has been seen"
+	} else {
+	    gdb_test "core-file $corefile" ".*" \
+		"load core file"
+
+	    gdb_test "events corefile_changed check" \
+		"Event 1/1, Inferior $inferior, Corefile [string_to_regexp $corefile]" \
+		"expected corefile event has been seen"
+
+	    gdb_test "events exited check" \
+		"^No exited event has been seen\\." \
+		"no exited event was emitted"
+	}
+    }
+
+    gdb_test_no_output -nopass "events corefile_changed reset"
+    gdb_test_no_output -nopass "events exited reset"
+}
+
+# A helper proc runs clean_restart passing through ARGS, and then loads the
+# test's Python script.
+proc clean_restart_and_load_py_script { args } {
+    clean_restart {*}$args
+
+    # Load the Python script into GDB.
+    gdb_test "source $::remote_python_file" "^Success" \
+	"source python script"
+}
+
 # Create a copy of the corefile.
 set other_corefile [standard_output_file ${testfile}-other.core]
 remote_exec build "cp $corefile $other_corefile"
 
-clean_restart
+clean_restart_and_load_py_script
 
 gdb_test_no_output "python inf = gdb.selected_inferior()" \
     "capture current inferior"
@@ -46,8 +123,7 @@ gdb_test_no_output "python inf = gdb.selected_inferior()" \
 gdb_test "python print(inf.corefile)" "^None" \
     "Inferior.corefile is None before loading a core file"
 
-gdb_test "core-file $corefile" ".*" \
-    "load core file"
+core_file_cmd -corefile $corefile
 
 set file_re [string_to_regexp $corefile]
 gdb_test "python print(inf.corefile)" "^<gdb\\.Corefile inferior=1 filename='$file_re'>" \
@@ -73,7 +149,7 @@ gdb_test "python print(core1.filename)" "^$file_re" \
 gdb_test "python print(core1.is_valid())" "^True" \
     "Corefile.is_valid() is True while corefile is loaded"
 
-gdb_test "core-file" "^No core file now\\." "unload current core file"
+core_file_cmd
 
 gdb_test "python print(core1.is_valid())" "^False" \
     "Corefile.is_valid() is False after corefile is unloaded"
@@ -101,8 +177,7 @@ gdb_test "add-inferior"
 gdb_test "inferior 2"
 
 with_test_prefix "in second inferior" {
-    gdb_test "core-file $corefile" ".*" \
-	"load core file"
+    core_file_cmd -corefile $corefile -inferior 2
 
     gdb_test "python print(inf.corefile)" "^None" \
 	"first inferior still has no core file"
@@ -128,8 +203,8 @@ gdb_test "python print(core2.filename)" "^$file_re" \
     "Corefile.filename attribute works from different progspace"
 
 # Load the other corefile into the first inferior.
-gdb_test "core $other_corefile" ".*" \
-    "load other corefile into inferior 1"
+core_file_cmd -corefile $other_corefile \
+    -prefix "load other corefile into inferior 1"
 
 # Delete the second inferior.  We need to switch to the second
 # inferior and unload its corefile before we can do that.  Then,
@@ -152,7 +227,7 @@ with_test_prefix "remove second inferior" {
 	"AttributeError.*: 'gdb\\.Corefile' object has no attribute '_my_attribute'" \
 	"try to read attribute that doesn't exist"
 
-    gdb_test "core-file"
+    core_file_cmd -inferior 2
 
     gdb_test "python print(core2.filename)" \
 	[multi_line \
@@ -182,18 +257,10 @@ with_test_prefix "remove second inferior" {
 # mapped_files API.  The output from the built-in command, and the
 # Python command should be identical.
 with_test_prefix "test mapped files data" {
-    clean_restart
-
-    set remote_python_file \
-	[gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
-
-    # Load the Python script into GDB.
-    gdb_test "source $remote_python_file" "^Success" \
-	"source python script"
+    clean_restart_and_load_py_script
 
     # Load the core file.
-    gdb_test "core-file $corefile" ".*" \
-	"load core file"
+    core_file_cmd -corefile $corefile
 
     # Two files to write the output to.
     set out_1 [standard_output_file ${gdb_test_file_name}-out-1.txt]
@@ -279,3 +346,38 @@ with_test_prefix "test mapped files data" {
     gdb_test "python regions\[0\] = None" \
 	"'tuple' object does not support item assignment"
 }
+
+# Load a core file.  GDB should figure out which file is being debugged.
+# Then use 'start' to run this executable, this will replace the core file
+# target.  At least on Linux, this replacement is done without calling
+# target_detach.  This test checks that the expected core file changed and
+# inferior exited events are still seen.
+with_test_prefix "start from corefile" {
+    if { [gdb_protocol_is_native] } {
+	clean_restart_and_load_py_script
+
+	# Load the core file.
+	core_file_cmd -corefile $corefile
+
+	# Check GDB figured out the executable.
+	gdb_test "info inferiors 1" \
+	    [multi_line \
+		 "\[^\r\n\]+[string_to_regexp $binfile]\\s*" \
+		 "\[^\r\n\]+[string_to_regexp $corefile]\\s*"] \
+	    "check executable was detected correctly"
+
+	gdb_test "start" \
+	    "Temporary breakpoint $::decimal, main \\(\\).*" \
+
+	gdb_test "events corefile_changed check" \
+	    "Event 1/1, Inferior 1, Corefile None" \
+	    "expected corefile event has been seen"
+
+	gdb_test "events exited check" \
+	    "Event 1/1, Inferior 1, Exit Code None" \
+	    "expected exited event has been seen"
+
+	gdb_test_no_output -nopass "events corefile_changed reset"
+	gdb_test_no_output -nopass "events exited reset"
+    }
+}
diff --git a/gdb/testsuite/gdb.python/py-corefile.py b/gdb/testsuite/gdb.python/py-corefile.py
index 43b64085117..1aaf15093ac 100644
--- a/gdb/testsuite/gdb.python/py-corefile.py
+++ b/gdb/testsuite/gdb.python/py-corefile.py
@@ -196,4 +196,121 @@ class CheckMainExec(gdb.Command):
 CheckMainExec()
 
 
+# An 'events' prefix command.
+class events_cmd(gdb.Command):
+    """Information about recent Python events."""
+
+    def __init__(self):
+        gdb.Command.__init__(self, "events", gdb.COMMAND_USER, prefix=True)
+
+
+# An 'events corefile_changed' sub-command.
+class events_corefile_changed_cmd(gdb.Command):
+    """Check recent corefile_changed events.
+
+    Requires a single argument either 'check' or 'reset'.  With
+    'check', print details of every recent corefile_changed event.
+    With 'reset' clear the list of recent corefile_changed events."""
+
+    def __init__(self):
+        gdb.Command.__init__(self, "events corefile_changed", gdb.COMMAND_USER)
+        self._events = []
+        gdb.events.corefile_changed.connect(lambda e: self._corefile_changed_handler(e))
+
+    def _corefile_changed_handler(self, event):
+        assert isinstance(event, gdb.CorefileChangedEvent)
+        inf = event.inferior
+        assert isinstance(inf, gdb.Inferior)
+
+        corefile = inf.corefile
+        if corefile is not None:
+            assert corefile.is_valid()
+            corefile = corefile.filename
+
+        obj = {"inferior": inf.num, "corefile": corefile}
+        self._events.append(obj)
+
+    def invoke(self, args, from_tty):
+        if args == "check":
+            if len(self._events) == 0:
+                print("No corefile_changed event has been seen.")
+            else:
+                total = len(self._events)
+                for idx, obj in enumerate(self._events, start=1):
+                    inf_num = obj["inferior"]
+                    corefile = obj["corefile"]
+
+                    if corefile is None:
+                        msg = "None"
+                    else:
+                        msg = corefile
+
+                    print(
+                        "Event {}/{}, Inferior {}, Corefile {}".format(
+                            idx, total, inf_num, msg
+                        )
+                    )
+        elif args == "reset":
+            self._events = []
+        else:
+            raise gdb.GdbError("Unknown command args: {}".format(args))
+
+
+# An 'events exited' sub-command.
+class events_exited_cmd(gdb.Command):
+    """Check recent exited events.
+
+    Requires a single argument either 'check' or 'reset'.  With
+    'check', print details of every recent corefile_changed event.
+    With 'reset' clear the list of recent corefile_changed events."""
+
+    def __init__(self):
+        gdb.Command.__init__(self, "events exited", gdb.COMMAND_USER)
+        self._events = []
+        gdb.events.exited.connect(lambda e: self._exited_handler(e))
+
+    def _exited_handler(self, event):
+        assert isinstance(event, gdb.ExitedEvent)
+        inf = event.inferior
+        assert isinstance(inf, gdb.Inferior)
+
+        if hasattr(event, "exit_code"):
+            assert isinstance(event.exit_code, int)
+            exit_code = event.exit_code
+        else:
+            exit_code = None
+
+        obj = {"inferior": inf.num, "exit_code": exit_code}
+        self._events.append(obj)
+
+    def invoke(self, args, from_tty):
+        if args == "check":
+            if len(self._events) == 0:
+                print("No exited event has been seen.")
+            else:
+                total = len(self._events)
+                for idx, obj in enumerate(self._events, start=1):
+                    inf_num = obj["inferior"]
+                    exit_code = obj["exit_code"]
+
+                    if exit_code is None:
+                        msg = "None"
+                    else:
+                        msg = exit_code
+
+                    print(
+                        "Event {}/{}, Inferior {}, Exit Code {}".format(
+                            idx, total, inf_num, msg
+                        )
+                    )
+        elif args == "reset":
+            self._events = []
+        else:
+            raise gdb.GdbError("Unknown command args: {}".format(args))
+
+
+events_cmd()
+events_corefile_changed_cmd()
+events_exited_cmd()
+
 print("Success")
