From c83d0032185ac63619f338e066e348146af0bb79 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 20 Apr 2026 19:02:23 +0300 Subject: [PATCH 1/7] bpo-19094: Raise TypeError in urljoin(), urlparse(), and urlsplit() for inappropriate types --- Doc/library/urllib.parse.rst | 25 +++++++++++++++++ Doc/whatsnew/3.15.rst | 6 ++++ Lib/test/test_urlparse.py | 28 +++++++++++++++++++ Lib/urllib/parse.py | 14 ++++++++-- .../2021-06-11-20-00-16.bpo-19094.rMRoIL.rst | 3 ++ 5 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-06-11-20-00-16.bpo-19094.rMRoIL.rst diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst index ef48addaba03e9..26400537206d00 100644 --- a/Doc/library/urllib.parse.rst +++ b/Doc/library/urllib.parse.rst @@ -220,6 +220,11 @@ or on combining URL components into a URL string. .. versionchanged:: 3.15 Added the *missing_as_none* parameter. + .. versionchanged:: 3.15 + Values for ``url`` and ``scheme`` other than strings or bytes raise + :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be + changed to :exc:`TypeError` in future versions of Python). + .. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser @@ -315,6 +320,11 @@ or on combining URL components into a URL string. query parameter separator. This has been changed to allow only a single separator key, with ``&`` as the default separator. + .. versionchanged:: 3.15 + Values for ``qs`` and ``separator`` other than strings or bytes raise + :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be + changed to :exc:`TypeError` in future versions of Python). + .. function:: urlunsplit(parts) urlunsplit(parts, *, keep_empty) @@ -374,6 +384,11 @@ or on combining URL components into a URL string. .. versionchanged:: 3.15 Added the *keep_empty* parameter. + .. versionchanged:: 3.15 + Items in ``parts`` other than strings or bytes raise + :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be + changed to :exc:`TypeError` in future versions of Python). + .. function:: urljoin(base, url, allow_fragments=True) @@ -417,6 +432,11 @@ or on combining URL components into a URL string. Behavior updated to match the semantics defined in :rfc:`3986`. + .. versionchanged:: 3.15 + Values for ``base`` and ``url`` other than strings or bytes raise + :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be + changed to :exc:`TypeError` in future versions of Python). + .. function:: urldefrag(url, *, missing_as_none=False) @@ -447,6 +467,11 @@ or on combining URL components into a URL string. .. versionchanged:: 3.15 Added the *missing_as_none* parameter. + .. versionchanged:: 3.15 + Values other than strings or bytes raise + :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be + changed to :exc:`TypeError` in future versions of Python). + .. function:: unwrap(url) Extract the url from a wrapped URL (that is, a string formatted as diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index c4dac339be66af..384e92980e1143 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1761,6 +1761,12 @@ New deprecations :func:`issubclass`, but warnings were not previously emitted if it was merely imported or accessed from the :mod:`!typing` module. +* :mod:`urllib`: + + * Providing anything but a string or bytes object to :mod:`urllib.parse` + functions expecting strings or bytes now raises :exc:`DeprecationWarning` + if the value tests false, or :exc:`TypeError` if it tests true. + (Contributed by Jacob Walls in :issue:`19094`.) * ``__version__`` diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py index 8e4da0e0f09919..51ee12d0957d14 100644 --- a/Lib/test/test_urlparse.py +++ b/Lib/test/test_urlparse.py @@ -1255,7 +1255,14 @@ def test_mixed_types_rejected(self): with self.assertRaisesRegex(TypeError, "Cannot mix str"): urllib.parse.urljoin(b"http://python.org", "http://python.org") + def test_non_string_true_values_rejected(self): + # True values raise informative TypeErrors + msg = "Expected a string or bytes object: got Date: Mon, 20 Apr 2026 19:11:49 +0300 Subject: [PATCH 2/7] fix missing elif --- Lib/urllib/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index ef539fa5d815e2..639767ea561983 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -131,7 +131,7 @@ def _coerce_args(*args): else: if isinstance(arg, str) != str_input: raise TypeError("Cannot mix str and non-str arguments") - if not hasattr(arg, 'decode'): + elif not hasattr(arg, 'decode'): if arg: raise TypeError(f"Expected a string or bytes object: got {type(arg)}") else: From 2bf57f3803b4b9a3db80ce12ed4e253396672cbc Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 20 Apr 2026 19:13:17 +0300 Subject: [PATCH 3/7] fix condition --- Lib/urllib/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index 639767ea561983..41e157960a5ba2 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -131,7 +131,7 @@ def _coerce_args(*args): else: if isinstance(arg, str) != str_input: raise TypeError("Cannot mix str and non-str arguments") - elif not hasattr(arg, 'decode'): + elif str_input is False and not hasattr(arg, 'decode'): if arg: raise TypeError(f"Expected a string or bytes object: got {type(arg)}") else: From bffbd321c4171345b40841cd06d9b687fca69968 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 20 Apr 2026 16:03:22 -0400 Subject: [PATCH 4/7] Allow NoneType --- Doc/library/urllib.parse.rst | 22 +++++++++++----------- Doc/whatsnew/3.15.rst | 7 ++++--- Lib/urllib/parse.py | 16 ++++++++-------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst index 26400537206d00..24b2ed47d43485 100644 --- a/Doc/library/urllib.parse.rst +++ b/Doc/library/urllib.parse.rst @@ -221,9 +221,9 @@ or on combining URL components into a URL string. Added the *missing_as_none* parameter. .. versionchanged:: 3.15 - Values for ``url`` and ``scheme`` other than strings or bytes raise - :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be - changed to :exc:`TypeError` in future versions of Python). + Values for ``url`` and ``scheme`` other than strings, bytes, or ``None`` + raise :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to + be changed to :exc:`TypeError` in future versions of Python). .. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser @@ -321,9 +321,9 @@ or on combining URL components into a URL string. separator key, with ``&`` as the default separator. .. versionchanged:: 3.15 - Values for ``qs`` and ``separator`` other than strings or bytes raise - :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be - changed to :exc:`TypeError` in future versions of Python). + Values for ``qs`` and ``separator`` other than strings, bytes, or + ``None`` raise :exc:`TypeError` if true or :exc:`DeprecationWarning` if + false (to be changed to :exc:`TypeError` in future versions of Python). .. function:: urlunsplit(parts) @@ -385,7 +385,7 @@ or on combining URL components into a URL string. Added the *keep_empty* parameter. .. versionchanged:: 3.15 - Items in ``parts`` other than strings or bytes raise + Items in ``parts`` other than strings, bytes, or ``None`` raise :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be changed to :exc:`TypeError` in future versions of Python). @@ -433,9 +433,9 @@ or on combining URL components into a URL string. Behavior updated to match the semantics defined in :rfc:`3986`. .. versionchanged:: 3.15 - Values for ``base`` and ``url`` other than strings or bytes raise - :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be - changed to :exc:`TypeError` in future versions of Python). + Values for ``base`` and ``url`` other than strings, bytes, or ``None`` + raise :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to + be changed to :exc:`TypeError` in future versions of Python). .. function:: urldefrag(url, *, missing_as_none=False) @@ -468,7 +468,7 @@ or on combining URL components into a URL string. Added the *missing_as_none* parameter. .. versionchanged:: 3.15 - Values other than strings or bytes raise + Values other than other than strings, bytes, or ``None`` raise :exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be changed to :exc:`TypeError` in future versions of Python). diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 384e92980e1143..f67c6220a5d6b6 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1763,9 +1763,10 @@ New deprecations * :mod:`urllib`: - * Providing anything but a string or bytes object to :mod:`urllib.parse` - functions expecting strings or bytes now raises :exc:`DeprecationWarning` - if the value tests false, or :exc:`TypeError` if it tests true. + * Providing anything but a string, bytes object, or ``None`` to + :mod:`urllib.parse` functions expecting strings or bytes now raises + :exc:`DeprecationWarning` if the value tests false, or :exc:`TypeError` if + it tests true. (Contributed by Jacob Walls in :issue:`19094`.) * ``__version__`` diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index 41e157960a5ba2..205b4b5e591de4 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -124,6 +124,7 @@ def _coerce_args(*args): # - noop for str inputs # - encoding function otherwise str_input = None + empty_values = {"", b"", None} for arg in args: if arg: if str_input is None: @@ -131,14 +132,13 @@ def _coerce_args(*args): else: if isinstance(arg, str) != str_input: raise TypeError("Cannot mix str and non-str arguments") - elif str_input is False and not hasattr(arg, 'decode'): - if arg: - raise TypeError(f"Expected a string or bytes object: got {type(arg)}") - else: - warnings.warn( - f"Providing false values other than strings or bytes " - f"to urllib.parse is deprecated: got {type(arg)}", - DeprecationWarning, stacklevel=3) + if str_input is False and arg is not None and not hasattr(arg, "decode"): + raise TypeError(f"Expected a string, bytes, or None: got {type(arg)}") + elif arg not in empty_values: + warnings.warn( + f"Providing false values other than empty strings, bytes, or" + f"None to urllib.parse is deprecated: got {type(arg)}", + DeprecationWarning, stacklevel=3) if str_input is None: for arg in args: if arg is not None: From 01809a91425de2f64b86eda012703d2afd94823f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 20 Apr 2026 16:06:16 -0400 Subject: [PATCH 5/7] Relax assertion --- Lib/test/test_urlparse.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py index 51ee12d0957d14..d55ea6b8b3bff9 100644 --- a/Lib/test/test_urlparse.py +++ b/Lib/test/test_urlparse.py @@ -1995,10 +1995,6 @@ def test_to_bytes_deprecation(self): 'urllib.parse.to_bytes() is deprecated as of 3.8') def test_falsey_deprecation(self): - pattern = ( - "Providing false values other than strings or bytes to urllib.parse " - "is deprecated: got Date: Mon, 20 Apr 2026 16:19:16 -0400 Subject: [PATCH 6/7] fixups --- Lib/test/test_urlparse.py | 8 ++++---- Lib/urllib/parse.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py index d55ea6b8b3bff9..ff5cf70ee30470 100644 --- a/Lib/test/test_urlparse.py +++ b/Lib/test/test_urlparse.py @@ -1257,7 +1257,7 @@ def test_mixed_types_rejected(self): def test_non_string_true_values_rejected(self): # True values raise informative TypeErrors - msg = "Expected a string or bytes object: got Date: Tue, 21 Apr 2026 06:52:43 -0400 Subject: [PATCH 7/7] fixups --- Lib/urllib/parse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index e17e8eabb7dbc9..b1d442e4778aca 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -132,9 +132,9 @@ def _coerce_args(*args): else: if isinstance(arg, str) != str_input: raise TypeError("Cannot mix str and non-str arguments") - if str_input is False and arg is not None and not hasattr(arg, "decode"): + if arg is not None and str_input is False and not hasattr(arg, "decode"): raise TypeError(f"Expected a string, bytes, or None: got {type(arg)}") - elif arg is not None and arg != "" and arg != b"": + elif arg is not None and not isinstance(arg, str) and not hasattr(arg, "decode"): warnings.warn( f"Providing false values other than empty strings, bytes, or" f"None to urllib.parse is deprecated: got {type(arg)}",