Skip to content

Add NewType #226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions pep-0484.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
==========

Expand Down Expand Up @@ -1454,6 +1516,8 @@ Fundamental building blocks:

* Generic, used to create user-defined generic classes

* Type, used to annotate class objects
Copy link
Member

Choose a reason for hiding this comment

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

This and the next one I'll apply right now. They're unrelated to this issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, sure, I just posted a link to this PR to python-dev asking for comments.


Generic variants of builtin collections:

* Dict, used as ``Dict[key_type, value_type]``
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions python2/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
33 changes: 32 additions & 1 deletion python2/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
'Callable',
'Generic',
'Optional',
'Tuple',
'Type',
'TypeVar',
'Union',
'Tuple',

# ABCs (from collections.abc).
'AbstractSet', # collections.abc.Set.
Expand Down Expand Up @@ -60,6 +61,7 @@
'AnyStr',
'cast',
'get_type_hints',
'NewType',
'no_type_check',
'no_type_check_decorator',
'overload',
Expand Down Expand Up @@ -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

Expand Down
20 changes: 20 additions & 0 deletions src/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
28 changes: 28 additions & 0 deletions src/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
'AnyStr',
'cast',
'get_type_hints',
'NewType',
'no_type_check',
'no_type_check_decorator',
'overload',
Expand Down Expand Up @@ -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

Expand Down