https://github.com/python/cpython/commit/a844747b07bbbe1019ee493afb2f3696b6bb41b5
commit: a844747b07bbbe1019ee493afb2f3696b6bb41b5
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-14T10:39:11+03:00
summary:

gh-117404: Add structured version info for compression modules (GH-150567)

Add constants which provide information about the version of the
compression library used for building the module and the version
actually loaded:

* zlib: ZLIB_VERSION_INFO, zlib_version_info, ZLIBNG_VERSION_INFO,
  and zlib_version as an alias of ZLIB_RUNTIME_VERSION
* bz2: bzlib_version, bzlib_version_info
* lzma: LZMA_VERSION, lzma_version, LZMA_VERSION_INFO, lzma_version_info
* compression.zstd: ZSTD_VERSION, ZSTD_VERSION_INFO

Make compression.zstd.zstd_version_info a named tuple.

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst
M Doc/library/bz2.rst
M Doc/library/compression.zstd.rst
M Doc/library/lzma.rst
M Doc/library/zlib.rst
M Doc/whatsnew/3.16.rst
M Lib/bz2.py
M Lib/compression/zstd/__init__.py
M Lib/lzma.py
M Lib/test/pythoninfo.py
M Lib/test/test_bz2.py
M Lib/test/test_lzma.py
M Lib/test/test_zlib.py
M Lib/test/test_zstd.py
M Modules/_bz2module.c
M Modules/_lzmamodule.c
M Modules/_zstd/_zstdmodule.c
M Modules/zlibmodule.c

diff --git a/Doc/library/bz2.rst b/Doc/library/bz2.rst
index 6c20e9c94a3eaee..d5f367d2d19e90b 100644
--- a/Doc/library/bz2.rst
+++ b/Doc/library/bz2.rst
@@ -307,6 +307,36 @@ One-shot (de)compression
    .. versionchanged:: 3.3
       Support for multi-stream inputs was added.
 
+
+Miscellaneous
+-------------
+
+Information about the version of the bzip2 compression library
+actually loaded by the interpreter is available through the following
+constants.
+The version used for building the module is not available,
+because the bzip2 library does not provide it.
+
+
+.. data:: bzlib_version
+
+   The version string of the bzip2 compression library
+   actually loaded by the interpreter.
+
+   .. versionadded:: next
+
+
+.. data:: bzlib_version_info
+
+   A named tuple containing the three components of the bzip2 compression
+   library version actually loaded by the interpreter:
+   *major*, *minor*, and *patch*.  All values are integers.
+   The components can also be accessed by name, so 
``bz2.bzlib_version_info[0]``
+   is equivalent to ``bz2.bzlib_version_info.major`` and so on.
+
+   .. versionadded:: next
+
+
 .. _bz2-usage-examples:
 
 Examples of usage
diff --git a/Doc/library/compression.zstd.rst b/Doc/library/compression.zstd.rst
index a9fc4fa4b8cc36c..33db45fb7c3b46f 100644
--- a/Doc/library/compression.zstd.rst
+++ b/Doc/library/compression.zstd.rst
@@ -827,10 +827,45 @@ Miscellaneous
    The default compression level for Zstandard: ``3``.
 
 
-.. attribute:: zstd_version_info
+Information about the version of the zstd library in use is available through
+the following constants:
 
-   Version number of the runtime zstd library as a tuple of integers
-   (major, minor, release).
+
+.. data:: ZSTD_VERSION
+
+   The version string of the zstd library that was used for building the 
module.
+   This may be different from the zstd library actually used at runtime, which
+   is available as :const:`zstd_version`.
+
+   .. versionadded:: next
+
+
+.. data:: zstd_version
+
+   The version string of the zstd library actually loaded by the interpreter.
+
+
+.. data:: ZSTD_VERSION_INFO
+
+   A named tuple containing the three components of the zstd library
+   version that was used for building the module:
+   *major*, *minor*, and *patch*.  All values are integers.
+   The components can also be accessed by name, so 
``zstd.ZSTD_VERSION_INFO[0]``
+   is equivalent to ``zstd.ZSTD_VERSION_INFO.major`` and so on.
+   This may be different from the zstd library actually used at runtime, which
+   is available as :const:`zstd_version_info`.
+
+   .. versionadded:: next
+
+
+.. data:: zstd_version_info
+
+   A named tuple containing the version of the zstd library
+   actually loaded by the interpreter,
+   with the same fields as :const:`ZSTD_VERSION_INFO`.
+
+   .. versionchanged:: next
+      It is now a named tuple.
 
 
 Examples
diff --git a/Doc/library/lzma.rst b/Doc/library/lzma.rst
index 580829cfbb46e07..49ba6d9b7ed4f2b 100644
--- a/Doc/library/lzma.rst
+++ b/Doc/library/lzma.rst
@@ -314,6 +314,50 @@ Miscellaneous
    feature set.
 
 
+Information about the version of the lzma library in use is available through
+the following constants:
+
+
+.. data:: LZMA_VERSION
+
+   The version string of the lzma library that was used for building the 
module.
+   This may be different from the lzma library actually used at runtime, which
+   is available as :const:`lzma_version`.
+
+   .. versionadded:: next
+
+
+.. data:: lzma_version
+
+   The version string of the lzma library actually loaded by the interpreter.
+
+   .. versionadded:: next
+
+
+.. data:: LZMA_VERSION_INFO
+
+   A named tuple containing the four components of the lzma library
+   version that was used for building the module:
+   *major*, *minor*, *patch*, and *stability*.
+   All values except *stability* are integers; *stability* is ``'alpha'``,
+   ``'beta'``, or ``'stable'``.
+   The components can also be accessed by name, so 
``lzma.LZMA_VERSION_INFO[0]``
+   is equivalent to ``lzma.LZMA_VERSION_INFO.major`` and so on.
+   This may be different from the lzma library actually used at runtime, which
+   is available as :const:`lzma_version_info`.
+
+   .. versionadded:: next
+
+
+.. data:: lzma_version_info
+
+   A named tuple containing the version of the lzma library
+   actually loaded by the interpreter,
+   with the same fields as :const:`LZMA_VERSION_INFO`.
+
+   .. versionadded:: next
+
+
 .. _filter-chain-specs:
 
 Specifying custom filter chains
diff --git a/Doc/library/zlib.rst b/Doc/library/zlib.rst
index 266bf968e329b4a..5035fa27fb6c1b9 100644
--- a/Doc/library/zlib.rst
+++ b/Doc/library/zlib.rst
@@ -479,27 +479,65 @@ the following constants:
 
    The version string of the zlib library that was used for building the 
module.
    This may be different from the zlib library actually used at runtime, which
-   is available as :const:`ZLIB_RUNTIME_VERSION`.
+   is available as :const:`zlib_version`.
 
 
 .. data:: ZLIB_RUNTIME_VERSION
+          zlib_version
 
    The version string of the zlib library actually loaded by the interpreter.
 
    .. versionadded:: 3.3
+      The :const:`!ZLIB_RUNTIME_VERSION` constant.
+
+   .. versionadded:: next
+      The :const:`!zlib_version` alias.
+
+
+.. data:: ZLIB_VERSION_INFO
+
+   A named tuple containing the four components of the zlib library
+   version that was used for building the module:
+   *major*, *minor*, *revision*, and *subversion*.
+   All values are integers.
+   The components can also be accessed by name, so 
``zlib.ZLIB_VERSION_INFO[0]``
+   is equivalent to ``zlib.ZLIB_VERSION_INFO.major`` and so on.
+   This may be different from the zlib library actually used at runtime, which
+   is available as :const:`zlib_version_info`.
+
+   .. versionadded:: next
+
+
+.. data:: zlib_version_info
+
+   A named tuple containing the version of the zlib library
+   actually loaded by the interpreter,
+   with the same fields as :const:`ZLIB_VERSION_INFO`.
+
+   .. versionadded:: next
+
+
+The following constants are only present if zlib-ng was used to build
+the module:
 
 
 .. data:: ZLIBNG_VERSION
 
    The version string of the zlib-ng library that was used for building the
-   module if zlib-ng was used. When present, the :data:`ZLIB_VERSION` and
-   :data:`ZLIB_RUNTIME_VERSION` constants reflect the version of the zlib API
+   module if zlib-ng was used. When present, the :const:`ZLIB_VERSION` and
+   :const:`zlib_version` constants reflect the version of the zlib API
    provided by zlib-ng.
 
-   If zlib-ng was not used to build the module, this constant will be absent.
-
    .. versionadded:: 3.14
 
+.. data:: ZLIBNG_VERSION_INFO
+
+   A named tuple containing the three components of the zlib-ng library
+   version that was used for building the module:
+   *major*, *minor*, and *revision*.  All values are integers.
+
+   .. versionadded:: next
+
 
 .. seealso::
 
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 8e1dd93f6ed8731..53983637f520c8a 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -116,6 +116,13 @@ asyncio
   socket file created for *path*.
   (Contributed by Sam Bull in :gh:`94984`.)
 
+bz2
+---
+
+* Added constants :const:`~bz2.bzlib_version` and
+  :const:`~bz2.bzlib_version_info` which provide information about
+  the version of the bzip2 compression library in use.
+  (Contributed by Serhiy Storchaka in :gh:`117404`.)
 
 codecs
 ------
@@ -127,6 +134,14 @@ codecs
   even when a built-in codec of the same name exists.
   (Contributed by Serhiy Storchaka in :gh:`152997`.)
 
+compression.zstd
+----------------
+
+* Added constants :const:`~compression.zstd.ZSTD_VERSION` and
+  :const:`~compression.zstd.ZSTD_VERSION_INFO` which provide information
+  about the version of the zstd library that was used for building the module.
+  :const:`~compression.zstd.zstd_version_info` is now a named tuple.
+  (Contributed by Serhiy Storchaka in :gh:`117404`.)
 
 concurrent.futures
 ------------------
@@ -439,6 +454,11 @@ lzma
   requires ``lzma`` 5.4.0 or newer while RISC-V requires 5.6.0 or newer.
   (Contributed by Chien Wong in :gh:`115988`.)
 
+* Added constants :const:`~lzma.LZMA_VERSION`, :const:`~lzma.lzma_version`,
+  :const:`~lzma.LZMA_VERSION_INFO`, and :const:`~lzma.lzma_version_info`,
+  which provide information about the version of the lzma library in use.
+  (Contributed by Serhiy Storchaka in :gh:`117404`.)
+
 
 math
 ----
@@ -747,6 +767,17 @@ zipfile
   by the local file entries of removed members.
   (Contributed by Danny Lin in :gh:`51067`.)
 
+zlib
+----
+
+* Added constants :const:`~zlib.ZLIB_VERSION_INFO`,
+  :const:`~zlib.zlib_version_info`, and :const:`~zlib.ZLIBNG_VERSION_INFO`,
+  which provide information about the version of the zlib and the zlib-ng
+  libraries in use.
+  Added :const:`~zlib.zlib_version` as an alias of
+  :const:`~zlib.ZLIB_RUNTIME_VERSION`.
+  (Contributed by Serhiy Storchaka in :gh:`117404`.)
+
 .. Add improved modules above alphabetically, not here at the end.
 
 Optimizations
diff --git a/Lib/bz2.py b/Lib/bz2.py
index eb58f4da596ea18..79305669efc38c9 100644
--- a/Lib/bz2.py
+++ b/Lib/bz2.py
@@ -5,7 +5,8 @@
 """
 
 __all__ = ["BZ2File", "BZ2Compressor", "BZ2Decompressor",
-           "open", "compress", "decompress"]
+           "open", "compress", "decompress",
+           "bzlib_version", "bzlib_version_info"]
 
 __author__ = "Nadeem Vawda <[email protected]>"
 
@@ -14,7 +15,8 @@
 import io
 import os
 
-from _bz2 import BZ2Compressor, BZ2Decompressor
+from _bz2 import (BZ2Compressor, BZ2Decompressor,
+                  bzlib_version, bzlib_version_info)
 
 
 # Value 0 no longer used
diff --git a/Lib/compression/zstd/__init__.py b/Lib/compression/zstd/__init__.py
index 5326cf9b19cf883..22ce028c435caa8 100644
--- a/Lib/compression/zstd/__init__.py
+++ b/Lib/compression/zstd/__init__.py
@@ -20,6 +20,8 @@
     'get_frame_size',
     'zstd_version',
     'zstd_version_info',
+    'ZSTD_VERSION',
+    'ZSTD_VERSION_INFO',
     'ZstdCompressor',
     'ZstdDecompressor',
     'ZstdDict',
@@ -29,13 +31,10 @@
 import _zstd
 import enum
 from _zstd import (ZstdCompressor, ZstdDecompressor, ZstdDict, ZstdError,
-                   get_frame_size, zstd_version)
+                   get_frame_size, zstd_version, ZSTD_VERSION,
+                   zstd_version_info, ZSTD_VERSION_INFO)
 from compression.zstd._zstdfile import ZstdFile, open, _nbytes
 
-# zstd_version_number is (MAJOR * 100 * 100 + MINOR * 100 + RELEASE)
-zstd_version_info = (*divmod(_zstd.zstd_version_number // 100, 100),
-                     _zstd.zstd_version_number % 100)
-"""Version number of the runtime zstd library as a tuple of integers."""
 
 COMPRESSION_LEVEL_DEFAULT = _zstd.ZSTD_CLEVEL_DEFAULT
 """The default compression level for Zstandard, currently '3'."""
diff --git a/Lib/lzma.py b/Lib/lzma.py
index fb4bbf650f849a4..73a7c702e037112 100644
--- a/Lib/lzma.py
+++ b/Lib/lzma.py
@@ -20,6 +20,7 @@
 
     "LZMACompressor", "LZMADecompressor", "LZMAFile", "LZMAError",
     "open", "compress", "decompress", "is_check_supported",
+    "lzma_version", "lzma_version_info", "LZMA_VERSION", "LZMA_VERSION_INFO",
 ]
 
 import builtins
diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py
index 90b32aaaaf9a40a..b59e2acb9376f1e 100644
--- a/Lib/test/pythoninfo.py
+++ b/Lib/test/pythoninfo.py
@@ -711,17 +711,37 @@ def collect_zlib(info_add):
     except ImportError:
         return
 
-    attributes = ('ZLIB_VERSION', 'ZLIB_RUNTIME_VERSION', 'ZLIBNG_VERSION')
+    attributes = ('ZLIB_VERSION', 'zlib_version', 'ZLIBNG_VERSION')
     copy_attributes(info_add, zlib, 'zlib.%s', attributes)
 
 
+def collect_bz2(info_add):
+    try:
+        import _bz2
+    except ImportError:
+        return
+
+    attributes = ('bzlib_version',)
+    copy_attributes(info_add, _bz2, 'bz2.%s', attributes)
+
+
+def collect_lzma(info_add):
+    try:
+        import _lzma
+    except ImportError:
+        return
+
+    attributes = ('LZMA_VERSION', 'lzma_version')
+    copy_attributes(info_add, _lzma, 'lzma.%s', attributes)
+
+
 def collect_zstd(info_add):
     try:
         import _zstd
     except ImportError:
         return
 
-    attributes = ('zstd_version',)
+    attributes = ('ZSTD_VERSION', 'zstd_version')
     copy_attributes(info_add, _zstd, 'zstd.%s', attributes)
 
 
@@ -1328,6 +1348,7 @@ def collect_info(info):
         collect_urandom,
 
         collect_builtins,
+        collect_bz2,
         collect_cc,
         collect_curses,
         collect_datetime,
@@ -1338,6 +1359,7 @@ def collect_info(info):
         collect_gdbm,
         collect_get_config,
         collect_locale,
+        collect_lzma,
         collect_os,
         collect_platform,
         collect_pwd,
diff --git a/Lib/test/test_bz2.py b/Lib/test/test_bz2.py
index 64293d757331d75..2c95a8eec510b0a 100644
--- a/Lib/test/test_bz2.py
+++ b/Lib/test/test_bz2.py
@@ -1222,6 +1222,33 @@ def test_newline(self):
             self.assertEqual(f.readlines(), [text])
 
 
+class MiscTests(unittest.TestCase):
+
+    def test_bzlib_version(self):
+        if support.verbose:
+            print(f'bzlib_version = {bz2.bzlib_version}', flush=True)
+            print(f'bzlib_version_info = {bz2.bzlib_version_info}', flush=True)
+        v = bz2.bzlib_version_info
+        self.assertIsInstance(v[:], tuple)
+        self.assertEqual(len(v), 3)
+        self.assertIsInstance(v[0], int)
+        self.assertIsInstance(v[1], int)
+        self.assertIsInstance(v[2], int)
+        self.assertIsInstance(v.major, int)
+        self.assertIsInstance(v.minor, int)
+        self.assertIsInstance(v.patch, int)
+        self.assertEqual(v[0], v.major)
+        self.assertEqual(v[1], v.minor)
+        self.assertEqual(v[2], v.patch)
+        self.assertGreaterEqual(v.major, 0)
+        self.assertGreaterEqual(v.minor, 0)
+        self.assertGreaterEqual(v.patch, 0)
+
+        # The version string can have a suffix, e.g. "1.0.8, 13-Jul-2019"
+        # for bzip2 or "1.1.0-libbz2-rs-sys-0.2.5" for libbz2-rs.
+        self.assertStartsWith(bz2.bzlib_version, '%d.%d.%d' % v)
+
+
 def tearDownModule():
     support.reap_children()
 
diff --git a/Lib/test/test_lzma.py b/Lib/test/test_lzma.py
index d4f954b34c12526..df0b66039d77a1a 100644
--- a/Lib/test/test_lzma.py
+++ b/Lib/test/test_lzma.py
@@ -1513,6 +1513,41 @@ def test_x_mode(self):
 
 class MiscellaneousTestCase(unittest.TestCase):
 
+    def _test_lzma_version(self, v, string):
+        self.assertIsInstance(v[:], tuple)
+        self.assertEqual(len(v), 4)
+        self.assertIsInstance(v[0], int)
+        self.assertIsInstance(v[1], int)
+        self.assertIsInstance(v[2], int)
+        self.assertIsInstance(v[3], str)
+        self.assertIsInstance(v.major, int)
+        self.assertIsInstance(v.minor, int)
+        self.assertIsInstance(v.patch, int)
+        self.assertIsInstance(v.stability, str)
+        self.assertEqual(v[0], v.major)
+        self.assertEqual(v[1], v.minor)
+        self.assertEqual(v[2], v.patch)
+        self.assertEqual(v[3], v.stability)
+        self.assertGreaterEqual(v.major, 0)
+        self.assertGreaterEqual(v.minor, 0)
+        self.assertGreaterEqual(v.patch, 0)
+        self.assertIn(v.stability, {'alpha', 'beta', 'stable'})
+
+        if v.stability == 'stable':
+            self.assertEqual(string, '%d.%d.%d' % v[:3])
+        else:
+            self.assertStartsWith(string, '%d.%d.%d%s' % v)
+
+    def test_lzma_version(self):
+        if support.verbose:
+            print(f'LZMA_VERSION = {lzma.LZMA_VERSION}', flush=True)
+            print(f'lzma_version = {lzma.lzma_version}', flush=True)
+            print(f'LZMA_VERSION_INFO = {lzma.LZMA_VERSION_INFO}', flush=True)
+            print(f'lzma_version_info = {lzma.lzma_version_info}', flush=True)
+        self._test_lzma_version(lzma.LZMA_VERSION_INFO, lzma.LZMA_VERSION)
+        self._test_lzma_version(lzma.lzma_version_info, lzma.lzma_version)
+        self.assertEqual(lzma.LZMA_VERSION_INFO[0], lzma.lzma_version_info[0])
+
     def test_is_check_supported(self):
         # CHECK_NONE and CHECK_CRC32 should always be supported,
         # regardless of the options liblzma was compiled with.
diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py
index 024c58e7cabd79d..205916579b5f49d 100644
--- a/Lib/test/test_zlib.py
+++ b/Lib/test/test_zlib.py
@@ -19,20 +19,17 @@
         'requires Decompress.copy()')
 
 
-def _zlib_runtime_version_tuple(zlib_version=zlib.ZLIB_RUNTIME_VERSION):
+def _parse_version(version):
     # Register "1.2.3" as "1.2.3.0"
     # or "1.2.0-linux","1.2.0.f","1.2.0.f-linux"
-    v = zlib_version.split('-', 1)[0].split('.')
-    if len(v) < 4:
+    v = version.split('-', 1)[0].split('.')
+    if not v[-1].isnumeric():
+        v.pop()
+    while len(v) < 4:
         v.append('0')
-    elif not v[-1].isnumeric():
-        v[-1] = '0'
     return tuple(map(int, v))
 
 
-ZLIB_RUNTIME_VERSION_TUPLE = _zlib_runtime_version_tuple()
-
-
 # bpo-46623: When a hardware accelerator is used (currently only on s390x),
 # using different ways to compress data with zlib can produce different
 # compressed data.
@@ -71,7 +68,70 @@ def test_library_version(self):
         # the minor versions will match (even on the machine on which the 
module
         # was compiled), and the API is stable between minor versions, so
         # testing only the major versions avoids spurious failures.
-        self.assertEqual(zlib.ZLIB_RUNTIME_VERSION[0], zlib.ZLIB_VERSION[0])
+        self.assertEqual(zlib.zlib_version.split('.')[0],
+                         zlib.ZLIB_VERSION.split('.')[0])
+        self.assertEqual(zlib.zlib_version_info[0],
+                         zlib.ZLIB_VERSION_INFO[0])
+
+    def _test_zlib_version(self, v):
+        self.assertIsInstance(v[:], tuple)
+        self.assertEqual(len(v), 4)
+        self.assertIsInstance(v[0], int)
+        self.assertIsInstance(v[1], int)
+        self.assertIsInstance(v[2], int)
+        self.assertIsInstance(v[3], int)
+        self.assertIsInstance(v.major, int)
+        self.assertIsInstance(v.minor, int)
+        self.assertIsInstance(v.revision, int)
+        self.assertIsInstance(v.subversion, int)
+        self.assertEqual(v[0], v.major)
+        self.assertEqual(v[1], v.minor)
+        self.assertEqual(v[2], v.revision)
+        self.assertEqual(v[3], v.subversion)
+        self.assertGreaterEqual(v.major, 0)
+        self.assertGreaterEqual(v.minor, 0)
+        self.assertGreaterEqual(v.revision, 0)
+        self.assertGreaterEqual(v.subversion, 0)
+
+    def test_zlib_version(self):
+        if support.verbose:
+            print(f'ZLIB_VERSION = {zlib.ZLIB_VERSION}', flush=True)
+            print(f'zlib_version = {zlib.zlib_version}', flush=True)
+            print(f'ZLIB_VERSION_INFO = {zlib.ZLIB_VERSION_INFO}', flush=True)
+            print(f'zlib_version_info = {zlib.zlib_version_info}', flush=True)
+        self._test_zlib_version(zlib.ZLIB_VERSION_INFO)
+        self.assertEqual(zlib.ZLIB_VERSION_INFO,
+                         _parse_version(zlib.ZLIB_VERSION))
+        self._test_zlib_version(zlib.zlib_version_info)
+        self.assertEqual(zlib.zlib_version_info,
+                         _parse_version(zlib.zlib_version))
+        self.assertEqual(zlib.ZLIB_RUNTIME_VERSION, zlib.zlib_version)
+
+    def _test_zlibng_version(self, v):
+        self.assertIsInstance(v[:], tuple)
+        self.assertEqual(len(v), 3)
+        self.assertIsInstance(v[0], int)
+        self.assertIsInstance(v[1], int)
+        self.assertIsInstance(v[2], int)
+        self.assertIsInstance(v.major, int)
+        self.assertIsInstance(v.minor, int)
+        self.assertIsInstance(v.revision, int)
+        self.assertEqual(v[0], v.major)
+        self.assertEqual(v[1], v.minor)
+        self.assertEqual(v[2], v.revision)
+        self.assertGreaterEqual(v.major, 0)
+        self.assertGreaterEqual(v.minor, 0)
+        self.assertGreaterEqual(v.revision, 0)
+
+    @unittest.skipUnless(hasattr(zlib, 'ZLIBNG_VERSION'), 'requires zlib-ng')
+    def test_zlibng_version(self):
+        if support.verbose:
+            print(f'ZLIBNG_VERSION = {zlib.ZLIBNG_VERSION}', flush=True)
+            print(f'ZLIBNG_VERSION_INFO = {zlib.ZLIBNG_VERSION_INFO}',
+                  flush=True)
+        self._test_zlibng_version(zlib.ZLIBNG_VERSION_INFO)
+        self.assertEqual(zlib.ZLIBNG_VERSION,
+                         '%d.%d.%d' % zlib.ZLIBNG_VERSION_INFO)
 
 
 class ChecksumTestCase(unittest.TestCase):
@@ -607,7 +667,7 @@ def test_flushes(self):
                     'Z_PARTIAL_FLUSH']
 
         # Z_BLOCK has a known failure prior to 1.2.5.3
-        if ZLIB_RUNTIME_VERSION_TUPLE >= (1, 2, 5, 3):
+        if zlib.zlib_version_info >= (1, 2, 5, 3):
             sync_opt.append('Z_BLOCK')
 
         sync_opt = [getattr(zlib, opt) for opt in sync_opt
@@ -935,7 +995,7 @@ def test_large_unconsumed_tail(self, size):
 
     def test_wbits(self):
         # wbits=0 only supported since zlib v1.2.3.5
-        supports_wbits_0 = ZLIB_RUNTIME_VERSION_TUPLE >= (1, 2, 3, 5)
+        supports_wbits_0 = zlib.zlib_version_info >= (1, 2, 3, 5)
 
         co = zlib.compressobj(level=1, wbits=15)
         zlib15 = co.compress(HAMLET_SCENE) + co.flush()
diff --git a/Lib/test/test_zstd.py b/Lib/test/test_zstd.py
index 9225c42e0e711aa..372326a26c71eaf 100644
--- a/Lib/test/test_zstd.py
+++ b/Lib/test/test_zstd.py
@@ -9,6 +9,7 @@
 import tempfile
 import threading
 
+from test import support
 from test.support.import_helper import import_module
 from test.support import threading_helper
 from test.support import _1M
@@ -24,8 +25,6 @@
     ZstdDecompressor,
     ZstdDict,
     ZstdError,
-    zstd_version,
-    zstd_version_info,
     COMPRESSION_LEVEL_DEFAULT,
     get_frame_info,
     get_frame_size,
@@ -133,9 +132,33 @@ def setUpModule():
 
 class FunctionsTestCase(unittest.TestCase):
 
-    def test_version(self):
-        s = ".".join((str(i) for i in zstd_version_info))
-        self.assertEqual(s, zstd_version)
+    def _test_zstd_version(self, v, string):
+        self.assertIsInstance(v[:], tuple)
+        self.assertEqual(len(v), 3)
+        self.assertIsInstance(v[0], int)
+        self.assertIsInstance(v[1], int)
+        self.assertIsInstance(v[2], int)
+        self.assertIsInstance(v.major, int)
+        self.assertIsInstance(v.minor, int)
+        self.assertIsInstance(v.patch, int)
+        self.assertEqual(v[0], v.major)
+        self.assertEqual(v[1], v.minor)
+        self.assertEqual(v[2], v.patch)
+        self.assertGreaterEqual(v.major, 0)
+        self.assertGreaterEqual(v.minor, 0)
+        self.assertGreaterEqual(v.patch, 0)
+
+        self.assertEqual(string, '%d.%d.%d' % v)
+
+    def test_zstd_version(self):
+        if support.verbose:
+            print(f'ZSTD_VERSION = {zstd.ZSTD_VERSION}', flush=True)
+            print(f'zstd_version = {zstd.zstd_version}', flush=True)
+            print(f'ZSTD_VERSION_INFO = {zstd.ZSTD_VERSION_INFO}', flush=True)
+            print(f'zstd_version_info = {zstd.zstd_version_info}', flush=True)
+        self._test_zstd_version(zstd.ZSTD_VERSION_INFO, zstd.ZSTD_VERSION)
+        self._test_zstd_version(zstd.zstd_version_info, zstd.zstd_version)
+        self.assertEqual(zstd.ZSTD_VERSION_INFO[0], zstd.zstd_version_info[0])
 
     def test_compressionLevel_values(self):
         min, max = CompressionParameter.compression_level.bounds()
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst 
b/Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst
new file mode 100644
index 000000000000000..3f1942cf7e143c6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-29-10-11-34.gh-issue-117404.EeP6xe.rst
@@ -0,0 +1,5 @@
+Add a number of constants in modules :mod:`zlib`, :mod:`bz2`, :mod:`lzma`,
+and :mod:`compression.zstd`, which provide information about the version of
+libraries that were used for building the module and that are actually
+loaded.
+Make :const:`compression.zstd.zstd_version_info` a named tuple.
diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c
index 9db3ac39da52099..e0d85b78741a91e 100644
--- a/Modules/_bz2module.c
+++ b/Modules/_bz2module.c
@@ -747,6 +747,61 @@ static PyType_Spec bz2_decompressor_type_spec = {
     .slots = bz2_decompressor_type_slots,
 };
 
+
+PyDoc_STRVAR(bzlib_version_info__doc__,
+"_bz2.bzlib_version_info\n\
+\n\
+Bzlib version information as a named tuple.");
+
+static PyStructSequence_Field bzlib_version_info_fields[] = {
+    {"major", "Major release number"},
+    {"minor", "Minor release number"},
+    {"patch", "Patch release number"},
+    {0}
+};
+
+static PyStructSequence_Desc bzlib_version_info_desc = {
+    "_bz2.bzlib_version_info",        /* name */
+    bzlib_version_info__doc__,        /* doc */
+    bzlib_version_info_fields,        /* fields */
+    3
+};
+
+/* BZ2_bzlibVersion() returns a string with a trailing suffix, for example
+   "1.0.8, 13-Jul-2019" for bzip2 or "1.1.0-libbz2-rs-sys-0.2.5" for
+   libbz2-rs.  sscanf() stops at the suffix; components which were not
+   parsed are left zero.  This is deliberate -- a zero is more useful
+   here than a hard error. */
+static PyObject *
+make_bzlib_version_info(PyTypeObject *type, const char *string)
+{
+    PyObject *version;
+    int pos = 0;
+    unsigned int major = 0, minor = 0, patch = 0;
+
+    sscanf(string, "%u.%u.%u", &major, &minor, &patch);
+
+    version = PyStructSequence_New(type);
+    if (version == NULL) {
+        return NULL;
+    }
+
+#define SetIntItem(VALUE) \
+    PyStructSequence_SET_ITEM(version, pos++, PyLong_FromUnsignedLong(VALUE)); 
\
+    if (PyErr_Occurred()) { \
+        Py_DECREF(version); \
+        return NULL; \
+    }
+
+    SetIntItem(major)
+    SetIntItem(minor)
+    SetIntItem(patch)
+#undef SetIntItem
+
+    return version;
+}
+
+
 /* Module initialization. */
 
 static int
@@ -771,6 +826,24 @@ _bz2_exec(PyObject *module)
         return -1;
     }
 
+    /* bzlib_version */
+    if (PyModule_Add(module, "bzlib_version",
+            PyUnicode_FromString(BZ2_bzlibVersion())) < 0)
+    {
+        return -1;
+    }
+    PyTypeObject *version_type;
+    version_type = PyStructSequence_NewType(&bzlib_version_info_desc);
+    if (version_type == NULL) {
+        return -1;
+    }
+    if (PyModule_Add(module, "bzlib_version_info",
+            make_bzlib_version_info(version_type, BZ2_bzlibVersion())) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    Py_DECREF(version_type);
     return 0;
 }
 
diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c
index 4335a8bb162414d..92784904f3c9930 100644
--- a/Modules/_lzmamodule.c
+++ b/Modules/_lzmamodule.c
@@ -1509,6 +1509,62 @@ _lzma__decode_filter_properties_impl(PyObject *module, 
lzma_vli filter_id,
     return result;
 }
 
+
+PyDoc_STRVAR(lzma_version_info__doc__,
+"_lzma.lzma_version_info\n\
+\n\
+Lzma version information as a named tuple.");
+
+static PyStructSequence_Field lzma_version_info_fields[] = {
+    {"major", "Major release number"},
+    {"minor", "Minor release number"},
+    {"patch", "Patch release number"},
+    {"stability", "'alpha', 'beta', or 'stable'"},
+    {0}
+};
+
+static PyStructSequence_Desc lzma_version_info_desc = {
+    "_lzma.lzma_version_info",        /* name */
+    lzma_version_info__doc__,         /* doc */
+    lzma_version_info_fields,         /* fields */
+    4
+};
+
+static PyObject *
+make_lzma_version_info(PyTypeObject *type, unsigned int number)
+{
+    PyObject *version;
+    int pos = 0;
+    unsigned int major = number / 10000000u;
+    unsigned int minor = (number % 10000000u) / 10000u;
+    unsigned int patch = (number % 10000u) / 10u;
+    unsigned int stability = number % 10u;
+    const char *stability_string = (stability == 0) ? "alpha"
+                                 : (stability == 1) ? "beta"
+                                 : "stable";
+
+    version = PyStructSequence_New(type);
+    if (version == NULL) {
+        return NULL;
+    }
+
+#define SetItem(VALUE) \
+    PyStructSequence_SET_ITEM(version, pos++, VALUE); \
+    if (PyErr_Occurred()) { \
+        Py_DECREF(version); \
+        return NULL; \
+    }
+
+    SetItem(PyLong_FromUnsignedLong(major))
+    SetItem(PyLong_FromUnsignedLong(minor))
+    SetItem(PyLong_FromUnsignedLong(patch))
+    SetItem(PyUnicode_FromString(stability_string))
+#undef SetItem
+
+    return version;
+}
+
+
 /* Some of our constants are more than 32 bits wide, so PyModule_AddIntConstant
    would not work correctly on platforms with 32-bit longs. */
 static int
@@ -1603,6 +1659,33 @@ lzma_exec(PyObject *module)
         return -1;
     }
 
+    /* lzma_version */
+    if (PyModule_Add(module, "LZMA_VERSION",
+                     PyUnicode_FromString(LZMA_VERSION_STRING)) < 0) {
+        return -1;
+    }
+    if (PyModule_Add(module, "lzma_version",
+                     PyUnicode_FromString(lzma_version_string())) < 0) {
+        return -1;
+    }
+    PyTypeObject *version_type;
+    version_type = PyStructSequence_NewType(&lzma_version_info_desc);
+    if (version_type == NULL) {
+        return -1;
+    }
+    if (PyModule_Add(module, "LZMA_VERSION_INFO",
+            make_lzma_version_info(version_type, LZMA_VERSION)) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    if (PyModule_Add(module, "lzma_version_info",
+            make_lzma_version_info(version_type, lzma_version_number())) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    Py_DECREF(version_type);
     return 0;
 }
 
diff --git a/Modules/_zstd/_zstdmodule.c b/Modules/_zstd/_zstdmodule.c
index c9ed1ff22bdbd7f..9bf9658a99f87c7 100644
--- a/Modules/_zstd/_zstdmodule.c
+++ b/Modules/_zstd/_zstdmodule.c
@@ -567,6 +567,55 @@ static PyMethodDef _zstd_methods[] = {
     {NULL, NULL}
 };
 
+PyDoc_STRVAR(zstd_version_info__doc__,
+"_zstd.zstd_version_info\n\
+\n\
+Zstd version information as a named tuple.");
+
+static PyStructSequence_Field zstd_version_info_fields[] = {
+    {"major", "Major release number"},
+    {"minor", "Minor release number"},
+    {"patch", "Patch release number"},
+    {0}
+};
+
+static PyStructSequence_Desc zstd_version_info_desc = {
+    "_zstd.zstd_version_info",        /* name */
+    zstd_version_info__doc__,         /* doc */
+    zstd_version_info_fields,         /* fields */
+    3
+};
+
+static PyObject *
+make_zstd_version_info(PyTypeObject *type, unsigned int number)
+{
+    PyObject *version;
+    int pos = 0;
+    unsigned int major = number / 10000u;
+    unsigned int minor = (number % 10000u) / 100u;
+    unsigned int patch = number % 100u;
+
+    version = PyStructSequence_New(type);
+    if (version == NULL) {
+        return NULL;
+    }
+
+#define SetIntItem(VALUE) \
+    PyStructSequence_SET_ITEM(version, pos++, PyLong_FromUnsignedLong(VALUE)); 
\
+    if (PyErr_Occurred()) { \
+        Py_DECREF(version); \
+        return NULL; \
+    }
+
+    SetIntItem(major)
+    SetIntItem(minor)
+    SetIntItem(patch)
+#undef SetIntItem
+
+    return version;
+}
+
+
 static int
 _zstd_exec(PyObject *m)
 {
@@ -619,15 +668,32 @@ do {                                                      
                   \
     }
 
     /* Add constants */
-    if (PyModule_AddIntConstant(m, "zstd_version_number",
-                                ZSTD_versionNumber()) < 0) {
+    if (PyModule_AddStringConstant(m, "ZSTD_VERSION",
+                                   ZSTD_VERSION_STRING) < 0) {
         return -1;
     }
-
     if (PyModule_AddStringConstant(m, "zstd_version",
                                    ZSTD_versionString()) < 0) {
         return -1;
     }
+    PyTypeObject *version_type;
+    version_type = PyStructSequence_NewType(&zstd_version_info_desc);
+    if (version_type == NULL) {
+        return -1;
+    }
+    if (PyModule_Add(m, "ZSTD_VERSION_INFO",
+        make_zstd_version_info(version_type, ZSTD_VERSION_NUMBER)) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    if (PyModule_Add(m, "zstd_version_info",
+        make_zstd_version_info(version_type, ZSTD_versionNumber())) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    Py_DECREF(version_type);
 
 #if ZSTD_VERSION_NUMBER >= 10500
     if (PyModule_AddIntConstant(m, "ZSTD_CLEVEL_DEFAULT",
diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c
index a7fefb1fec5de93..56c119db7d10c05 100644
--- a/Modules/zlibmodule.c
+++ b/Modules/zlibmodule.c
@@ -2077,6 +2077,77 @@ zlib_getattr(PyObject *self, PyObject *args)
     return NULL;
 }
 
+PyDoc_STRVAR(zlib_version_info__doc__,
+"zlib.zlib_version_info\n\
+\n\
+Zlib version information as a named tuple.");
+
+static PyStructSequence_Field zlib_version_info_fields[] = {
+    {"major", "Major release number"},
+    {"minor", "Minor release number"},
+    {"revision", "Revision release number"},
+    {"subversion", "Subversion release number"},
+    {0}
+};
+
+static PyStructSequence_Desc zlib_version_info_desc = {
+    "zlib.zlib_version_info",        /* name */
+    zlib_version_info__doc__,        /* doc */
+    zlib_version_info_fields,        /* fields */
+    4
+};
+
+#ifdef ZLIBNG_VERSION
+PyDoc_STRVAR(zlibng_version_info__doc__,
+"zlib.zlibng_version_info\n\
+\n\
+Zlib-ng version information as a named tuple.");
+
+static PyStructSequence_Field zlibng_version_info_fields[] = {
+    {"major", "Major release number"},
+    {"minor", "Minor release number"},
+    {"revision", "Revision release number"},
+    {0}
+};
+
+static PyStructSequence_Desc zlibng_version_info_desc = {
+    "zlib.zlibng_version_info",        /* name */
+    zlibng_version_info__doc__,        /* doc */
+    zlibng_version_info_fields,        /* fields */
+    3
+};
+#endif // ZLIBNG_VERSION
+
+/* Create a named tuple from the first *size* components of a version string
+   like "1.2.11" or "1.2.11.1".  sscanf() is expected to fail on trailing
+   garbage and on versions with fewer components (for example "1.2.0.f" or
+   "1.3.1.zlib-ng"); the components which were not parsed are left zero.
+   This is deliberate -- a zero is more useful here than a hard error. */
+static PyObject *
+make_version_info(PyTypeObject *type, const char *string, Py_ssize_t size)
+{
+    unsigned int components[4] = {0, 0, 0, 0};
+    assert(size <= (Py_ssize_t)Py_ARRAY_LENGTH(components));
+
+    sscanf(string, "%u.%u.%u.%u",
+           &components[0], &components[1], &components[2], &components[3]);
+
+    PyObject *version = PyStructSequence_New(type);
+    if (version == NULL) {
+        return NULL;
+    }
+    for (Py_ssize_t i = 0; i < size; i++) {
+        PyObject *item = PyLong_FromUnsignedLong(components[i]);
+        if (item == NULL) {
+            Py_DECREF(version);
+            return NULL;
+        }
+        PyStructSequence_SET_ITEM(version, i, item);
+    }
+    return version;
+}
+
+
 static PyMethodDef zlib_methods[] =
 {
     ZLIB_ADLER32_METHODDEF
@@ -2272,10 +2343,16 @@ zlib_exec(PyObject *mod)
 #ifdef Z_TREES // 1.2.3.4, only for inflate
     ZLIB_ADD_INT_MACRO(Z_TREES);
 #endif
+
+    /* zlib_version */
     if (PyModule_Add(mod, "ZLIB_VERSION",
                      PyUnicode_FromString(ZLIB_VERSION)) < 0) {
         return -1;
     }
+    if (PyModule_Add(mod, "zlib_version",
+                     PyUnicode_FromString(zlibVersion())) < 0) {
+        return -1;
+    }
     if (PyModule_Add(mod, "ZLIB_RUNTIME_VERSION",
                      PyUnicode_FromString(zlibVersion())) < 0) {
         return -1;
@@ -2285,6 +2362,37 @@ zlib_exec(PyObject *mod)
                      PyUnicode_FromString(ZLIBNG_VERSION)) < 0) {
         return -1;
     }
+#endif
+    PyTypeObject *version_type;
+    version_type = PyStructSequence_NewType(&zlib_version_info_desc);
+    if (version_type == NULL) {
+        return -1;
+    }
+    if (PyModule_Add(mod, "ZLIB_VERSION_INFO",
+            make_version_info(version_type, ZLIB_VERSION, 4)) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    if (PyModule_Add(mod, "zlib_version_info",
+            make_version_info(version_type, zlibVersion(), 4)) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    Py_DECREF(version_type);
+#ifdef ZLIBNG_VERSION
+    version_type = PyStructSequence_NewType(&zlibng_version_info_desc);
+    if (version_type == NULL) {
+        return -1;
+    }
+    if (PyModule_Add(mod, "ZLIBNG_VERSION_INFO",
+            make_version_info(version_type, ZLIBNG_VERSION, 3)) < 0)
+    {
+        Py_DECREF(version_type);
+        return -1;
+    }
+    Py_DECREF(version_type);
 #endif
     return 0;
 }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to