Skip to content

Drop support for Python 3.8 #585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ jobs:
# For available versions, see:
# https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
python-version:
- "3.8"
- "3.9"
- "3.9.12"
- "3.10"
Expand All @@ -49,7 +48,6 @@ jobs:
- "3.12.0"
- "3.13"
- "3.13.0"
- "pypy3.8"
- "pypy3.9"
- "pypy3.10"

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Unreleased

- Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos).

# Release 4.13.2 (April 10, 2025)

- Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a
Expand Down
2 changes: 1 addition & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Example usage::
Python version support
----------------------

``typing_extensions`` currently supports Python versions 3.8 and higher. In the future,
``typing_extensions`` currently supports Python versions 3.9 and higher. In the future,
support for older Python versions will be dropped some time after that version
reaches end of life.

Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ build-backend = "flit_core.buildapi"
[project]
name = "typing_extensions"
version = "4.13.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
description = "Backported and Experimental Type Hints for Python 3.9+"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.9"
license = "PSF-2.0"
license-files = ["LICENSE"]
keywords = [
Expand All @@ -34,7 +34,6 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down Expand Up @@ -63,7 +62,7 @@ exclude = []

[tool.ruff]
line-length = 90
target-version = "py38"
target-version = "py39"

[tool.ruff.lint]
select = [
Expand Down
100 changes: 29 additions & 71 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@

# Flags used to mark tests that only apply after a specific
# version of the typing module.
TYPING_3_9_0 = sys.version_info[:3] >= (3, 9, 0)
TYPING_3_10_0 = sys.version_info[:3] >= (3, 10, 0)

# 3.11 makes runtime type checks (_type_check) more lenient.
Expand Down Expand Up @@ -1779,8 +1778,7 @@ class C(Generic[T]): pass
self.assertIs(get_origin(List), list)
self.assertIs(get_origin(Tuple), tuple)
self.assertIs(get_origin(Callable), collections.abc.Callable)
if sys.version_info >= (3, 9):
self.assertIs(get_origin(list[int]), list)
self.assertIs(get_origin(list[int]), list)
self.assertIs(get_origin(list), None)
self.assertIs(get_origin(P.args), P)
self.assertIs(get_origin(P.kwargs), P)
Expand Down Expand Up @@ -1817,20 +1815,18 @@ class C(Generic[T]): pass
self.assertEqual(get_args(List), ())
self.assertEqual(get_args(Tuple), ())
self.assertEqual(get_args(Callable), ())
if sys.version_info >= (3, 9):
self.assertEqual(get_args(list[int]), (int,))
self.assertEqual(get_args(list[int]), (int,))
self.assertEqual(get_args(list), ())
if sys.version_info >= (3, 9):
# Support Python versions with and without the fix for
# https://bugs.python.org/issue42195
# The first variant is for 3.9.2+, the second for 3.9.0 and 1
self.assertIn(get_args(collections.abc.Callable[[int], str]),
(([int], str), ([[int]], str)))
self.assertIn(get_args(collections.abc.Callable[[], str]),
(([], str), ([[]], str)))
self.assertEqual(get_args(collections.abc.Callable[..., str]), (..., str))
# Support Python versions with and without the fix for
# https://bugs.python.org/issue42195
# The first variant is for 3.9.2+, the second for 3.9.0 and 1
self.assertIn(get_args(collections.abc.Callable[[int], str]),
(([int], str), ([[int]], str)))
self.assertIn(get_args(collections.abc.Callable[[], str]),
(([], str), ([[]], str)))
self.assertEqual(get_args(collections.abc.Callable[..., str]), (..., str))
P = ParamSpec('P')
# In 3.9 and lower we use typing_extensions's hacky implementation
# In 3.9 we use typing_extensions's hacky implementation
# of ParamSpec, which gets incorrectly wrapped in a list
self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)])
self.assertEqual(get_args(Required[int]), (int,))
Expand Down Expand Up @@ -3808,7 +3804,7 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ...
MemoizedFunc[[int, str, str]]

if sys.version_info >= (3, 10):
# These unfortunately don't pass on <=3.9,
# These unfortunately don't pass on 3.9,
# due to typing._type_check on older Python versions
X = MemoizedFunc[[int, str, str], T, T2]
self.assertEqual(X.__parameters__, (T, T2))
Expand Down Expand Up @@ -4553,7 +4549,7 @@ class PointDict3D(PointDict2D, total=False):
assert is_typeddict(PointDict2D) is True
assert is_typeddict(PointDict3D) is True

@skipUnless(HAS_FORWARD_MODULE, "ForwardRef.__forward_module__ was added in 3.9")
@skipUnless(HAS_FORWARD_MODULE, "ForwardRef.__forward_module__ was added in 3.9.7")
def test_get_type_hints_cross_module_subclass(self):
self.assertNotIn("_DoNotImport", globals())
self.assertEqual(
Expand Down Expand Up @@ -4696,11 +4692,9 @@ class WithImplicitAny(B):
with self.assertRaises(TypeError):
WithImplicitAny[str]

@skipUnless(TYPING_3_9_0, "Was changed in 3.9")
def test_non_generic_subscript(self):
# For backward compatibility, subscription works
# on arbitrary TypedDict types.
# (But we don't attempt to backport this misfeature onto 3.8.)
class TD(TypedDict):
a: T
A = TD[int]
Expand Down Expand Up @@ -5163,7 +5157,7 @@ class C:
A.x = 5
self.assertEqual(C.x, 5)

@skipIf(sys.version_info[:2] in ((3, 9), (3, 10)), "Waiting for bpo-46491 bugfix.")
@skipIf(sys.version_info[:2] == (3, 10), "Waiting for https://github.com/python/cpython/issues/90649 bugfix.")
def test_special_form_containment(self):
class C:
classvar: Annotated[ClassVar[int], "a decoration"] = 4
Expand Down Expand Up @@ -5475,21 +5469,20 @@ def test_valid_uses(self):
self.assertEqual(C2.__parameters__, (P, T))

# Test collections.abc.Callable too.
if sys.version_info[:2] >= (3, 9):
# Note: no tests for Callable.__parameters__ here
# because types.GenericAlias Callable is hardcoded to search
# for tp_name "TypeVar" in C. This was changed in 3.10.
C3 = collections.abc.Callable[P, int]
self.assertEqual(C3.__args__, (P, int))
C4 = collections.abc.Callable[P, T]
self.assertEqual(C4.__args__, (P, T))
# Note: no tests for Callable.__parameters__ here
# because types.GenericAlias Callable is hardcoded to search
# for tp_name "TypeVar" in C. This was changed in 3.10.
C3 = collections.abc.Callable[P, int]
self.assertEqual(C3.__args__, (P, int))
C4 = collections.abc.Callable[P, T]
self.assertEqual(C4.__args__, (P, T))

# ParamSpec instances should also have args and kwargs attributes.
# Note: not in dir(P) because of __class__ hacks
self.assertTrue(hasattr(P, 'args'))
self.assertTrue(hasattr(P, 'kwargs'))

@skipIf((3, 10, 0) <= sys.version_info[:3] <= (3, 10, 2), "Needs bpo-46676.")
@skipIf((3, 10, 0) <= sys.version_info[:3] <= (3, 10, 2), "Needs https://github.com/python/cpython/issues/90834.")
def test_args_kwargs(self):
P = ParamSpec('P')
P_2 = ParamSpec('P_2')
Expand Down Expand Up @@ -5649,8 +5642,6 @@ class ProtoZ(Protocol[P]):
G10 = klass[int, Concatenate[str, P]]
with self.subTest("Check invalid form substitution"):
self.assertEqual(G10.__parameters__, (P, ))
if sys.version_info < (3, 9):
self.skipTest("3.8 typing._type_subst does not support this substitution process")
H10 = G10[int]
if (3, 10) <= sys.version_info < (3, 11, 3):
self.skipTest("3.10-3.11.2 does not substitute Concatenate here")
Expand Down Expand Up @@ -5780,9 +5771,6 @@ def test_valid_uses(self):
T = TypeVar('T')
for callable_variant in (Callable, collections.abc.Callable):
with self.subTest(callable_variant=callable_variant):
if not TYPING_3_9_0 and callable_variant is collections.abc.Callable:
self.skipTest("Needs PEP 585")

C1 = callable_variant[Concatenate[int, P], int]
C2 = callable_variant[Concatenate[int, T, P], T]
self.assertEqual(C1.__origin__, C2.__origin__)
Expand Down Expand Up @@ -5830,7 +5818,7 @@ def test_invalid_uses(self):
):
Concatenate[(str,), P]

@skipUnless(TYPING_3_10_0, "Missing backport to <=3.9. See issue #48")
@skipUnless(TYPING_3_10_0, "Missing backport to 3.9. See issue #48")
def test_alias_subscription_with_ellipsis(self):
P = ParamSpec('P')
X = Callable[Concatenate[int, P], Any]
Expand Down Expand Up @@ -6813,7 +6801,6 @@ class Y(Generic[T], NamedTuple):
with self.assertRaisesRegex(TypeError, f'Too many {things}'):
G[int, str]

@skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9")
def test_non_generic_subscript_py39_plus(self):
# For backward compatibility, subscription works
# on arbitrary NamedTuple types.
Expand All @@ -6828,19 +6815,6 @@ class Group(NamedTuple):
self.assertIs(type(a), Group)
self.assertEqual(a, (1, [2]))

@skipIf(TYPING_3_9_0, "Test isn't relevant to 3.9+")
def test_non_generic_subscript_error_message_py38(self):
class Group(NamedTuple):
key: T
group: List[T]

with self.assertRaisesRegex(TypeError, 'not subscriptable'):
Group[int]

for attr in ('__args__', '__origin__', '__parameters__'):
with self.subTest(attr=attr):
self.assertFalse(hasattr(Group, attr))

def test_namedtuple_keyword_usage(self):
with self.assertWarnsRegex(
DeprecationWarning,
Expand Down Expand Up @@ -6959,21 +6933,13 @@ def test_copy_and_pickle(self):
def test_docstring(self):
self.assertIsInstance(NamedTuple.__doc__, str)

@skipUnless(TYPING_3_9_0, "NamedTuple was a class on 3.8 and lower")
def test_same_as_typing_NamedTuple_39_plus(self):
def test_same_as_typing_NamedTuple(self):
self.assertEqual(
set(dir(NamedTuple)) - {"__text_signature__"},
set(dir(typing.NamedTuple))
)
self.assertIs(type(NamedTuple), type(typing.NamedTuple))

@skipIf(TYPING_3_9_0, "tests are only relevant to <=3.8")
def test_same_as_typing_NamedTuple_38_minus(self):
self.assertEqual(
self.NestedEmployee.__annotations__,
self.NestedEmployee._field_types
)

def test_orig_bases(self):
T = TypeVar('T')

Expand Down Expand Up @@ -7235,11 +7201,8 @@ def test_bound_errors(self):
r"Bound must be a type\. Got \(1, 2\)\."):
TypeVar('X', bound=(1, 2))

# Technically we could run it on later versions of 3.8,
# but that's not worth the effort.
@skipUnless(TYPING_3_9_0, "Fix was not backported")
def test_missing__name__(self):
# See bpo-39942
# See https://github.com/python/cpython/issues/84123
code = ("import typing\n"
"T = typing.TypeVar('T')\n"
)
Expand Down Expand Up @@ -7420,9 +7383,8 @@ def test_allow_default_after_non_default_in_alias(self):
a1 = Callable[[T_default], T]
self.assertEqual(a1.__args__, (T_default, T))

if sys.version_info >= (3, 9):
a2 = dict[T_default, T]
self.assertEqual(a2.__args__, (T_default, T))
a2 = dict[T_default, T]
self.assertEqual(a2.__args__, (T_default, T))

a3 = typing.Dict[T_default, T]
self.assertEqual(a3.__args__, (T_default, T))
Expand Down Expand Up @@ -7602,7 +7564,6 @@ class D(B[str], float): pass
with self.assertRaisesRegex(TypeError, "Expected an instance of type"):
get_original_bases(object())

@skipUnless(TYPING_3_9_0, "PEP 585 is yet to be")
def test_builtin_generics(self):
class E(list[T]): pass
class F(list[int]): pass
Expand Down Expand Up @@ -8848,7 +8809,6 @@ def test_fwdref_value_is_cached(self):
self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str)
self.assertIs(evaluate_forward_ref(fr), str)

@skipUnless(TYPING_3_9_0, "Needs PEP 585 support")
def test_fwdref_with_owner(self):
self.assertEqual(
evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections),
Expand Down Expand Up @@ -8894,16 +8854,14 @@ class Y(Generic[Tx]):
with self.subTest("nested string of TypeVar"):
evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y})
self.assertEqual(get_origin(evaluated_ref2), Y)
if not TYPING_3_9_0:
self.skipTest("Nested string 'Tx' stays ForwardRef in 3.8")
self.assertEqual(get_args(evaluated_ref2), (Y[Tx],))

with self.subTest("nested string of TypeAliasType and alias"):
# NOTE: Using Y here works for 3.10
evaluated_ref3 = evaluate_forward_ref(typing.ForwardRef("""Y['Z["StrAlias"]']"""), locals={"Y": Y, "Z": Z, "StrAlias": str})
self.assertEqual(get_origin(evaluated_ref3), Y)
if sys.version_info[:2] in ((3,8), (3, 10)):
self.skipTest("Nested string 'StrAlias' is not resolved in 3.8 and 3.10")
if sys.version_info[:2] == (3, 10):
self.skipTest("Nested string 'StrAlias' is not resolved in 3.10")
self.assertEqual(get_args(evaluated_ref3), (Z[str],))

def test_invalid_special_forms(self):
Expand Down
Loading
Loading