From c83d0032185ac63619f338e066e348146af0bb79 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 20 Apr 2026 19:02:23 +0300 Subject: [PATCH 01/10] 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 ef48addaba03e9c..26400537206d00d 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 c4dac339be66afc..384e92980e11430 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 8e4da0e0f099191..51ee12d0957d141 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 02/10] 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 ef539fa5d815e22..639767ea561983f 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 03/10] 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 639767ea561983f..41e157960a5ba2b 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 04/10] 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 26400537206d00d..24b2ed47d43485a 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 384e92980e11430..f67c6220a5d6b6f 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 41e157960a5ba2b..205b4b5e591de44 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 05/10] 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 51ee12d0957d141..d55ea6b8b3bff9e 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 06/10] 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 d55ea6b8b3bff9e..ff5cf70ee304700 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 07/10] 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 e17e8eabb7dbc90..b1d442e4778acae 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)}", From 03d4f21cc8feed9e20927b77aab415f46036c300 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 11 Jul 2026 21:19:33 -0400 Subject: [PATCH 08/10] Bump docs --- Doc/library/urllib.parse.rst | 10 +++++----- Doc/whatsnew/3.15.rst | 8 -------- Doc/whatsnew/3.16.rst | 8 ++++++++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst index 66339aae3214021..91000abf76debd7 100644 --- a/Doc/library/urllib.parse.rst +++ b/Doc/library/urllib.parse.rst @@ -225,7 +225,7 @@ or on combining URL components into a URL string. .. versionchanged:: 3.15 Added the *missing_as_none* parameter. - .. versionchanged:: 3.15 + .. versionchanged:: 3.16 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). @@ -325,7 +325,7 @@ 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 + .. versionchanged:: 3.16 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). @@ -389,7 +389,7 @@ or on combining URL components into a URL string. .. versionchanged:: 3.15 Added the *keep_empty* parameter. - .. versionchanged:: 3.15 + .. versionchanged:: 3.16 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). @@ -437,7 +437,7 @@ or on combining URL components into a URL string. Behavior updated to match the semantics defined in :rfc:`3986`. - .. versionchanged:: 3.15 + .. versionchanged:: 3.16 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). @@ -472,7 +472,7 @@ or on combining URL components into a URL string. .. versionchanged:: 3.15 Added the *missing_as_none* parameter. - .. versionchanged:: 3.15 + .. versionchanged:: 3.16 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 3446c49f266435c..d711ef5d82f0bdf 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -2309,14 +2309,6 @@ 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, 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`.) - * :mod:`webbrowser`: * :class:`!webbrowser.MacOSXOSAScript` is deprecated in favour of diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index da98f1ad6b9bc3b..686ec4c1e10d8f8 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -680,6 +680,14 @@ New deprecations open them one by one instead. (Contributed by Serhiy Storchaka in :gh:`152638`.) +* :mod:`urllib`: + + * 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`.) + .. Add deprecations above alphabetically, not here at the end. .. include:: ../deprecations/pending-removal-in-3.17.rst From a34c412eb120d005384787b6a703176bc9511fc5 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 11 Jul 2026 21:26:09 -0400 Subject: [PATCH 09/10] cosmetic --- Doc/whatsnew/3.15.rst | 1 + Lib/test/test_urlparse.py | 7 +++---- .../next/Library/2021-06-11-20-00-16.bpo-19094.rMRoIL.rst | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d711ef5d82f0bdf..8cba187bf31dd16 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -2309,6 +2309,7 @@ New deprecations :func:`issubclass`, but warnings were not previously emitted if it was merely imported or accessed from the :mod:`!typing` module. + * :mod:`webbrowser`: * :class:`!webbrowser.MacOSXOSAScript` is deprecated in favour of diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py index 7112e33b7a57186..fb20766c7a79350 100644 --- a/Lib/test/test_urlparse.py +++ b/Lib/test/test_urlparse.py @@ -1262,7 +1262,6 @@ def test_non_string_true_values_rejected(self): urllib.parse.urlsplit(1, b'http') def _check_result_type(self, str_type, str_args): - num_args = len(str_type._fields) bytes_type = str_type._encoded_counterpart self.assertIs(bytes_type._decoded_counterpart, str_type) bytes_args = tuple_encode(str_args) @@ -2011,10 +2010,10 @@ def test_falsey_deprecation(self): (urllib.parse.urlunparse, [[None, b'www.python.org', (), (), (), ()]]), (urllib.parse.urlunsplit, [['http', 0, '', '', '']]), ] - for callable, args in cases: - with self.subTest(callable=callable.__name__, args=args): + for func, args in cases: + with self.subTest(callable=func.__name__, args=args): with self.assertWarnsRegex(DeprecationWarning, "false values"): - callable(*args) + func(*args) def str_encode(s): diff --git a/Misc/NEWS.d/next/Library/2021-06-11-20-00-16.bpo-19094.rMRoIL.rst b/Misc/NEWS.d/next/Library/2021-06-11-20-00-16.bpo-19094.rMRoIL.rst index 51c864b91a5fe0e..60a29391d90a77b 100644 --- a/Misc/NEWS.d/next/Library/2021-06-11-20-00-16.bpo-19094.rMRoIL.rst +++ b/Misc/NEWS.d/next/Library/2021-06-11-20-00-16.bpo-19094.rMRoIL.rst @@ -1,3 +1,4 @@ -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 values other than other than strings, bytes, or ``None`` to +:mod:`urllib.parse` functions expecting strings or bytes now raises +:exc:`TypeError` if true or :exc:`DeprecationWarning` if false (to be +changed to :exc:`TypeError` in future versions of Python). From f5bc23297d6b8ff8dbce10e76f7eda56d50e4c4f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 12 Jul 2026 08:12:03 -0400 Subject: [PATCH 10/10] Resolve BytesWarning --- Lib/urllib/parse.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index 572a84ad72917ba..8141c81f517be55 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -124,7 +124,6 @@ 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: @@ -132,7 +131,7 @@ def _coerce_args(*args): else: if isinstance(arg, str) != str_input: raise TypeError("Cannot mix str and non-str arguments") - if arg is not None and str_input is False and not hasattr(arg, "decode"): + if not hasattr(arg, "decode") and str_input is False: raise TypeError(f"Expected a string, bytes, or None: got {type(arg)}") elif arg is not None and not isinstance(arg, str) and not hasattr(arg, "decode"): warnings.warn(