You have to create an int subclass "for real" (pun not intended, but it is bad enough to stay there).
There are two problems there:
- typing.NewType does not create a new class. It just separate a "lineage" of objects so that they will "look like" a new class to static-type checking tools - but objects created with such a class are still of the indicated class in runtime.
See "typing.Newtype" at work in the interactive prompt, rather than relying on the static-check report:
In [31]: import typing
In [32]: Index = typing.NewType("Index", int)
In [33]: a = Index(5)
In [34]: type(a)
Out[34]: int
- The second problem, is that even if you subclass
int the proper way, the operations that result from applying any operator will still cast the result type back to int, and won't be of the created subclass:
In [35]: class Index(int): pass
In [36]: a = Index(5)
In [37]: type(a)
Out[37]: __main__.Index
In [38]: type(a + 1)
Out[38]: int
In [39]: type(a + a)
Out[39]: int
In [40]: a += 1
In [41]: type(a)
Out[41]: int
So, the only way out is to actually wrap all the magic methods which perform numeric operations in functions that "cast" the result back to the subclass. One can avoid repeating the same pattern several times in the class body by creating a decorator to perform this casting, and appling it to all numeric methods in a for loop in the class body itself.
In [68]: num_meths = ['__abs__', '__add__', '__and__', '__ceil__',
'__divmod__', '__floor__', '__floordiv__', '__invert__', '__lshift__',
'__mod__', '__mul__', '__neg__', '__pos__', '__pow__', '__radd__',
'__rand__', '__rdivmod__', '__rfloordiv__', '__rlshift__', '__rmod__',
'__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__',
'__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__sub__',
'__truediv__', '__trunc__', '__xor__', 'from_bytes']
# to get these, I did `print(dir(int))` copy-pasted the result, and
# deleted all non-relevant methods to this case, leaving only the ones
# that perform operations which should preserve the subclass
In [70]: class Index(int):
for meth in num_meths:
locals()[meth] = (lambda meth:
lambda self, *args:
__class__(getattr(int, meth)(self, *args))
)(meth)
# creating another "lambda" layer to pass the value of meth in _each_
# iteration of the for loop is needed so that these values are "frozen"
# for each created method
In [71]: a = Index(5)
In [72]: type(a)
Out[72]: __main__.Index
In [73]: type(a + 1)
Out[73]: __main__.Index
In [74]: a += 1
In [75]: type(a)
Out[75]: __main__.Index
This will actually work.
However, if the intent is that static type checking "sees" this wrapping taking place, you are off-the trail once more. Static type checkers won't understand method creation by applying a decorator in a loop inside a class body.
In other words, I see no way out of this but by copy-and-pasting the casting applyed automatically in the example above in all relevant numeric methods, and then, create annotations while at it:
from __future__ import annotations
from typing import Union
class Index(int):
def __add__(self: Index, other: Union[Index, int]) -> Index:
return __class__(super().__add__(other))
def __radd__(self: Index, other: Union[Index, int]) -> Index:
return __class__(super().__radd__(other))
# Rinse and repeat for all numeric methods you intend to use