Skip to content

Commit

Permalink
pythongh-105566: Deprecate unusual ways of constructing `typing.Named…
Browse files Browse the repository at this point in the history
…Tuple`s
  • Loading branch information
AlexWaygood committed Jun 9, 2023
1 parent 8e75592 commit be88d69
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 12 deletions.
13 changes: 13 additions & 0 deletions Doc/library/typing.rst
Expand Up @@ -2038,6 +2038,19 @@ These are not used in annotations. They are building blocks for declaring types.
.. versionchanged:: 3.11
Added support for generic namedtuples.

.. deprecated-removed:: 3.13 3.15
The undocumented keyword argument syntax for creating NamedTuple classes
(``NT = NamedTuple("NT", x=int)``) is deprecated, and will be removed in
3.15.

.. deprecated-removed:: 3.13 3.15
When using the functional syntax to create a NamedTuple class, failing to
pass a value to the 'fields' parameter (``NT = NamedTuple("NT")``) is
deprecated. Passing ``None`` to the 'fields' parameter
(``NT = NamedTuple("NT", None)``) is also deprecated. Both will be
removed in Python 3.15. To create a NamedTuple class with 0 fields, use
``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``.

.. class:: NewType(name, tp)

Helper class to create low-overhead :ref:`distinct types <distinct>`.
Expand Down
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.13.rst
Expand Up @@ -133,6 +133,17 @@ Deprecated
methods of the :class:`wave.Wave_read` and :class:`wave.Wave_write` classes.
They will be removed in Python 3.15.
(Contributed by Victor Stinner in :gh:`105096`.)
* Creating a :class:`typing.NamedTuple` class using keyword arguments to denote
the fields (``NT = NamedTuple("NT", x=int, y=int)``) is deprecated, and will
be disallowed in Python 3.15. Use the class-based syntax or the functional
syntax instead. (Contributed by Alex Waygood in :gh:`105566`.)
* When using the functional syntax to create a :class:`typing.NamedTuple`
class, failing to pass a value to the 'fields' parameter
(``NT = NamedTuple("NT")``) is deprecated. Passing ``None`` to the 'fields'
parameter (``NT = NamedTuple("NT", None)``) is also deprecated. Both will be
removed in Python 3.15. To create a NamedTuple class with 0 fields, use
``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``.
(Contributed by Alex Waygood in :gh:`105566`.)


Removed
Expand Down
75 changes: 66 additions & 9 deletions Lib/test/test_typing.py
Expand Up @@ -7124,18 +7124,47 @@ class Group(NamedTuple):
self.assertEqual(a, (1, [2]))

def test_namedtuple_keyword_usage(self):
LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)
with self.assertWarnsRegex(
DeprecationWarning,
"Creating NamedTuple classes using keyword arguments is deprecated"
):
LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)

nick = LocalEmployee('Nick', 25)
self.assertIsInstance(nick, tuple)
self.assertEqual(nick.name, 'Nick')
self.assertEqual(LocalEmployee.__name__, 'LocalEmployee')
self.assertEqual(LocalEmployee._fields, ('name', 'age'))
self.assertEqual(LocalEmployee.__annotations__, dict(name=str, age=int))
with self.assertRaises(TypeError):

with self.assertRaisesRegex(
TypeError,
"Either list of fields or keywords can be provided to NamedTuple, not both"
):
NamedTuple('Name', [('x', int)], y=str)

with self.assertRaisesRegex(
TypeError,
"Either list of fields or keywords can be provided to NamedTuple, not both"
):
NamedTuple('Name', [], y=str)

with self.assertRaisesRegex(
TypeError,
(
r"Cannot pass `None` as the 'fields' parameter "
r"and also specify fields using keyword arguments"
)
):
NamedTuple('Name', None, x=int)

def test_namedtuple_special_keyword_names(self):
NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)
with self.assertWarnsRegex(
DeprecationWarning,
"Creating NamedTuple classes using keyword arguments is deprecated"
):
NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list)

self.assertEqual(NT.__name__, 'NT')
self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields'))
a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)])
Expand All @@ -7145,12 +7174,24 @@ def test_namedtuple_special_keyword_names(self):
self.assertEqual(a.fields, [('bar', tuple)])

def test_empty_namedtuple(self):
NT = NamedTuple('NT')
with self.assertWarnsRegex(
DeprecationWarning,
"Failing to pass a value for the 'fields' parameter is deprecated"
):
NT1 = NamedTuple('NT1')

with self.assertWarnsRegex(
DeprecationWarning,
"Passing `None` as the 'fields' parameter is deprecated"
):
NT2 = NamedTuple('NT2', None)

NT3 = NamedTuple('NT2', [])

class CNT(NamedTuple):
pass # empty body

for struct in [NT, CNT]:
for struct in NT1, NT2, NT3, CNT:
with self.subTest(struct=struct):
self.assertEqual(struct._fields, ())
self.assertEqual(struct._field_defaults, {})
Expand All @@ -7160,13 +7201,29 @@ class CNT(NamedTuple):
def test_namedtuple_errors(self):
with self.assertRaises(TypeError):
NamedTuple.__new__()
with self.assertRaises(TypeError):

with self.assertRaisesRegex(
TypeError,
"missing 1 required positional argument"
):
NamedTuple()
with self.assertRaises(TypeError):

with self.assertRaisesRegex(
TypeError,
"takes from 1 to 2 positional arguments but 3 were given"
):
NamedTuple('Emp', [('name', str)], None)
with self.assertRaises(ValueError):

with self.assertRaisesRegex(
ValueError,
"Field names cannot start with an underscore"
):
NamedTuple('Emp', [('_name', str)])
with self.assertRaises(TypeError):

with self.assertRaisesRegex(
TypeError,
"missing 1 required positional argument: 'typename'"
):
NamedTuple(typename='Emp', name=str, id=int)

def test_copy_and_pickle(self):
Expand Down
44 changes: 41 additions & 3 deletions Lib/typing.py
Expand Up @@ -2753,7 +2753,16 @@ def __new__(cls, typename, bases, ns):
return nm_tpl


def NamedTuple(typename, fields=None, /, **kwargs):
class _Sentinel:
__slots__ = ()
def __repr__(self):
return '<sentinel>'


_sentinel = _Sentinel()


def NamedTuple(typename, fields=_sentinel, /, **kwargs):
"""Typed version of namedtuple.
Usage::
Expand All @@ -2773,11 +2782,40 @@ class Employee(NamedTuple):
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
"""
if fields is None:
fields = kwargs.items()
if fields is _sentinel:
if kwargs:
deprecated_thing = "Creating NamedTuple classes using keyword arguments"
deprecation_msg = (
"{name} is deprecated and will be disallowed in Python {remove}. "
"Use the class-based or functional syntax instead."
)
else:
deprecated_thing = "Failing to pass a value for the 'fields' parameter"
deprecation_msg = (
"{name} is deprecated and will be disallowed in Python {remove}. "
"To create an empty NamedTuple using functional syntax, "
"pass an empty list, e.g. `NT = NamedTuple('NT', [])`."
)
elif fields is None:
if kwargs:
raise TypeError(
"Cannot pass `None` as the 'fields' parameter "
"and also specify fields using keyword arguments"
)
else:
deprecated_thing = "Passing `None` as the 'fields' parameter"
deprecation_msg = (
"{name} is deprecated and will be disallowed in Python {remove}. "
"To create an empty NamedTuple using functional syntax, "
"pass an empty list, e.g. `NT = NamedTuple('NT', [])`."
)
elif kwargs:
raise TypeError("Either list of fields or keywords"
" can be provided to NamedTuple, not both")
if fields is _sentinel or fields is None:
import warnings
warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15))
fields = kwargs.items()
nt = _make_nmtuple(typename, fields, module=_caller())
nt.__orig_bases__ = (NamedTuple,)
return nt
Expand Down
@@ -0,0 +1,8 @@
Deprecate creating a :class:`typing.NamedTuple` class using keyword
arguments to denote the fields (``NT = NamedTuple("NT", x=int, y=str)``).
Use the class-based syntax or the functional syntax instead.

Two methods of creating ``NamedTuple``\s with 0 fields using the functional
syntax are also deprecated: ``NT = NamedTuple("NT")`` and ``NT =
NamedTuple("NT", None)``. To create a ``NamedTuple`` class with 0 fields,
either use ``class NT(NamedTuple): ...`` or ``NT = NamedTuple("NT", [])``.

0 comments on commit be88d69

Please sign in to comment.