diff --git a/gdb/NEWS b/gdb/NEWS
index 36443c38aca..500d5ab7160 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -92,6 +92,10 @@ show remote thread-options-packet
      objects can still be obtained through calling other API
      functions, for example 'gdb.current_progspace()'.
 
+  ** User defined attributes can be added to a gdb.Inferior object,
+     these will be stored in the object's new Inferior.__dict__
+     attribute.
+
 * Debugger Adapter Protocol changes
 
   ** GDB now emits the "process" event.
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index d74defeec0c..721f0100178 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -3667,6 +3667,38 @@
 string.
 @end defun
 
+One may add arbitrary attributes to @code{gdb.Inferior} objects in the
+usual Python way.  This is useful if, for example, one needs to do
+some extra record keeping associated with the inferior.
+
+In this contrived example we record the time when an inferior last
+stopped:
+
+@smallexample
+(gdb) python
+import datetime
+
+def thread_stopped(event):
+    if event.inferior_thread is not None:
+        thread = event.inferior_thread
+    else:
+        thread = gdb.selected_thread()
+    inferior = thread.inferior
+    inferior._last_stop_time = datetime.datetime.today()
+
+gdb.events.stop.connect(thread_stopped)
+(gdb) file /tmp/hello
+Reading symbols from /tmp/hello...
+(gdb) start
+Temporary breakpoint 1 at 0x401198: file /tmp/hello.c, line 18.
+Starting program: /tmp/hello
+
+Temporary breakpoint 1, main () at /tmp/hello.c:18
+18	  printf ("Hello World\n");
+(gdb) python print(gdb.selected_inferior()._last_stop_time)
+2024-01-04 14:48:41.347036
+@end smallexample
+
 @node Events In Python
 @subsubsection Events In Python
 @cindex inferior events in Python
diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c
index 929d8bd17b5..3f14bc31459 100644
--- a/gdb/python/py-inferior.c
+++ b/gdb/python/py-inferior.c
@@ -46,6 +46,10 @@ struct inferior_object
   /* thread_object instances under this inferior.  This owns a
      reference to each object it contains.  */
   thread_map_t *threads;
+
+  /* Dictionary holding user-added attributes.
+     This is the __dict__ attribute of the object.  */
+  PyObject *dict;
 };
 
 extern PyTypeObject inferior_object_type
@@ -241,6 +245,9 @@ inferior_to_inferior_object (struct inferior *inferior)
 
       inf_obj->inferior = inferior;
       inf_obj->threads = new thread_map_t ();
+      inf_obj->dict = PyDict_New ();
+      if (inf_obj->dict == nullptr)
+	return nullptr;
 
       /* PyObject_New initializes the new object with a refcount of 1.  This
 	 counts for the reference we are keeping in the inferior data.  */
@@ -981,6 +988,8 @@ infpy_dealloc (PyObject *obj)
      function is called.  */
   gdb_assert (inf_obj->inferior == nullptr);
 
+  Py_XDECREF (inf_obj->dict);
+
   Py_TYPE (obj)->tp_free (obj);
 }
 
@@ -1038,6 +1047,8 @@ GDBPY_INITIALIZE_FILE (gdbpy_initialize_inferior);
 
 static gdb_PyGetSetDef inferior_object_getset[] =
 {
+  { "__dict__", gdb_py_generic_dict, nullptr,
+    "The __dict__ for this inferior.", &inferior_object_type },
   { "arguments", infpy_get_args, infpy_set_args,
     "Arguments to this program.", nullptr },
   { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL },
@@ -1135,7 +1146,7 @@ PyTypeObject inferior_object_type =
   0,				  /* tp_dict */
   0,				  /* tp_descr_get */
   0,				  /* tp_descr_set */
-  0,				  /* tp_dictoffset */
+  offsetof (inferior_object, dict), /* tp_dictoffset */
   0,				  /* tp_init */
   0				  /* tp_alloc */
 };
diff --git a/gdb/testsuite/gdb.python/py-inferior.exp b/gdb/testsuite/gdb.python/py-inferior.exp
index 5a221f800c3..0e00636fa1c 100644
--- a/gdb/testsuite/gdb.python/py-inferior.exp
+++ b/gdb/testsuite/gdb.python/py-inferior.exp
@@ -85,6 +85,15 @@ gdb_test "python print (i0.threads ())" \
 gdb_test "python print (i0.progspace)" "<gdb.Progspace object at $hex>"
 gdb_test "python print (i0.progspace == gdb.progspaces()\[0\])" "True"
 
+# Add a user defined attribute to the inferior, and check the
+# attribute can be read back.
+gdb_test_no_output "python i0._user_attr = 123" \
+    "add a user defined attribute to the inferior object"
+gdb_test "python print(i0._user_attr)" \
+    "123" "read back user defined attribute from i0"
+gdb_test "python print(gdb.inferiors()\[0\]._user_attr)" \
+    "123" "read back user defined attribute from gdb.inferiors"
+
 # Test the number of inferior threads.
 
 gdb_breakpoint check_threads
@@ -127,6 +136,16 @@ gdb_test "print str" " = \"hallo, testsuite\"" \
 # correct inferior.
 set num [add_inferior]
 
+# Confirm the new inferior doesn't have the user defined attribute,
+# but that the first inferior does still have the attribute.
+gdb_test "python print(gdb.inferiors()\[1\]._user_attr)" \
+    [multi_line \
+	 "AttributeError: 'gdb\\.Inferior' object has no attribute '_user_attr'" \
+	 "Error while executing Python code\\."] \
+    "check new inferior doesn't have user defined attribute"
+gdb_test "python print(gdb.inferiors()\[0\]._user_attr)" \
+    "123" "read back user defined attribute again"
+
 # Test memory search.
 
 set hex_number {0x[0-9a-fA-F][0-9a-fA-F]*}
