I am trying to figure out the best way to implement type annotations for types that are selected dynamically at runtime.
I have a type called MPZ
that represents an implementation of integers and is defined at runtime by something like:
try:
from flint import fmpz
MPZ = fmpz
except ImportError:
MPZ = int
I then have various functions that either work with int
or with MPZ
e.g.:
def f(x: int) -> int:
x_mpz = MPZ(x)
result_mpz = g(x_mpz)
return int(result_mpz)
def g(x: MPZ) -> MPZ:
return x + MPZ(1)
It is possible that at runtime MPZ
and int
are the same type but it is also possible that they are not. The int
and fmpz
types are sufficiently compatible that they can do what I need them to do but it is also important to distinguish between int
and MPZ
in the codebase because accepting/leaking an MPZ
(possibly an fmpz
) at runtime can cause problems elsewhere.
I would like for a typechecker to distinguish int
and MPZ
as incompatible types e.g.:
def f() -> MPZ:
return 0 # error
def g() -> int:
return MPZ(0) # error
Currently the best I have come up with for this is something like:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
class MPZ:
def __new__(cls, x: int) -> MPZ: ...
def __int__(self) -> int: ...
def __add__(self, other: MPZ) -> MPZ: ...
else:
try:
from flint import fmpz
MPZ = fmpz
except ImportError:
MPZ = int
def f(x: int) -> int:
return int(g(MPZ(x)))
def g(x: MPZ) -> MPZ:
return x + MPZ(1)
print(f(1))
def h() -> int:
return MPZ(1) # error
def i() -> MPZ:
return 1 # error
This works as intended in all ways but one: it has a downside that much of the interesting checking is bypassed in the else
clause of the if TYPE_CHECKING
block. There are no stubs files for python-flint but for MPZ = int
a type checker could verify the assignment against MPZ
as e.g. a protocol but here all checking is disabled.
I have tried using protocols but then it means that I can’t use the MPZ
type for annotations because it is a runtime variable:
from __future__ import annotations
from typing import Protocol, Self
class MPZ_proto(Protocol):
def __new__(cls, x: int, /) -> Self: ...
def __int__(self) -> int: ...
def __add__(self, other: Self, /) -> Self: ...
MPZ: type[MPZ_proto]
try:
from flint import fmpz # type: ignore
MPZ = fmpz
except ImportError:
MPZ = int
# Variable not allowed in type expression
def g(x: MPZ) -> MPZ: # error
return x + MPZ(1)
Hopefully I am just missing something obvious about how to do this…