From b508dd04d232e3e15f6324e721e32df051c1d437 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 18 Sep 2025 14:31:27 +0100 Subject: [PATCH 1/3] gh-118803: Make `ByteString` deprecations louder; remove `ByteString` from `typing.__all__` and `collections.abc.__all__` --- Doc/whatsnew/3.15.rst | 34 ++++++++++++++ Lib/_collections_abc.py | 11 ++++- Lib/test/libregrtest/refleak.py | 8 ++++ Lib/test/test_collections.py | 13 ++++- Lib/test/test_typing.py | 23 ++++++--- Lib/typing.py | 47 +++++++++++-------- ...-09-18-14-21-57.gh-issue-118803.2JPbto.rst | 15 ++++++ 7 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-18-14-21-57.gh-issue-118803.2JPbto.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d0d7f6dce142ed..424e23ab354245 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -289,6 +289,25 @@ New modules Improved modules ================ +collections.abc +--------------- + +* :class:`collections.abc.ByteString` has been removed from + ``collections.abc.__all__``. :class:`!collections.abc.ByteString` has been + deprecated since Python 3.12, and is scheduled for removal in Python 3.17. + +* The following statements now cause ``DeprecationWarning``\ s to be emitted at + runtime: + + * ``from collections.abc import ByteString`` + * ``import collections.abc; collections.abc.ByteString``. + + ``DeprecationWarning``\ s were already emitted if + :class:`collections.abc.ByteString` was subclassed or used as the second + argument to :func:`isinstance` or :func:`issubclass`, but warnings were not + previously emitted if it was merely imported or accessed from the + :mod:`!collections.abc` module. + dbm --- @@ -671,6 +690,21 @@ typing as it was incorrectly infered in runtime before. (Contributed by Nikita Sobolev in :gh:`137191`.) +* :class:`typing.ByteString` has been removed from ``typing.__all__``. + :class:`!typing.ByteString` has been deprecated since Python 3.9, and is + scheduled for removal in Python 3.17. + +* The following statements now cause ``DeprecationWarning``\ s to be emitted at + runtime: + + * ``from typing import ByteString`` + * ``import typing; typing.ByteString``. + + ``DeprecationWarning``\ s were already emitted if :class:`typing.ByteString` + was subclassed or used as the second argument to :func:`isinstance` or + :func:`issubclass`, but warnings were not previously emitted if it was merely + imported or accessed from the :mod:`!typing` module. + unicodedata ----------- diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 28427077127890..0d84c4bbc1c2c5 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -49,7 +49,7 @@ def _f(): pass "Mapping", "MutableMapping", "MappingView", "KeysView", "ItemsView", "ValuesView", "Sequence", "MutableSequence", - "ByteString", "Buffer", + "Buffer", ] # This module has been renamed from collections.abc to _collections_abc to @@ -1161,3 +1161,12 @@ def __iadd__(self, values): MutableSequence.register(list) MutableSequence.register(bytearray) + +_deprecated_ByteString = globals().pop("ByteString") + +def __getattr__(attr): + if attr == "ByteString": + import warnings + warnings._deprecated("collections.abc.ByteString", remove=(3, 17)) + return _deprecated_ByteString + raise AttributeError(f"module 'collections.abc' has no attribute {attr!r}") diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index 5c78515506df59..93e437d5cd7fa3 100644 --- a/Lib/test/libregrtest/refleak.py +++ b/Lib/test/libregrtest/refleak.py @@ -93,6 +93,12 @@ def runtest_refleak(test_name, test_func, for obj in abc.__subclasses__() + [abc]: abcs[obj] = _get_dump(obj)[0] + # `ByteString` is not included in `collections.abc.__all__` + with warnings.catch_warnings(action='ignore', category=DeprecationWarning): + ByteString = collections.abc.ByteString + for obj in ByteString.__subclasses__() + [ByteString]: + abcs[obj] = _get_dump(obj)[0] + # bpo-31217: Integer pool to get a single integer object for the same # value. The pool is used to prevent false alarm when checking for memory # block leaks. Fill the pool with values in -1000..1000 which are the most @@ -254,6 +260,8 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs, linecache_data): # Clear ABC registries, restoring previously saved ABC registries. abs_classes = [getattr(collections.abc, a) for a in collections.abc.__all__] + with warnings.catch_warnings(action='ignore', category=DeprecationWarning): + abs_classes.append(collections.abc.ByteString) abs_classes = filter(isabstract, abs_classes) for abc in abs_classes: for obj in abc.__subclasses__() + [abc]: diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 3dac736e0189b1..d9613d0466f5e9 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -12,6 +12,7 @@ import string import sys from test import support +from test.support.import_helper import import_fresh_module import types import unittest @@ -26,7 +27,7 @@ from collections.abc import Set, MutableSet from collections.abc import Mapping, MutableMapping, KeysView, ItemsView, ValuesView from collections.abc import Sequence, MutableSequence -from collections.abc import ByteString, Buffer +from collections.abc import Buffer class TestUserObjects(unittest.TestCase): @@ -1935,6 +1936,8 @@ def assert_index_same(seq1, seq2, index_args): nativeseq, seqseq, (letter, start, stop)) def test_ByteString(self): + with self.assertWarns(DeprecationWarning): + from collections.abc import ByteString for sample in [bytes, bytearray]: with self.assertWarns(DeprecationWarning): self.assertIsInstance(sample(), ByteString) @@ -1956,6 +1959,14 @@ class X(ByteString): pass # No metaclass conflict class Z(ByteString, Awaitable): pass + def test_ByteString_attribute_access(self): + collections_abc = import_fresh_module( + "collections.abc", + fresh=("collections", "_collections_abc") + ) + with self.assertWarns(DeprecationWarning): + collections_abc.ByteString + def test_Buffer(self): for sample in [bytes, bytearray, memoryview]: self.assertIsInstance(sample(b"x"), Buffer) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 6ea1f2a35d615d..f744a873711535 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -13,6 +13,7 @@ import pickle import re import sys +import warnings from unittest import TestCase, main, skip from unittest.mock import patch from copy import copy, deepcopy @@ -7500,14 +7501,19 @@ def test_mutablesequence(self): self.assertNotIsInstance((), typing.MutableSequence) def test_bytestring(self): + previous_typing_module = sys.modules.pop("typing", None) + self.addCleanup(sys.modules.__setitem__, "typing", previous_typing_module) + + with self.assertWarns(DeprecationWarning): + from typing import ByteString with self.assertWarns(DeprecationWarning): - self.assertIsInstance(b'', typing.ByteString) + self.assertIsInstance(b'', ByteString) with self.assertWarns(DeprecationWarning): - self.assertIsInstance(bytearray(b''), typing.ByteString) + self.assertIsInstance(bytearray(b''), ByteString) with self.assertWarns(DeprecationWarning): - class Foo(typing.ByteString): ... + class Foo(ByteString): ... with self.assertWarns(DeprecationWarning): - class Bar(typing.ByteString, typing.Awaitable): ... + class Bar(ByteString, typing.Awaitable): ... def test_list(self): self.assertIsSubclass(list, typing.List) @@ -10455,6 +10461,10 @@ def test_no_isinstance(self): class SpecialAttrsTests(BaseTestCase): def test_special_attrs(self): + with warnings.catch_warnings( + action='ignore', category=DeprecationWarning + ): + typing_ByteString = typing.ByteString cls_to_check = { # ABC classes typing.AbstractSet: 'AbstractSet', @@ -10463,7 +10473,7 @@ def test_special_attrs(self): typing.AsyncIterable: 'AsyncIterable', typing.AsyncIterator: 'AsyncIterator', typing.Awaitable: 'Awaitable', - typing.ByteString: 'ByteString', + typing_ByteString: 'ByteString', typing.Callable: 'Callable', typing.ChainMap: 'ChainMap', typing.Collection: 'Collection', @@ -10816,7 +10826,8 @@ def test_all_exported_names(self): # there's a few types and metaclasses that aren't exported not k.endswith(('Meta', '_contra', '_co')) and not k.upper() == k and - # but export all things that have __module__ == 'typing' + k not in {"ByteString"} and + # but export all other things that have __module__ == 'typing' getattr(v, '__module__', None) == typing.__name__ ) } diff --git a/Lib/typing.py b/Lib/typing.py index a1bf2c9cb09747..f8398c39ad90cc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -65,7 +65,6 @@ # ABCs (from collections.abc). 'AbstractSet', # collections.abc.Set. - 'ByteString', 'Container', 'ContextManager', 'Hashable', @@ -1603,21 +1602,6 @@ def __ror__(self, left): return Union[left, self] -class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True): - def __init__( - self, origin, nparams, *, removal_version, inst=True, name=None - ): - super().__init__(origin, nparams, inst=inst, name=name) - self._removal_version = removal_version - - def __instancecheck__(self, inst): - import warnings - warnings._deprecated( - f"{self.__module__}.{self._name}", remove=self._removal_version - ) - return super().__instancecheck__(inst) - - class _CallableGenericAlias(_NotIterable, _GenericAlias, _root=True): def __repr__(self): assert self._name == 'Callable' @@ -2805,9 +2789,6 @@ class Other(Leaf): # Error reported by type checker MutableMapping = _alias(collections.abc.MutableMapping, 2) Sequence = _alias(collections.abc.Sequence, 1) MutableSequence = _alias(collections.abc.MutableSequence, 1) -ByteString = _DeprecatedGenericAlias( - collections.abc.ByteString, 0, removal_version=(3, 17) # Not generic. -) # Tuple accepts variable number of parameters. Tuple = _TupleType(tuple, -1, inst=False, name='Tuple') Tuple.__doc__ = \ @@ -3799,6 +3780,34 @@ def __getattr__(attr): ) warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2) obj = _collect_type_parameters + elif attr == "ByteString": + import warnings + + warnings._deprecated("typing.ByteString", remove=(3, 17)) + + class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True): + def __init__( + self, origin, nparams, *, removal_version, inst=True, name=None + ): + super().__init__(origin, nparams, inst=inst, name=name) + self._removal_version = removal_version + + def __instancecheck__(self, inst): + import warnings + warnings._deprecated( + f"{self.__module__}.{self._name}", remove=self._removal_version + ) + return super().__instancecheck__(inst) + + with warnings.catch_warnings( + action="ignore", category=DeprecationWarning + ): + # Not generic + ByteString = globals()["ByteString"] = _DeprecatedGenericAlias( + collections.abc.ByteString, 0, removal_version=(3, 17) + ) + + return ByteString else: raise AttributeError(f"module {__name__!r} has no attribute {attr!r}") globals()[attr] = obj diff --git a/Misc/NEWS.d/next/Library/2025-09-18-14-21-57.gh-issue-118803.2JPbto.rst b/Misc/NEWS.d/next/Library/2025-09-18-14-21-57.gh-issue-118803.2JPbto.rst new file mode 100644 index 00000000000000..a70fd0f3b4f9c1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-18-14-21-57.gh-issue-118803.2JPbto.rst @@ -0,0 +1,15 @@ +:class:`collections.abc.ByteString` has been removed from +``collections.abc.__all__``, and :class:`typing.ByteString` has been removed +from ``typing.__all__``. The former has been deprecated since Python 3.12, +and the latter has been deprecated since Python 3.9. Both classes are +scheduled for removal in Python 3.17. + +Additionally, the following statements now cause ``DeprecationWarning``\ s to +be emitted at runtime: ``from collections.abc import ByteString``, ``from +typing import ByteString``, ``import collections.abc; +collections.abc.ByteString`` and ``import typing; typing.ByteString``. Both +classes already caused ``DeprecationWarning``\ s to be emitted if they were +subclassed or used as the second argument to ``isinstance()`` or +``issubclass()``, but they did not previously lead to +``DeprecationWarning``\ s if they were merely imported or accessed from their +respective modules. From fc787a51fb3b3848211d499faeebc0ef0dd43b23 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 18 Sep 2025 14:49:36 +0100 Subject: [PATCH 2/3] improvements --- Lib/_collections_abc.py | 1 + Lib/test/libregrtest/refleak.py | 3 ++- Lib/test/test_collections.py | 6 ++++++ Lib/typing.py | 9 ++++++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 0d84c4bbc1c2c5..8e343954d92519 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -1168,5 +1168,6 @@ def __getattr__(attr): if attr == "ByteString": import warnings warnings._deprecated("collections.abc.ByteString", remove=(3, 17)) + globals()["ByteString"] = _deprecated_ByteString return _deprecated_ByteString raise AttributeError(f"module 'collections.abc' has no attribute {attr!r}") diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index 93e437d5cd7fa3..e7da17e500ead9 100644 --- a/Lib/test/libregrtest/refleak.py +++ b/Lib/test/libregrtest/refleak.py @@ -96,7 +96,8 @@ def runtest_refleak(test_name, test_func, # `ByteString` is not included in `collections.abc.__all__` with warnings.catch_warnings(action='ignore', category=DeprecationWarning): ByteString = collections.abc.ByteString - for obj in ByteString.__subclasses__() + [ByteString]: + # Mypy doesn't even think `ByteString` is a class, hence the `type: ignore` + for obj in ByteString.__subclasses__() + [ByteString]: # type: ignore[attr-defined] abcs[obj] = _get_dump(obj)[0] # bpo-31217: Integer pool to get a single integer object for the same diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index d9613d0466f5e9..76995c52b1a3c2 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -1936,6 +1936,12 @@ def assert_index_same(seq1, seq2, index_args): nativeseq, seqseq, (letter, start, stop)) def test_ByteString(self): + previous_sys_modules = sys.modules.copy() + self.addCleanup(sys.modules.update, previous_sys_modules) + + for module in "collections", "_collections_abc", "collections.abc": + sys.modules.pop(module, None) + with self.assertWarns(DeprecationWarning): from collections.abc import ByteString for sample in [bytes, bytearray]: diff --git a/Lib/typing.py b/Lib/typing.py index f8398c39ad90cc..81f3f2df7279f8 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3783,7 +3783,14 @@ def __getattr__(attr): elif attr == "ByteString": import warnings - warnings._deprecated("typing.ByteString", remove=(3, 17)) + warnings._deprecated( + "typing.ByteString", + message=( + "{name!r} and 'collections.abc.ByteString' are deprecated " + "and slated for removal in Python {remove}" + ), + remove=(3, 17) + ) class _DeprecatedGenericAlias(_SpecialGenericAlias, _root=True): def __init__( From 9836443ce614639917b686420ab5fd64019d6079 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 18 Sep 2025 19:32:27 +0100 Subject: [PATCH 3/3] deprecation warnings for `issubclass()` too, why not --- Lib/test/test_typing.py | 4 ++++ Lib/typing.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f744a873711535..1c8b2978aa3f09 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -7510,6 +7510,10 @@ def test_bytestring(self): self.assertIsInstance(b'', ByteString) with self.assertWarns(DeprecationWarning): self.assertIsInstance(bytearray(b''), ByteString) + with self.assertWarns(DeprecationWarning): + self.assertIsSubclass(bytes, ByteString) + with self.assertWarns(DeprecationWarning): + self.assertIsSubclass(bytearray, ByteString) with self.assertWarns(DeprecationWarning): class Foo(ByteString): ... with self.assertWarns(DeprecationWarning): diff --git a/Lib/typing.py b/Lib/typing.py index 81f3f2df7279f8..df84e2c8764d9c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3806,6 +3806,13 @@ def __instancecheck__(self, inst): ) return super().__instancecheck__(inst) + def __subclasscheck__(self, cls): + import warnings + warnings._deprecated( + f"{self.__module__}.{self._name}", remove=self._removal_version + ) + return super().__subclasscheck__(cls) + with warnings.catch_warnings( action="ignore", category=DeprecationWarning ):