diff --git a/gdb/NEWS b/gdb/NEWS
index fd7ab282f69..41987c6b9ec 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -315,6 +315,13 @@ show tui mouse-events
      gdb.Progspace) and 'reload' (a Boolean) attributes.  This event
      is emitted when gdb.Progspace.executable_filename changes.
 
+  ** New event registries gdb.events.new_progspace and
+     gdb.events.free_progspace, these emit NewProgspaceEvent and
+     FreeProgspaceEvent event types respectively.  Both of these event
+     types have a single 'progspace' attribute, which is the
+     gdb.Progspace that is either being added to GDB, or removed from
+     GDB.
+
 *** Changes in GDB 13
 
 * MI version 1 is deprecated, and will be removed in GDB 14.
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 0471e78aa19..a97e4451897 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -3962,6 +3962,35 @@
 file is updated first, so when this event is emitted, the executable
 filename will have changed, but the symbol filename might still hold
 its previous value.
+
+@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
+is of type @code{gdb.NewProgspaceEvent}, and has a single read-only
+attribute:
+
+@defvar NewProgspaceEvent.progspace
+The @code{gdb.Progspace} that was added to @value{GDBN}.
+@end defvar
+
+No @code{NewProgspaceEvent} is emitted for the very first program
+space, which is assigned to the first inferior.  This first program
+space is created within @value{GDBN} before any Python scripts are
+sourced.
+
+@item events.free_progspace
+This is emitted when @value{GDBN} removes a program space
+(@pxref{Progspaces In Python,,Program Spaces In Python}), for example
+as a result of the @kbd{remove-inferiors} command
+(@pxref{remove_inferiors_cli,,@kbd{remove-inferiors}}).  The event is
+of type @code{gdb.FreeProgspaceEvent}, and has a single read-only
+attribute:
+
+@defvar FreeProgspaceEvent.progspace
+The @code{gdb.Progspace} that is about to be removed from
+@value{GDBN}.
+@end defvar
+
 @end table
 
 @node Threads In Python
diff --git a/gdb/observable.c b/gdb/observable.c
index 33e51223cf4..93a842e911f 100644
--- a/gdb/observable.c
+++ b/gdb/observable.c
@@ -72,6 +72,8 @@ DEFINE_OBSERVABLE (gdb_exiting);
 DEFINE_OBSERVABLE (connection_removed);
 DEFINE_OBSERVABLE (target_pre_wait);
 DEFINE_OBSERVABLE (target_post_wait);
+DEFINE_OBSERVABLE (new_program_space);
+DEFINE_OBSERVABLE (free_program_space);
 
 } /* namespace observers */
 } /* namespace gdb */
diff --git a/gdb/observable.h b/gdb/observable.h
index 464d0b70f5b..20535bb655d 100644
--- a/gdb/observable.h
+++ b/gdb/observable.h
@@ -241,6 +241,12 @@ extern observable <ptid_t /* ptid */> target_pre_wait;
 /* About to leave target_wait (). */
 extern observable <ptid_t /* event_ptid */> target_post_wait;
 
+/* New program space PSPACE was created.  */
+extern observable <program_space */* pspace */> new_program_space;
+
+/* The program space PSPACE is about to be deleted.  */
+extern observable <program_space */* pspace */> free_program_space;
+
 } /* namespace observers */
 
 } /* namespace gdb */
diff --git a/gdb/progspace.c b/gdb/progspace.c
index 5cf8334ee67..1dbcd5875dd 100644
--- a/gdb/progspace.c
+++ b/gdb/progspace.c
@@ -28,6 +28,7 @@
 #include "inferior.h"
 #include <algorithm>
 #include "cli/cli-style.h"
+#include "observable.h"
 
 /* The last program space number assigned.  */
 static int last_program_space_num = 0;
@@ -98,6 +99,7 @@ program_space::program_space (address_space *aspace_)
     aspace (aspace_)
 {
   program_spaces.push_back (this);
+  gdb::observers::new_program_space.notify (this);
 }
 
 /* See progspace.h.  */
@@ -106,6 +108,7 @@ program_space::~program_space ()
 {
   gdb_assert (this != current_program_space);
 
+  gdb::observers::free_program_space.notify (this);
   remove_program_space (this);
 
   scoped_restore_current_program_space restore_pspace;
diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def
index 04a12e1bdf0..b1decc77231 100644
--- a/gdb/python/py-all-events.def
+++ b/gdb/python/py-all-events.def
@@ -43,3 +43,5 @@ GDB_PY_DEFINE_EVENT(before_prompt)
 GDB_PY_DEFINE_EVENT(gdb_exiting)
 GDB_PY_DEFINE_EVENT(connection_removed)
 GDB_PY_DEFINE_EVENT(executable_changed)
+GDB_PY_DEFINE_EVENT(new_progspace)
+GDB_PY_DEFINE_EVENT(free_progspace)
diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def
index b862094650d..c6225115027 100644
--- a/gdb/python/py-event-types.def
+++ b/gdb/python/py-event-types.def
@@ -130,3 +130,13 @@ GDB_PY_DEFINE_EVENT_TYPE (executable_changed,
 			  "ExecutableChangedEvent",
 			  "GDB executable changed event",
 			  event_object_type);
+
+GDB_PY_DEFINE_EVENT_TYPE (new_progspace,
+			  "NewProgspaceEvent",
+			  "GDB new Progspace event object",
+			  event_object_type);
+
+GDB_PY_DEFINE_EVENT_TYPE (free_progspace,
+			  "FreeProgspaceEvent",
+			  "GDB free Progspace event object",
+			  event_object_type);
diff --git a/gdb/python/py-progspace.c b/gdb/python/py-progspace.c
index 082509b2b5b..72a5d4f4c62 100644
--- a/gdb/python/py-progspace.c
+++ b/gdb/python/py-progspace.c
@@ -643,11 +643,74 @@ gdbpy_executable_changed (struct program_space *pspace, bool reload_p)
       gdbpy_print_stack ();
 }
 
+/* Helper function to emit NewProgspaceEvent (when ADDING_P is true) or
+   FreeProgspaceEvent events (when ADDING_P is false).  */
+
+static void
+gdbpy_program_space_event (program_space *pspace, bool adding_p)
+{
+  if (!gdb_python_initialized)
+    return;
+
+  gdbpy_enter enter_py;
+
+  eventregistry_object *registry;
+  PyTypeObject *event_type;
+  if (adding_p)
+    {
+      registry = gdb_py_events.new_progspace;
+      event_type = &new_progspace_event_object_type;
+    }
+  else
+    {
+      registry = gdb_py_events.free_progspace;
+      event_type = &free_progspace_event_object_type;
+    }
+
+  if (evregpy_no_listeners_p (registry))
+    return;
+
+  gdbpy_ref<> pspace_obj = pspace_to_pspace_object (pspace);
+  if (pspace_obj == nullptr)
+    {
+      gdbpy_print_stack ();
+      return;
+    }
+
+  gdbpy_ref<> event = create_event_object (event_type);
+  if (event == nullptr
+      || evpy_add_attribute (event.get (), "progspace",
+			     pspace_obj.get ()) < 0
+      || evpy_emit_event (event.get (), registry) < 0)
+    gdbpy_print_stack ();
+}
+
+/* Emit a NewProgspaceEvent to indicate PSPACE has been created.  */
+
+static void
+gdbpy_new_program_space_event (program_space *pspace)
+{
+  gdbpy_program_space_event (pspace, true);
+}
+
+/* Emit a FreeProgspaceEvent to indicate PSPACE is just about to be removed
+   from GDB.  */
+
+static void
+gdbpy_free_program_space_event (program_space *pspace)
+{
+  gdbpy_program_space_event (pspace, false);
+}
+
 static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
 gdbpy_initialize_pspace (void)
 {
   gdb::observers::executable_changed.attach (gdbpy_executable_changed,
 					     "py-progspace");
+  gdb::observers::new_program_space.attach (gdbpy_new_program_space_event,
+					    "py-progspace");
+  gdb::observers::free_program_space.attach (gdbpy_free_program_space_event,
+					     "py-progspace");
 
   if (PyType_Ready (&pspace_object_type) < 0)
     return -1;
diff --git a/gdb/testsuite/gdb.python/py-progspace-events.c b/gdb/testsuite/gdb.python/py-progspace-events.c
new file mode 100644
index 00000000000..6684cf8ac0a
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-progspace-events.c
@@ -0,0 +1,73 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2023 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see  <http://www.gnu.org/licenses/>.  */
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <assert.h>
+#include <sys/wait.h>
+
+/* This gives the inferior something to do.  */
+volatile int global_var = 0;
+
+void
+breakpt ()
+{ /* Nothing.  */ }
+
+void
+do_child_stuff ()
+{
+  breakpt ();
+  ++global_var;
+}
+
+void
+do_parent_stuff ()
+{
+  breakpt ();
+  ++global_var;
+}
+
+void
+create_child ()
+{
+  int stat;
+  pid_t wpid;
+  breakpt ();
+  pid_t pid = fork ();
+  assert (pid != -1);
+
+  if (pid == 0)
+    {
+      /* Child.  */
+      do_child_stuff ();
+      return;
+    }
+
+  /* Parent.  */
+  do_parent_stuff ();
+  wpid = waitpid (pid, &stat, 0);
+  assert (wpid == pid);
+}
+
+
+
+int
+main ()
+{
+  create_child ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.python/py-progspace-events.exp b/gdb/testsuite/gdb.python/py-progspace-events.exp
new file mode 100644
index 00000000000..60ae290c0b2
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-progspace-events.exp
@@ -0,0 +1,125 @@
+# Copyright (C) 2023 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# This file is part of the GDB testsuite.  It tests the program space
+# related events in the Python API.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile
+
+if {[prepare_for_testing "preparing" $testfile $srcfile] == -1} {
+    return -1
+}
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/py-progspace-events.py]
+gdb_test_no_output "source ${pyfile}" "load python file"
+
+if {![runto_main]} {
+    return
+}
+
+gdb_breakpoint breakpt
+
+gdb_continue_to_breakpoint "run to breakpt function"
+
+gdb_test_no_output "set detach-on-fork off"
+
+# Continue until the parent process forks and a new child is added.
+# Done this way so we can count the new progspace events; we expect to
+# see exactly one.
+set new_progspace_event_count 0
+gdb_test_multiple "continue" "continue until child process appears" {
+    -re "^NewProgspaceEvent: <gdb.Progspace object at $hex>\r\n" {
+	# This is a correctly formed event line.
+	incr new_progspace_event_count
+	exp_continue
+    }
+
+    -re "^NewProgspaceEvent:\[^\r\n\]+\r\n" {
+	# This is an incorrectly formed event line.
+	fail $gdb_test_name
+    }
+
+    -re "^$gdb_prompt $" {
+	pass $gdb_test_name
+    }
+
+    -re "^\[^\r\n\]*\r\n" {
+	exp_continue
+    }
+}
+
+gdb_assert { $new_progspace_event_count == 1 } \
+    "only a single new progspace event seen"
+
+# Switch to inferior 2 and continue until we hit breakpt.
+gdb_test "inferior 2" "\\\[Switching to inferior 2 .*"
+gdb_continue_to_breakpoint "run to breakpt in inferior 2"
+
+# Let inferior 2 exit.  The new program space is not removed at this
+# point.
+gdb_test "continue" \
+    [multi_line \
+	 "^Continuing\\." \
+	 "\\\[Inferior $decimal \[^\r\n\]+ exited normally\\\]"] \
+    "continue until inferior 2 exits"
+
+gdb_test "inferior 1" "\\\[Switching to inferior 1 .*"
+
+# Step the inferior.  During this process GDB will prune the now
+# defunct inferior, which deletes its program space, which should
+# trigger the FreeProgspaceEvent.
+#
+# However, there is a slight problem.  When the target is remote, and
+# GDB is accessing files using remote fileio, then GDB will attempt to
+# prune the inferior at a point in time when the remote target is
+# waiting for a stop reply.  Pruning an inferior causes GDB to close
+# files associated with that inferior.
+#
+# In non-async mode we can't send fileio packets while waiting for a
+# stop reply, so the attempts to close files fails, and this shows up
+# as an error.
+#
+# As this error has nothing to do with the feature being tested here,
+# we just accept the error message, the important part is the
+# 'FreeProgspaceEvent' string, so long as that appears (just once)
+# then the test is a success.
+set warning_msg \
+    [multi_line \
+	 "warning: cannot close \"\[^\r\n\]+\": Cannot execute this command while the target is running\\." \
+	 "Use the \"interrupt\" command to stop the target" \
+	 "and then try again\\."]
+
+gdb_test "step" \
+    [multi_line \
+	 "^FreeProgspaceEvent: <gdb.Progspace object at $hex>(?:\r\n$warning_msg)*" \
+	 "do_parent_stuff \\(\\) at \[^\r\n\]+" \
+	 "$decimal\\s+\[^\r\n\]+"]
+
+# Let this inferior run to completion.
+gdb_continue_to_end
+
+# Check the program space events trigger when a new inferior is
+# manually added and removed.
+gdb_test "add-inferior" \
+    [multi_line \
+	 "^NewProgspaceEvent: <gdb.Progspace object at $hex>" \
+	 "\\\[New inferior 3\\\]" \
+	 "Added inferior 3\[^\r\n\]*"]
+gdb_test "remove-inferior 3" \
+    "^FreeProgspaceEvent: <gdb.Progspace object at $hex>"
diff --git a/gdb/testsuite/gdb.python/py-progspace-events.py b/gdb/testsuite/gdb.python/py-progspace-events.py
new file mode 100644
index 00000000000..3abb421cffa
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-progspace-events.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2023 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# This file is part of the GDB testsuite, it registers listeners for
+# the Progspace related events.
+
+import gdb
+import gdb.events
+
+def new_progspace(event):
+    print("NewProgspaceEvent: %s" % str(event.progspace))
+
+def free_progspace(event):
+    print("FreeProgspaceEvent: %s" % str(event.progspace))
+
+gdb.events.new_progspace.connect(new_progspace)
+gdb.events.free_progspace.connect(free_progspace)
