diff --git a/CMakeLists.txt b/CMakeLists.txt
index cd12dcd7bd49ab9abfdec1eaec7b032ee781991f..4bdf2591dbdb9c6429d37fa95cddd90c469a89c7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -35,6 +35,7 @@ if(MTIME_ENABLE_PYTHON)
   # Technically, we can produce the Python interface without the interpreter but
   # we REQUIRE it for now for the consistency with the Autotools-based building:
   find_package(Python REQUIRED COMPONENTS Interpreter)
+  include(GNUInstallPythonDirs)
   add_subdirectory(python)
 endif()
 
diff --git a/cmake/GNUInstallPythonDirs.cmake b/cmake/GNUInstallPythonDirs.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..c0b2a8caf8b8221daf24a411925be5cf53967d6e
--- /dev/null
+++ b/cmake/GNUInstallPythonDirs.cmake
@@ -0,0 +1,85 @@
+# Copyright (c) 2013-2024 MPI-M, Luis Kornblueh, Rahul Sinha and DWD, Florian Prill. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+if(NOT DEFINED Python_FOUND)
+  message(
+    AUTHOR_WARNING
+      "The macro requires Python: you either need to find_package(Python) or "
+      "make sure that the following variables are set (e.g. you can get valid "
+      "values under different variable names from find_package(Python3)): "
+      "Python_FOUND, Python_EXECUTABLE, "
+      "Python_VERSION_MAJOR, Python_VERSION_MINOR"
+  )
+endif()
+
+# ~~~
+# _GNUInstallPythonDirs_get_sitedir(<plat_specific>,
+#                                   <fallback>,
+#                                   <variable>,
+#                                   <description>)
+# ~~~
+# Requests ${Python_EXECUTABLE} for the subdirectory for either the general
+# (when <plat_specific> is set to 0) or platform-dependent (when <plat_specific>
+# is set to 1) library installation. Set the cash <variable> with the provided
+# <description> to the result of the request. If the request fails, the
+# <variable> is set to the <fallback> value.
+#
+macro(
+  _GNUInstallPythonDirs_get_sitedir plat_specific fallback variable description
+)
+  unset(_sitedir)
+  if(Python_FOUND)
+    execute_process(
+      COMMAND
+        ${Python_EXECUTABLE} -c
+        "from distutils import sysconfig as sc;print(sc.get_python_lib(${plat_specific},0,''))"
+      RESULT_VARIABLE _success
+      OUTPUT_VARIABLE _sitedir
+      ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE
+    )
+    if(NOT _success EQUAL 0)
+      unset(_sitedir)
+    endif()
+    unset(_success)
+  endif()
+  if(NOT _sitedir)
+    set(_sitedir "${fallback}")
+  endif()
+  set(${variable}
+      "${_sitedir}"
+      CACHE PATH "${description}"
+  )
+  unset(_sitedir)
+endmacro()
+
+if(NOT CMAKE_INSTALL_PYTHON_PURELIBDIR)
+  _gnuinstallpythondirs_get_sitedir(
+    0
+    "${CMAKE_INSTALL_LIBDIR}/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages"
+    CMAKE_INSTALL_PYTHON_PURELIBDIR
+    "Python platform-independent libraries"
+  )
+endif()
+
+if(NOT CMAKE_INSTALL_PYTHON_PLATLIB)
+  _gnuinstallpythondirs_get_sitedir(
+    1
+    "${CMAKE_INSTALL_PYTHON_PURELIBDIR}"
+    CMAKE_INSTALL_PYTHON_PLATLIBDIR
+    "Python platform-specific libraries"
+  )
+endif()
+
+foreach(dir PYTHON_PURELIBDIR PYTHON_PLATLIBDIR)
+  if(COMMAND GNUInstallDirs_get_absolute_install_dir)
+    GNUInstallDirs_get_absolute_install_dir(
+      CMAKE_INSTALL_FULL_${dir} CMAKE_INSTALL_${dir} ${dir}
+    )
+  else()
+    set(CMAKE_INSTALL_FULL_${dir}
+        "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_${dir}}"
+    )
+  endif()
+endforeach()
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index 26b57ce38081b3066d2b0a71f9680eefe8e7bb5b..836c8e5cbf241ece19a26e4e552ae7ba52e2365e 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -34,3 +34,13 @@ file(
     dummy # just don't fail if something goes wrong (e.g. building in-source)
   COPY_ON_ERROR SYMBOLIC
 )
+
+install(
+  TARGETS mtime_py
+  LIBRARY DESTINATION ${CMAKE_INSTALL_PYTHON_PLATLIBDIR}/${module_dir_name}
+)
+
+install(
+  FILES ${module_source_dir}/__init__.py ${module_binary_dir}/_mtime.py
+  DESTINATION ${CMAKE_INSTALL_PYTHON_PLATLIBDIR}/${module_dir_name}
+)