https://github.com/python/cpython/commit/46107ad9da0add7aa5c0a899e159d89c1376d6be
commit: 46107ad9da0add7aa5c0a899e159d89c1376d6be
branch: main
author: Xiao Yuan <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-15T15:05:29Z
summary:

gh-92455: Respect case-sensitive mimetype suffixes (GH-148782)

files:
A Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst
M Doc/library/mimetypes.rst
M Lib/mimetypes.py
M Lib/test/test_mimetypes.py

diff --git a/Doc/library/mimetypes.rst b/Doc/library/mimetypes.rst
index f33098faf7d8a77..5c29fff146eef00 100644
--- a/Doc/library/mimetypes.rst
+++ b/Doc/library/mimetypes.rst
@@ -39,8 +39,8 @@ the information :func:`init` sets up.
    (e.g. :program:`compress` or :program:`gzip`). The encoding is suitable for 
use
    as a :mailheader:`Content-Encoding` header, **not** as a
    :mailheader:`Content-Transfer-Encoding` header. The mappings are table 
driven.
-   Encoding suffixes are case sensitive; type suffixes are first tried case
-   sensitively, then case insensitively.
+   Encoding suffixes are case-sensitive. Suffix mappings and type suffixes are
+   first tried case-sensitively, then case-insensitively.
 
    The optional *strict* argument is a flag specifying whether the list of 
known MIME types
    is limited to only the official types `registered with IANA
@@ -131,6 +131,8 @@ behavior of the module.
    is already known the extension will be added to the list of known 
extensions.
    Valid extensions are empty or start with a ``'.'``.
 
+   Registered lower-case extensions are matched case-insensitively.
+
    When *strict* is ``True`` (the default), the mapping will be added to the
    official MIME types, otherwise to the non-standard ones.
 
@@ -312,6 +314,8 @@ than one MIME-type database; it provides an interface 
similar to the one of the
       extension is already known, the new type will replace the old one. When 
the type
       is already known the extension will be added to the list of known 
extensions.
 
+      Registered lower-case extensions are matched case-insensitively.
+
       When *strict* is ``True`` (the default), the mapping will be added to the
       official MIME types, otherwise to the non-standard ones.
 
diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py
index 15e8c0a437bfd93..4339ef5a61397dd 100644
--- a/Lib/mimetypes.py
+++ b/Lib/mimetypes.py
@@ -86,6 +86,9 @@ def add_type(self, type, ext, strict=True):
         is already known the extension will be added
         to the list of known extensions.
 
+        Registered lower-case extensions are matched
+        case-insensitively.
+
         If strict is true, information will be added to
         list of standard types, else to the list of non-standard
         types.
@@ -172,23 +175,33 @@ def guess_file_type(self, path, *, strict=True):
 
     def _guess_file_type(self, path, strict, splitext):
         base, ext = splitext(path)
-        while (ext_lower := ext.lower()) in self.suffix_map:
-            base, ext = splitext(base + self.suffix_map[ext_lower])
+        while True:
+            if ext in self.suffix_map:
+                suffix = self.suffix_map[ext]
+            elif (ext_lower := ext.lower()) in self.suffix_map:
+                suffix = self.suffix_map[ext_lower]
+            else:
+                break
+            base, ext = splitext(base + suffix)
         # encodings_map is case sensitive
         if ext in self.encodings_map:
             encoding = self.encodings_map[ext]
             base, ext = splitext(base)
         else:
             encoding = None
-        ext = ext.lower()
+        ext_lower = ext.lower()
         types_map = self.types_map[True]
         if ext in types_map:
             return types_map[ext], encoding
+        if ext_lower in types_map:
+            return types_map[ext_lower], encoding
         elif strict:
             return None, encoding
         types_map = self.types_map[False]
         if ext in types_map:
             return types_map[ext], encoding
+        if ext_lower in types_map:
+            return types_map[ext_lower], encoding
         else:
             return None, encoding
 
@@ -386,6 +399,9 @@ def add_type(type, ext, strict=True):
     is already known the extension will be added
     to the list of known extensions.
 
+    Registered lower-case extensions are matched
+    case-insensitively.
+
     If strict is true, information will be added to
     list of standard types, else to the list of non-standard
     types.
diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py
index 1a3b49b87b121f2..19983fa3fa7628d 100644
--- a/Lib/test/test_mimetypes.py
+++ b/Lib/test/test_mimetypes.py
@@ -287,6 +287,50 @@ def test_case_sensitivity(self):
         eq(self.db.guess_file_type("foobar.tar.z"), (None, None))
         eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None))
 
+    def test_suffix_map_case_sensitive_preferred(self):
+        self.db.suffix_map[".TEST-SUFFIX"] = ".tar.gz"
+        self.db.suffix_map[".test-suffix"] = ".tar.xz"
+        self.assertEqual(
+            self.db.guess_file_type("example.TEST-SUFFIX"),
+            ("application/x-tar", "gzip"),
+        )
+        self.assertEqual(
+            self.db.guess_file_type("example.test-suffix"),
+            ("application/x-tar", "xz"),
+        )
+
+    def test_added_types_case_sensitive_preferred(self):
+        self.db.add_type("text/x-test-uppercase-r", ".R")
+        self.db.add_type("text/x-test-lowercase-r", ".r")
+        self.assertEqual(
+            self.db.guess_file_type("example.R"),
+            ("text/x-test-uppercase-r", None),
+        )
+        self.assertEqual(
+            self.db.guess_file_type("example.r"),
+            ("text/x-test-lowercase-r", None),
+        )
+        self.db.add_type("text/x-test-uppercase-non-strict",
+                         ".NON-STRICT-EXT", strict=False)
+        self.db.add_type("text/x-test-lowercase-non-strict",
+                         ".non-strict-ext", strict=False)
+        self.assertEqual(
+            self.db.guess_file_type("example.NON-STRICT-EXT"),
+            (None, None),
+        )
+        self.assertEqual(
+            self.db.guess_file_type("example.non-strict-ext"),
+            (None, None),
+        )
+        self.assertEqual(
+            self.db.guess_file_type("example.NON-STRICT-EXT", strict=False),
+            ("text/x-test-uppercase-non-strict", None),
+        )
+        self.assertEqual(
+            self.db.guess_file_type("example.non-strict-ext", strict=False),
+            ("text/x-test-lowercase-non-strict", None),
+        )
+
     def test_default_data(self):
         eq = self.assertEqual
         eq(self.db.guess_file_type("foo.html"), ("text/html", None))
diff --git 
a/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst 
b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst
new file mode 100644
index 000000000000000..8d2a11cb7761377
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst
@@ -0,0 +1,3 @@
+Fix :mod:`mimetypes` to prefer case-sensitive matches for suffix mappings and
+MIME type suffixes before falling back to case-insensitive matches.
+Contributed by Xiao Yuan.

_______________________________________________
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