Skip to content

Commit ad56340

Browse files
authored
gh-105566: Deprecate unusual ways of creating typing.NamedTuple classes (#105609)
Deprecate creating a typing.NamedTuple class using keyword arguments to denote the fields (`NT = NamedTuple("NT", x=int, y=str)`). This will be disallowed in Python 3.15. Use the class-based syntax or the functional syntax instead. Two methods of creating `NamedTuple` classes with 0 fields using the functional syntax are also deprecated, and will be disallowed in Python 3.15: `NT = NamedTuple("NT")` and `NT = NamedTuple("NT", None)`. To create a `NamedTuple` class with 0 fields, either use `class NT(NamedTuple): pass` or `NT = NamedTuple("NT", [])`.
1 parent fc8037d commit ad56340

File tree

5 files changed

+153
-12
lines changed

5 files changed

+153
-12
lines changed

Doc/library/typing.rst

+13
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,19 @@ These are not used in annotations. They are building blocks for declaring types.
20382038
.. versionchanged:: 3.11
20392039
Added support for generic namedtuples.
20402040

2041+
.. deprecated-removed:: 3.13 3.15
2042+
The undocumented keyword argument syntax for creating NamedTuple classes
2043+
(``NT = NamedTuple("NT", x=int)``) is deprecated, and will be disallowed
2044+
in 3.15. Use the class-based syntax or the functional syntax instead.
2045+
2046+
.. deprecated-removed:: 3.13 3.15
2047+
When using the functional syntax to create a NamedTuple class, failing to
2048+
pass a value to the 'fields' parameter (``NT = NamedTuple("NT")``) is
2049+
deprecated. Passing ``None`` to the 'fields' parameter
2050+
(``NT = NamedTuple("NT", None)``) is also deprecated. Both will be
2051+
disallowed in Python 3.15. To create a NamedTuple class with 0 fields,
2052+
use ``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``.
2053+
20412054
.. class:: NewType(name, tp)
20422055

20432056
Helper class to create low-overhead :ref:`distinct types <distinct>`.

Doc/whatsnew/3.13.rst

+11
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ Deprecated
141141
methods of the :class:`wave.Wave_read` and :class:`wave.Wave_write` classes.
142142
They will be removed in Python 3.15.
143143
(Contributed by Victor Stinner in :gh:`105096`.)
144+
* Creating a :class:`typing.NamedTuple` class using keyword arguments to denote
145+
the fields (``NT = NamedTuple("NT", x=int, y=int)``) is deprecated, and will
146+
be disallowed in Python 3.15. Use the class-based syntax or the functional
147+
syntax instead. (Contributed by Alex Waygood in :gh:`105566`.)
148+
* When using the functional syntax to create a :class:`typing.NamedTuple`
149+
class, failing to pass a value to the 'fields' parameter
150+
(``NT = NamedTuple("NT")``) is deprecated. Passing ``None`` to the 'fields'
151+
parameter (``NT = NamedTuple("NT", None)``) is also deprecated. Both will be
152+
disallowed in Python 3.15. To create a NamedTuple class with 0 fields, use
153+
``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``.
154+
(Contributed by Alex Waygood in :gh:`105566`.)
144155

145156
* :mod:`array`'s ``'u'`` format code, deprecated in docs since Python 3.3,
146157
emits :exc:`DeprecationWarning` since 3.13

Lib/test/test_typing.py

+74-9
Original file line numberDiff line numberDiff line change
@@ -7189,18 +7189,47 @@ class Group(NamedTuple):
71897189
self.assertEqual(a, (1, [2]))
71907190

71917191
def test_namedtuple_keyword_usage(self):
7192-
LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)
7192+
with self.assertWarnsRegex(
7193+
DeprecationWarning,
7194+
"Creating NamedTuple classes using keyword arguments is deprecated"
7195+
):
7196+
LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)
7197+
71937198
nick = LocalEmployee('Nick', 25)
71947199
self.assertIsInstance(nick, tuple)
71957200
self.assertEqual(nick.name, 'Nick')
71967201
self.assertEqual(LocalEmployee.__name__, 'LocalEmployee')
71977202
self.assertEqual(LocalEmployee._fields, ('name', 'age'))
71987203
self.assertEqual(LocalEmployee.__annotations__, dict(name=str, age=int))
7199-
with self.assertRaises(TypeError):
7204+
7205+
with self.assertRaisesRegex(
7206+
TypeError,
7207+
"Either list of fields or keywords can be provided to NamedTuple, not both"
7208+
):
72007209
NamedTuple('Name', [('x', int)], y=str)
72017210

7211+
with self.assertRaisesRegex(
7212+
TypeError,
7213+
"Either list of fields or keywords can be provided to NamedTuple, not both"
7214+
):
7215+
NamedTuple('Name', [], y=str)
7216+
7217+
with self.assertRaisesRegex(
7218+
TypeError,
7219+
(
7220+
r"Cannot pass `None` as the 'fields' parameter "
7221+
r"and also specify fields using keyword arguments"
7222+
)
7223+
):
7224+
NamedTuple('Name', None, x=int)
7225+
72027226
def test_namedtuple_special_keyword_names(self):
7203-
NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)
7227+
with self.assertWarnsRegex(
7228+
DeprecationWarning,
7229+
"Creating NamedTuple classes using keyword arguments is deprecated"
7230+
):
7231+
NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)
7232+
72047233
self.assertEqual(NT.__name__, 'NT')
72057234
self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields'))
72067235
a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)])
@@ -7210,12 +7239,32 @@ def test_namedtuple_special_keyword_names(self):
72107239
self.assertEqual(a.fields, [('bar', tuple)])
72117240

72127241
def test_empty_namedtuple(self):
7213-
NT = NamedTuple('NT')
7242+
expected_warning = re.escape(
7243+
"Failing to pass a value for the 'fields' parameter is deprecated "
7244+
"and will be disallowed in Python 3.15. "
7245+
"To create a NamedTuple class with 0 fields "
7246+
"using the functional syntax, "
7247+
"pass an empty list, e.g. `NT1 = NamedTuple('NT1', [])`."
7248+
)
7249+
with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
7250+
NT1 = NamedTuple('NT1')
7251+
7252+
expected_warning = re.escape(
7253+
"Passing `None` as the 'fields' parameter is deprecated "
7254+
"and will be disallowed in Python 3.15. "
7255+
"To create a NamedTuple class with 0 fields "
7256+
"using the functional syntax, "
7257+
"pass an empty list, e.g. `NT2 = NamedTuple('NT2', [])`."
7258+
)
7259+
with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
7260+
NT2 = NamedTuple('NT2', None)
7261+
7262+
NT3 = NamedTuple('NT2', [])
72147263

72157264
class CNT(NamedTuple):
72167265
pass # empty body
72177266

7218-
for struct in [NT, CNT]:
7267+
for struct in NT1, NT2, NT3, CNT:
72197268
with self.subTest(struct=struct):
72207269
self.assertEqual(struct._fields, ())
72217270
self.assertEqual(struct._field_defaults, {})
@@ -7225,13 +7274,29 @@ class CNT(NamedTuple):
72257274
def test_namedtuple_errors(self):
72267275
with self.assertRaises(TypeError):
72277276
NamedTuple.__new__()
7228-
with self.assertRaises(TypeError):
7277+
7278+
with self.assertRaisesRegex(
7279+
TypeError,
7280+
"missing 1 required positional argument"
7281+
):
72297282
NamedTuple()
7230-
with self.assertRaises(TypeError):
7283+
7284+
with self.assertRaisesRegex(
7285+
TypeError,
7286+
"takes from 1 to 2 positional arguments but 3 were given"
7287+
):
72317288
NamedTuple('Emp', [('name', str)], None)
7232-
with self.assertRaises(ValueError):
7289+
7290+
with self.assertRaisesRegex(
7291+
ValueError,
7292+
"Field names cannot start with an underscore"
7293+
):
72337294
NamedTuple('Emp', [('_name', str)])
7234-
with self.assertRaises(TypeError):
7295+
7296+
with self.assertRaisesRegex(
7297+
TypeError,
7298+
"missing 1 required positional argument: 'typename'"
7299+
):
72357300
NamedTuple(typename='Emp', name=str, id=int)
72367301

72377302
def test_copy_and_pickle(self):

Lib/typing.py

+45-3
Original file line numberDiff line numberDiff line change
@@ -2755,7 +2755,16 @@ def __new__(cls, typename, bases, ns):
27552755
return nm_tpl
27562756

27572757

2758-
def NamedTuple(typename, fields=None, /, **kwargs):
2758+
class _Sentinel:
2759+
__slots__ = ()
2760+
def __repr__(self):
2761+
return '<sentinel>'
2762+
2763+
2764+
_sentinel = _Sentinel()
2765+
2766+
2767+
def NamedTuple(typename, fields=_sentinel, /, **kwargs):
27592768
"""Typed version of namedtuple.
27602769
27612770
Usage::
@@ -2775,11 +2784,44 @@ class Employee(NamedTuple):
27752784
27762785
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
27772786
"""
2778-
if fields is None:
2779-
fields = kwargs.items()
2787+
if fields is _sentinel:
2788+
if kwargs:
2789+
deprecated_thing = "Creating NamedTuple classes using keyword arguments"
2790+
deprecation_msg = (
2791+
"{name} is deprecated and will be disallowed in Python {remove}. "
2792+
"Use the class-based or functional syntax instead."
2793+
)
2794+
else:
2795+
deprecated_thing = "Failing to pass a value for the 'fields' parameter"
2796+
example = f"`{typename} = NamedTuple({typename!r}, [])`"
2797+
deprecation_msg = (
2798+
"{name} is deprecated and will be disallowed in Python {remove}. "
2799+
"To create a NamedTuple class with 0 fields "
2800+
"using the functional syntax, "
2801+
"pass an empty list, e.g. "
2802+
) + example + "."
2803+
elif fields is None:
2804+
if kwargs:
2805+
raise TypeError(
2806+
"Cannot pass `None` as the 'fields' parameter "
2807+
"and also specify fields using keyword arguments"
2808+
)
2809+
else:
2810+
deprecated_thing = "Passing `None` as the 'fields' parameter"
2811+
example = f"`{typename} = NamedTuple({typename!r}, [])`"
2812+
deprecation_msg = (
2813+
"{name} is deprecated and will be disallowed in Python {remove}. "
2814+
"To create a NamedTuple class with 0 fields "
2815+
"using the functional syntax, "
2816+
"pass an empty list, e.g. "
2817+
) + example + "."
27802818
elif kwargs:
27812819
raise TypeError("Either list of fields or keywords"
27822820
" can be provided to NamedTuple, not both")
2821+
if fields is _sentinel or fields is None:
2822+
import warnings
2823+
warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15))
2824+
fields = kwargs.items()
27832825
nt = _make_nmtuple(typename, fields, module=_caller())
27842826
nt.__orig_bases__ = (NamedTuple,)
27852827
return nt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Deprecate creating a :class:`typing.NamedTuple` class using keyword
2+
arguments to denote the fields (``NT = NamedTuple("NT", x=int, y=str)``).
3+
This will be disallowed in Python 3.15.
4+
Use the class-based syntax or the functional syntax instead.
5+
6+
Two methods of creating ``NamedTuple`` classes with 0 fields using the
7+
functional syntax are also deprecated, and will be disallowed in Python 3.15:
8+
``NT = NamedTuple("NT")`` and ``NT = NamedTuple("NT", None)``. To create a
9+
``NamedTuple`` class with 0 fields, either use ``class NT(NamedTuple): pass`` or
10+
``NT = NamedTuple("NT", [])``.

0 commit comments

Comments
 (0)