Skip to content

Commit 5a4973e

Browse files
bpo-46998: Allow subclassing Any at runtime (GH-31841)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent bb86d1d commit 5a4973e

File tree

6 files changed

+44
-31
lines changed

6 files changed

+44
-31
lines changed

Doc/library/typing.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,11 @@ These can be used as types in annotations and do not support ``[]``.
580580
* Every type is compatible with :data:`Any`.
581581
* :data:`Any` is compatible with every type.
582582

583+
.. versionchanged:: 3.11
584+
:data:`Any` can now be used as a base class. This can be useful for
585+
avoiding type checker errors with classes that can duck type anywhere or
586+
are highly dynamic.
587+
583588
.. data:: Never
584589

585590
The `bottom type <https://en.wikipedia.org/wiki/Bottom_type>`_,

Lib/test/test_functools.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2802,8 +2802,6 @@ def f(arg):
28022802
f.register(list[int] | str, lambda arg: "types.UnionTypes(types.GenericAlias)")
28032803
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
28042804
f.register(typing.List[float] | bytes, lambda arg: "typing.Union[typing.GenericAlias]")
2805-
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2806-
f.register(typing.Any, lambda arg: "typing.Any")
28072805

28082806
self.assertEqual(f([1]), "default")
28092807
self.assertEqual(f([1.0]), "default")
@@ -2823,8 +2821,6 @@ def f(arg):
28232821
f.register(list[int] | str)
28242822
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
28252823
f.register(typing.List[int] | str)
2826-
with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
2827-
f.register(typing.Any)
28282824

28292825
def test_register_genericalias_annotation(self):
28302826
@functools.singledispatch
@@ -2847,10 +2843,6 @@ def _(arg: list[int] | str):
28472843
@f.register
28482844
def _(arg: typing.List[float] | bytes):
28492845
return "typing.Union[typing.GenericAlias]"
2850-
with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
2851-
@f.register
2852-
def _(arg: typing.Any):
2853-
return "typing.Any"
28542846

28552847
self.assertEqual(f([1]), "default")
28562848
self.assertEqual(f([1.0]), "default")

Lib/test/test_pydoc.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,14 +1066,14 @@ def test_union_type(self):
10661066
self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc)
10671067

10681068
def test_special_form(self):
1069-
self.assertEqual(pydoc.describe(typing.Any), '_SpecialForm')
1070-
doc = pydoc.render_doc(typing.Any, renderer=pydoc.plaintext)
1069+
self.assertEqual(pydoc.describe(typing.NoReturn), '_SpecialForm')
1070+
doc = pydoc.render_doc(typing.NoReturn, renderer=pydoc.plaintext)
10711071
self.assertIn('_SpecialForm in module typing', doc)
1072-
if typing.Any.__doc__:
1073-
self.assertIn('Any = typing.Any', doc)
1074-
self.assertIn(typing.Any.__doc__.strip().splitlines()[0], doc)
1072+
if typing.NoReturn.__doc__:
1073+
self.assertIn('NoReturn = typing.NoReturn', doc)
1074+
self.assertIn(typing.NoReturn.__doc__.strip().splitlines()[0], doc)
10751075
else:
1076-
self.assertIn('Any = class _SpecialForm(_Final)', doc)
1076+
self.assertIn('NoReturn = class _SpecialForm(_Final)', doc)
10771077

10781078
def test_typing_pydoc(self):
10791079
def foo(data: typing.List[typing.Any],

Lib/test/test_typing.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ def test_any_instance_type_error(self):
8989
with self.assertRaises(TypeError):
9090
isinstance(42, Any)
9191

92-
def test_any_subclass_type_error(self):
93-
with self.assertRaises(TypeError):
94-
issubclass(Employee, Any)
95-
with self.assertRaises(TypeError):
96-
issubclass(Any, Employee)
97-
9892
def test_repr(self):
9993
self.assertEqual(repr(Any), 'typing.Any')
10094

@@ -104,13 +98,21 @@ def test_errors(self):
10498
with self.assertRaises(TypeError):
10599
Any[int] # Any is not a generic type.
106100

107-
def test_cannot_subclass(self):
108-
with self.assertRaises(TypeError):
109-
class A(Any):
110-
pass
111-
with self.assertRaises(TypeError):
112-
class A(type(Any)):
113-
pass
101+
def test_can_subclass(self):
102+
class Mock(Any): pass
103+
self.assertTrue(issubclass(Mock, Any))
104+
self.assertIsInstance(Mock(), Mock)
105+
106+
class Something: pass
107+
self.assertFalse(issubclass(Something, Any))
108+
self.assertNotIsInstance(Something(), Mock)
109+
110+
class MockSomething(Something, Mock): pass
111+
self.assertTrue(issubclass(MockSomething, Any))
112+
ms = MockSomething()
113+
self.assertIsInstance(ms, MockSomething)
114+
self.assertIsInstance(ms, Something)
115+
self.assertIsInstance(ms, Mock)
114116

115117
def test_cannot_instantiate(self):
116118
with self.assertRaises(TypeError):

Lib/typing.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,17 @@ def __getitem__(self, parameters):
429429
return self._getitem(self, *parameters)
430430

431431

432-
@_SpecialForm
433-
def Any(self, parameters):
432+
class _AnyMeta(type):
433+
def __instancecheck__(self, obj):
434+
if self is Any:
435+
raise TypeError("typing.Any cannot be used with isinstance()")
436+
return super().__instancecheck__(obj)
437+
438+
def __repr__(self):
439+
return "typing.Any"
440+
441+
442+
class Any(metaclass=_AnyMeta):
434443
"""Special type indicating an unconstrained type.
435444
436445
- Any is compatible with every type.
@@ -439,9 +448,13 @@ def Any(self, parameters):
439448
440449
Note that all the above statements are true from the point of view of
441450
static type checkers. At runtime, Any should not be used with instance
442-
or class checks.
451+
checks.
443452
"""
444-
raise TypeError(f"{self} is not subscriptable")
453+
def __new__(cls, *args, **kwargs):
454+
if cls is Any:
455+
raise TypeError("Any cannot be instantiated")
456+
return super().__new__(cls, *args, **kwargs)
457+
445458

446459
@_SpecialForm
447460
def NoReturn(self, parameters):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow subclassing of :class:`typing.Any`. Patch by Shantanu Jain.

0 commit comments

Comments
 (0)