Skip to content
Draft
20 changes: 19 additions & 1 deletion Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,22 @@ These are not used in annotations. They are building blocks for creating generic

c = concatenate('one', b'two') # error: type variable 'A' can be either str or bytes in a function call, but not both

If a generic type is commonly generic over just one type you can use
``default`` to specify this type::

T = TypeVar("T", default=int)

class Box(Generic[T]):
def __init__(self, value: T | None = None):
self.value = value

reveal_type(Box()) # type is Box[int]
reveal_type(Box(value="Hello World!")) # type is Box[str]

A TypeVar without a default cannot follow a TypeVar with a default.

.. TODO add more about this

At runtime, ``isinstance(x, T)`` will raise :exc:`TypeError`. In general,
:func:`isinstance` and :func:`issubclass` should not be used with types.

Expand Down Expand Up @@ -1405,6 +1421,8 @@ These are not used in annotations. They are building blocks for creating generic

See :pep:`646` for more details on type variable tuples.

.. TODO docs on default

.. versionadded:: 3.11

.. data:: Unpack
Expand Down Expand Up @@ -1450,7 +1468,7 @@ These are not used in annotations. They are building blocks for creating generic

.. versionadded:: 3.11

.. class:: ParamSpec(name, *, bound=None, covariant=False, contravariant=False)
.. class:: ParamSpec(name, *, bound=None, default=..., covariant=False, contravariant=False)

Parameter specification variable. A specialized version of
:class:`type variables <TypeVar>`.
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__contains__)
STRUCT_FOR_ID(__copy__)
STRUCT_FOR_ID(__ctypes_from_outparam__)
STRUCT_FOR_ID(__default__)
STRUCT_FOR_ID(__del__)
STRUCT_FOR_ID(__delattr__)
STRUCT_FOR_ID(__delete__)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,14 @@ def test_union_parameter_chaining(self):
self.assertEqual((list[T] | list[S])[int, T], list[int] | list[T])
self.assertEqual((list[T] | list[S])[int, int], list[int])

def test_union_parameter_default_ordering(self):
T = typing.TypeVar("T")
U = typing.TypeVar("U", default=int)

self.assertEqual((list[U] | list[T]).__parameters__, (U, T))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this raise TypeError as the next test asserts?

with self.assertRaises(TypeError):
list[U] | list[T]

def test_union_parameter_substitution(self):
def eq(actual, expected, typed=True):
self.assertEqual(actual, expected)
Expand Down Expand Up @@ -996,6 +1004,18 @@ def __eq__(self, other):
with self.assertRaises(TypeError):
issubclass(int, type_)

def test_generic_alias_subclass_with_defaults(self):
T = typing.TypeVar("T")
U = typing.TypeVar("U", default=int)
class MyGeneric:
__class_getitem__ = classmethod(types.GenericAlias)

class Fine(MyGeneric[T, U]):
...

with self.assertRaises(TypeError):
class NonDefaultFollows(MyGeneric[U, T]): ...

def test_or_type_operator_with_bad_module(self):
class BadMeta(type):
__qualname__ = 'TypeVar'
Expand Down
91 changes: 47 additions & 44 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,35 @@
import contextlib
import abc
import collections
from collections import defaultdict
from functools import lru_cache, wraps
import contextlib
import inspect
import itertools
import pickle
import re
import sys
import warnings
from unittest import TestCase, main, skipUnless, skip
from unittest.mock import patch
from copy import copy, deepcopy

from typing import Any, NoReturn, Never, assert_never
from typing import overload, get_overloads, clear_overloads
from typing import TypeVar, TypeVarTuple, Unpack, AnyStr
from typing import T, KT, VT # Not in __all__.
from typing import Union, Optional, Literal
from typing import Tuple, List, Dict, MutableMapping
from typing import Callable
from typing import Generic, ClassVar, Final, final, Protocol
from typing import assert_type, cast, runtime_checkable
from typing import get_type_hints
from typing import get_origin, get_args
from typing import override
from typing import is_typeddict
from typing import reveal_type
from typing import dataclass_transform
from typing import no_type_check, no_type_check_decorator
from typing import Type
from typing import NamedTuple, NotRequired, Required, TypedDict
from typing import IO, TextIO, BinaryIO
from typing import Pattern, Match
from typing import Annotated, ForwardRef
from typing import Self, LiteralString
from typing import TypeAlias
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
from typing import TypeGuard
import abc
import textwrap
import types
import typing
import warnings
import weakref
import types

from test.support import import_helper, captured_stderr, cpython_only
from test import mod_generics_cache
from test import _typed_dict_helper

from collections import defaultdict
from copy import copy, deepcopy
from functools import lru_cache, wraps
from test import _typed_dict_helper, mod_generics_cache
from test.support import captured_stderr, cpython_only, import_helper
from typing import (IO, KT, VT, Annotated, Any, AnyStr, # Not in __all__.
BinaryIO, Callable, ClassVar, Concatenate, Dict, Final,
ForwardRef, Generic, List, Literal, LiteralString, Match,
MutableMapping, NamedTuple, Never, NoReturn, NotRequired,
Optional, ParamSpec, ParamSpecArgs, ParamSpecKwargs,
Pattern, Protocol, Required, Self, T, TextIO, Tuple, Type,
TypeAlias, TypedDict, TypeGuard, TypeVar, TypeVarTuple,
Union, Unpack, assert_never, assert_type, cast,
clear_overloads, dataclass_transform, final, get_args,
get_origin, get_overloads, get_type_hints, is_typeddict,
no_type_check, no_type_check_decorator, overload, override,
reveal_type, runtime_checkable)
from unittest import TestCase, main, skip, skipUnless
from unittest.mock import patch

CANNOT_SUBCLASS_TYPE = 'Cannot subclass special typing classes'
NOT_A_BASE_TYPE = "type 'typing.%s' is not an acceptable base type"
Expand Down Expand Up @@ -444,6 +427,22 @@ def test_bound_errors(self):
with self.assertRaises(TypeError):
TypeVar('X', str, float, bound=Employee)

def test_default_error(self):
with self.assertRaises(TypeError):
TypeVar('X', default=Union)

def test_default_ordering(self):
T = TypeVar("T")
U = TypeVar("U", default=int)
V = TypeVar("V", default=float)

class Foo(Generic[T, U]): ...
with self.assertRaises(TypeError):
class Bar(Generic[U, T]): ...

class Baz(Foo[V]): ...


def test_missing__name__(self):
# See bpo-39942
code = ("import typing\n"
Expand Down Expand Up @@ -3971,22 +3970,24 @@ def test_immutability_by_copy_and_pickle(self):
TPB = TypeVar('TPB', bound=int)
TPV = TypeVar('TPV', bytes, str)
PP = ParamSpec('PP')
for X in [TP, TPB, TPV, PP,
TD = TypeVar('TD', default=int)
for X in [TP, TPB, TPV, PP, TD,
List, typing.Mapping, ClassVar, typing.Iterable,
Union, Any, Tuple, Callable]:
with self.subTest(thing=X):
self.assertIs(copy(X), X)
self.assertIs(deepcopy(X), X)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
self.assertIs(pickle.loads(pickle.dumps(X, proto)), X)
del TP, TPB, TPV, PP
del TP, TPB, TPV, PP, TD

# Check that local type variables are copyable.
TL = TypeVar('TL')
TLB = TypeVar('TLB', bound=int)
TLV = TypeVar('TLV', bytes, str)
PL = ParamSpec('PL')
for X in [TL, TLB, TLV, PL]:
TDL = TypeVar('TDL', default=int)
for X in [TL, TLB, TLV, PL, TDL]:
with self.subTest(thing=X):
self.assertIs(copy(X), X)
self.assertIs(deepcopy(X), X)
Expand Down Expand Up @@ -4733,6 +4734,7 @@ def test_errors(self):
# We need this to make sure that `@no_type_check` respects `__module__` attr:
from test import ann_module8


@no_type_check
class NoTypeCheck_Outer:
Inner = ann_module8.NoTypeCheck_Outer.Inner
Expand Down Expand Up @@ -7342,7 +7344,7 @@ def stuff(a: BinaryIO) -> bytes:
def test_io_submodule(self):
with warnings.catch_warnings(record=True) as w:
warnings.filterwarnings("default", category=DeprecationWarning)
from typing.io import IO, TextIO, BinaryIO, __all__, __name__
from typing.io import IO, BinaryIO, TextIO, __all__, __name__
self.assertIs(IO, typing.IO)
self.assertIs(TextIO, typing.TextIO)
self.assertIs(BinaryIO, typing.BinaryIO)
Expand Down Expand Up @@ -8566,6 +8568,7 @@ class AllTests(BaseTestCase):

def test_all(self):
from typing import __all__ as a

# Just spot-check the first and last of every category.
self.assertIn('AbstractSet', a)
self.assertIn('ValuesView', a)
Expand Down
15 changes: 15 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ def _collect_parameters(args):
_collect_parameters((T, Callable[P, T])) == (T, P)
"""
parameters = []
seen_default = False
for t in args:
if isinstance(t, type):
# We don't want __parameters__ descriptor of a bare Python class.
Expand All @@ -273,10 +274,24 @@ def _collect_parameters(args):
parameters.append(collected)
elif hasattr(t, '__typing_subst__'):
if t not in parameters:
if t.__default__ is not None:
seen_default = True
elif seen_default:
raise TypeError(
f"TypeVarLike {t!r} without a default follows one with a default"
)

parameters.append(t)
else:
for x in getattr(t, '__parameters__', ()):
if x not in parameters:
if x.__default__ is not None:
seen_default = True
elif seen_default:
raise TypeError(
f"TypeVarLike {t!r} without a default follows one with a default"
)

parameters.append(x)
return tuple(parameters)

Expand Down
40 changes: 40 additions & 0 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,11 @@ _Py_make_parameters(PyObject *args)
if (parameters == NULL)
return NULL;
Py_ssize_t iparam = 0;
bool seen_default = false;
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
PyObject *t = PyTuple_GET_ITEM(args, iarg);
PyObject *subst;

// We don't want __parameters__ descriptor of a bare Python class.
if (PyType_Check(t)) {
continue;
Expand All @@ -226,6 +228,25 @@ _Py_make_parameters(PyObject *args)
return NULL;
}
if (subst) {
PyObject *default_;
bool does_not_contain = tuple_index(parameters, nargs, t) == -1;
if (does_not_contain) {
if (_PyObject_LookupAttr(t, &_Py_ID(__default__), &default_) < 0) {
Py_DECREF(default_);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't default_ uninitialized here? I think we need to DECREF it in the next branch instead.

Py_DECREF(subst);
return NULL;
}
if (!Py_IsNone(default_)) {
seen_default = true;
} else if (seen_default) {
return PyErr_Format(
PyExc_TypeError,
"type parameter %R without a default follows one with a default",
t
);
}
}

iparam += tuple_add(parameters, iparam, t);
Py_DECREF(subst);
}
Expand All @@ -248,7 +269,26 @@ _Py_make_parameters(PyObject *args)
}
}
for (Py_ssize_t j = 0; j < len2; j++) {
PyObject *default_;
PyObject *t2 = PyTuple_GET_ITEM(subparams, j);

bool does_not_contain = tuple_index(parameters, nargs, t2) == -1;
if (does_not_contain) {
if (_PyObject_LookupAttr(t2, &_Py_ID(__default__), &default_) < 0) {
Py_DECREF(default_);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar issues here

Py_DECREF(subst);
return NULL;
}
if (default_ && !Py_IsNone(default_)) {
seen_default = true;
} else if (seen_default) {
return PyErr_Format(
PyExc_TypeError,
"type parameter %R without a default follows one with a default",
t2
);
}
}
iparam += tuple_add(parameters, iparam, t2);
}
}
Expand Down