diff --git a/pep-0484.txt b/pep-0484.txt index 781e23ef7..97920f3d1 100644 --- a/pep-0484.txt +++ b/pep-0484.txt @@ -1173,6 +1173,68 @@ checker should blindly believe the programmer. Also, casts can be used in expressions, while type comments only apply to assignments. +NewType helper function +----------------------- + +There are also situations where a programmer might want to avoid logical +errors by creating simple classes. For example:: + + class UserId(int): + pass + + get_by_user_id(user_id: UserId): + ... + +However, this approach introduces a runtime overhead. To avoid this, +``typing.py`` provides a helper function ``NewType`` that creates +simple unique types with almost zero runtime overhead. For a static type +checker ``Derived = NewType('Derived', Base)`` is roughly equivalent +to a definition:: + +class Derived(Base): + def __init__(self, _x: Base) -> None: + ... + +While at runtime, ``NewType('Derived', Base)`` returns a dummy function +that simply returns its argument. Type checkers require explicit casts +from ``int`` where ``UserId`` is expected, while implicitly casting +from ``UserId`` where ``int`` is expected. Examples:: + + UserId = NewType('UserId', int) + + def name_by_id(user_id: UserId) -> str: + ... + + UserId('user') # Fails type check + + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + + num = UserId(5) + 1 # type: int + +``NewType`` accepts only one argument that shoud be a proper class, +i.e., not a type construct like ``Union``, etc. The function returned +by ``NewType`` accepts only one argument; this is equivalent to supporting +only one constructor accepting an instance of the base class (see above). +Example:: + + class PacketId: + def __init__(self, major: int, minor: int) -> None: + self._major = major + self._minor = minor + + TcpPacketId = NewType('TcpPacketId', PacketId) + + packet = PacketId(100, 100) + tcp_packet = TcpPacketId(packet) # OK + + tcp_packet = TcpPacketId(127, 0) # Fails in type checker and at runtime + +Both ``isinstance`` and ``issubclass``, as well as subclassing will fail +for ``NewType('Derived', Base)`` since function objects don't support +these operations. + + Stub Files ========== @@ -1454,6 +1516,8 @@ Fundamental building blocks: * Generic, used to create user-defined generic classes +* Type, used to annotate class objects + Generic variants of builtin collections: * Dict, used as ``Dict[key_type, value_type]`` @@ -1487,6 +1551,8 @@ Generic variants of container ABCs (and a few non-containers): * Container +* ContextManager + * Generator, used as ``Generator[yield_type, send_type, return_type]``. This represents the return value of generator functions. It is a subtype of ``Iterable`` and it has additional @@ -1558,6 +1624,9 @@ Convenience definitions: This is useful to declare the types of the fields of a named tuple type. +* NewType, used to create unique types with little runtime overhead + ``UserId = NewType('UserId', int)`` + * cast(), described earlier * @no_type_check, a decorator to disable type checking per class or diff --git a/python2/test_typing.py b/python2/test_typing.py index b801383cd..7cce3a543 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -15,6 +15,7 @@ from typing import Generic from typing import cast from typing import Type +from typing import NewType from typing import NamedTuple from typing import IO, TextIO, BinaryIO from typing import Pattern, Match @@ -1141,6 +1142,25 @@ def new_user(user_class): joe = new_user(BasicUser) +class NewTypeTests(BaseTestCase): + + def test_basic(self): + UserId = NewType('UserId', int) + UserName = NewType('UserName', str) + self.assertIsInstance(UserId(5), int) + self.assertIsInstance(UserName('Joe'), type('Joe')) + self.assertEqual(UserId(5) + 1, 6) + + def test_errors(self): + UserId = NewType('UserId', int) + UserName = NewType('UserName', str) + with self.assertRaises(TypeError): + issubclass(UserId, int) + with self.assertRaises(TypeError): + class D(UserName): + pass + + class NamedTupleTests(BaseTestCase): def test_basics(self): diff --git a/python2/typing.py b/python2/typing.py index ff6ba9111..35edb629f 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -20,9 +20,10 @@ 'Callable', 'Generic', 'Optional', + 'Tuple', + 'Type', 'TypeVar', 'Union', - 'Tuple', # ABCs (from collections.abc). 'AbstractSet', # collections.abc.Set. @@ -60,6 +61,7 @@ 'AnyStr', 'cast', 'get_type_hints', + 'NewType', 'no_type_check', 'no_type_check_decorator', 'overload', @@ -1577,6 +1579,35 @@ def NamedTuple(typename, fields): return cls +def NewType(name, tp): + """NewType creates simple unique types with almost zero + runtime overhead. NewType(name, tp) is considered a subtype of tp + by static type checkers. At runtime, NewType(name, tp) returns + a dummy function that simply returns its argument. Usage:: + + UserId = NewType('UserId', int) + + def name_by_id(user_id): + # type: (UserId) -> str + ... + + UserId('user') # Fails type check + + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + + num = UserId(5) + 1 # type: int + """ + + def new_type(x): + return x + + # Some versions of Python 2 complain because of making all strings unicode + new_type.__name__ = str(name) + new_type.__supertype__ = tp + return new_type + + # Python-version-specific alias (Python 2: unicode; Python 3: str) Text = unicode diff --git a/src/test_typing.py b/src/test_typing.py index ade8a3587..432bb1589 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -16,6 +16,7 @@ from typing import get_type_hints from typing import no_type_check, no_type_check_decorator from typing import Type +from typing import NewType from typing import NamedTuple from typing import IO, TextIO, BinaryIO from typing import Pattern, Match @@ -1401,6 +1402,25 @@ def new_user(user_class: Type[U]) -> U: joe = new_user(BasicUser) +class NewTypeTests(BaseTestCase): + + def test_basic(self): + UserId = NewType('UserId', int) + UserName = NewType('UserName', str) + self.assertIsInstance(UserId(5), int) + self.assertIsInstance(UserName('Joe'), str) + self.assertEqual(UserId(5) + 1, 6) + + def test_errors(self): + UserId = NewType('UserId', int) + UserName = NewType('UserName', str) + with self.assertRaises(TypeError): + issubclass(UserId, int) + with self.assertRaises(TypeError): + class D(UserName): + pass + + class NamedTupleTests(BaseTestCase): def test_basics(self): diff --git a/src/typing.py b/src/typing.py index 4bd213543..17b28c77d 100644 --- a/src/typing.py +++ b/src/typing.py @@ -64,6 +64,7 @@ 'AnyStr', 'cast', 'get_type_hints', + 'NewType', 'no_type_check', 'no_type_check_decorator', 'overload', @@ -1631,6 +1632,33 @@ def NamedTuple(typename, fields): return cls +def NewType(name, tp): + """NewType creates simple unique types with almost zero + runtime overhead. NewType(name, tp) is considered a subtype of tp + by static type checkers. At runtime, NewType(name, tp) returns + a dummy function that simply returns its argument. Usage:: + + UserId = NewType('UserId', int) + + def name_by_id(user_id: UserId) -> str: + ... + + UserId('user') # Fails type check + + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + + num = UserId(5) + 1 # type: int + """ + + def new_type(x): + return x + + new_type.__name__ = name + new_type.__supertype__ = tp + return new_type + + # Python-version-specific alias (Python 2: unicode; Python 3: str) Text = str