TL;DR
Change typing.NewType
and typing.TypeAliasType
to be metaclasses or class constructors to allow pattern matching and isinstance checks.
The Problem
typing.NewType
is here to remove type confusion between string types, string types, string types, and string types [1], and only a few of them have strictly-typed counterparts (like pathlib.Path
), as well as other similar cases.
typing.TypeAliasType
is a type of object created by a new type X = ...
statement, which is extremely useful for declaring named unions [2] or generic shortcuts [3].
You try combining them because it’s looks nice and declarative, these NewType
s start looking like real classes: type UserID = NewType('UserID', str)
… until you realise that you can no longer instantiate them.
And the obvious next step when using Python 3.12 is to combine new typing syntaxes with the structural pattern matching.
HTMLTag = NewType('HTMLTag', str)
def parse_tag(el: Element):
match el.tag:
case HTMLTag('div'):
print("processing div...")
case HTMLTag('span'):
print("processing span...")
case HTMLTag(tag):
print(f"Skipping unsupported tag: {tag}")
But this doesn’t work…
TypeError: called match pattern must be a class
And I have a proposal suggestion to fix both problems.
The Proposal
Change NewType()
and type ...
to return a fake type instead of a typing
proxy object.
Long code example here
new_typing.py
import typing
from abc import ABCMeta
from typing import overload
class NewType[T](ABCMeta):
__supertype__: typing.Type[T]
def __new__(cls, name: str, tp: typing.Type[T]):
result = super().__new__(cls, name, (tp, ), { })
result.__module__ = typing._caller()
result.__supertype__ = tp
result.__new__ = lambda _cls, x: x
if (hasattr(tp, '__match_args__')):
result.__match_args__ = tp.__match_args__
result = typing.final(result)
return result
def __init__(cls, name: str, tp: typing.Type[T]):
super().__init__(name, (tp, ), { })
def __instancecheck__(cls, other):
return isinstance(other, cls.__supertype__)
def __subclasscheck__(cls, other):
return issubclass(other, cls.__supertype__)
type _AnyType = typing.Type | typing.Union | typing._GenericAlias
@overload
def _extract_arg(name: str, value: _AnyType) -> tuple[str, _AnyType]:
...
@overload
def _extract_arg(alias: typing.TypeAliasType) -> tuple[str, _AnyType]:
...
def _extract_arg(*args):
match (args):
case (typing.TypeAliasType(__name__=name, __value__=value), ):
return name, value
case (name, value):
return name, value
case _:
raise TypeError(f"Invalid arguments: {args}")
class TypeAliasType(ABCMeta):
__value__: _AnyType
@typing.overload
def __new__(cls, name: str, value: _AnyType):
...
@typing.overload
def __new__(cls, alias: typing.TypeAliasType):
...
def __new__(cls, *args):
name, value = _extract_arg(*args)
if (isinstance(value, NewType)):
return value
result = super().__new__(cls, name, (), { })
result.__module__ = typing._caller()
result.__value__ = value
result.__new__ = lambda _cls, x: x
if (hasattr(value, '__match_args__')):
result.__match_args__ = value.__match_args__
result = typing.final(result)
return result
@typing.overload
def __init__(cls, _AnyType):
...
@typing.overload
def __init__(cls, alias: typing.TypeAliasType):
...
def __init__(cls, *args):
name, _ = _extract_arg(*args)
super().__init__(name, (), {})
def __instancecheck__(cls, other):
return isinstance(other, cls.__value__)
def __subclasscheck__(cls, other):
return issubclass(other, cls.__value__)
__all__ = \
[
'TypeAliasType',
'NewType',
]
new_typing.pyi
from typing import TypeAliasType, NewType
__all__ = \
[
'TypeAliasType',
'NewType',
]
new_types.py
import typing
from dataclasses import dataclass
from new_type_patterns.new_typing import NewType, TypeAliasType
type HTMLTag = NewType('HTMLTag', str)
type SupportedArg = str | int | float | bool
type StrMapping[T] = typing.MutableMapping[str, T]
HTMLTag = TypeAliasType(HTMLTag)
SupportedArg = TypeAliasType(SupportedArg)
StrMapping = TypeAliasType(StrMapping)
@dataclass
class Element:
tag: HTMLTag
text: str
__all__ = \
[
'HTMLTag',
'StrMapping',
'SupportedArg',
'Element',
]
new_types.pyi
import typing
from dataclasses import dataclass
from typing import NewType
HTMLTag = NewType('HTMLTag', str)
type SupportedArg = str | int | float | bool
type StrMapping[T] = typing.MutableMapping[str, T]
@dataclass
class Element:
tag: HTMLTag
text: str
__all__ = \
[
'HTMLTag',
'StrMapping',
'SupportedArg',
'Element',
]
main.py
import sys
from new_type_patterns.new_types import Element, HTMLTag, SupportedArg
from new_type_patterns.new_typing import NewType
def parse_tag(el: Element):
match (el.tag):
case HTMLTag('div'):
print("processing div...")
case HTMLTag('span'):
print("processing span...")
case HTMLTag(tag):
print(f"Skipping unsupported tag: {tag}")
case _:
raise TypeError(f"Unknown tag type: type={type(el.tag)}, value={el.tag}")
def parse_arg(arg: SupportedArg):
match (arg):
case str(s):
print(f"Processing string: {s}")
case SupportedArg():
print(f"Processing supported value: {arg}")
case _:
raise TypeError(f"Unknown arg type: type={type(arg)}, value={arg}")
def check_newtype_cls():
print(HTMLTag, repr(HTMLTag), str(HTMLTag), type(HTMLTag))
print(isinstance(HTMLTag, NewType), issubclass(HTMLTag, NewType), issubclass(HTMLTag, str))
print()
def check_newtype_pattern_matching():
parse_tag(Element('div', "content"))
parse_tag(Element('span', "span content"))
parse_tag(Element('a', 'https://example.com'))
try:
parse_tag(Element(42, "answer to everything"))
except TypeError as e:
print("Error:", type(e), e)
print()
def check_typealias_pattern_matching():
parse_arg('some string')
parse_arg(37)
parse_arg(False)
try:
parse_arg((1, 2, 3))
except TypeError as e:
print("Error:", type(e), e)
print()
def main():
check_newtype_cls()
check_newtype_pattern_matching()
check_typealias_pattern_matching()
return 0
if (__name__ == '__main__'):
exit_code = main()
exit(exit_code)
So, what have I done?
I just created new implementations for the NewType
and TypeAliasType
which are metaclasses supporting instance and subclass checks.
And it works:
<class 'new_type_patterns.new_types.HTMLTag'> <class 'new_type_patterns.new_types.HTMLTag'> <class 'new_type_patterns.new_types.HTMLTag'> <class 'new_type_patterns.new_typing.NewType'>
True False True
processing div...
processing span...
Skipping unsupported tag: a
Error: <class 'TypeError'> Unknown tag type: type=<class 'int'>, value=42
Processing string: some string
Processing supported value: 37
Processing supported value: False
Error: <class 'TypeError'> Unknown arg type: type=<class 'tuple'>, value=(1, 2, 3)
I would be glad to hear any feedback, positive or not. Preferably constructive.