I am working with SQLite where type hints are still far from perfect, and I ended up with a MCVE of what I think I want to do, and it may be related to statically-typing ABCs.
Example:
T = TypeVar('T')
class ColumnarValue(Generic[T]):
pass
def test1(guard: ColumnarValue[str]): ...
def test2(guard: ColumnarValue[int]): ...
# These should work
test1("Hello")
test2(42)
# These should be type errors
test1(42)
test2("Hello")
# Or, likewise:
test_assignment_1: ColumnarValue[str] = "this line should typecheck"
test_assignment_2: ColumnarValue[int] = "this line should NOT typecheck"
I tried to use ABCs, but since they seem to register using ABC.register(), the typechecker has no clue what I'm talking about.
I also tried generic protocols, but I'd have to engineer a protocol that guards for the functions expected exactly for every type T.
I'm testing this with Pyright/Pylance, but if this is part of the problem, an alternative would be considered an answer.
I also found https://github.com/antonagestam/phantom-types which, according to the documentation, "will not add any processing or memory overhead". This is not completely true since they go through the .parse and __instancecheck__
protocols on runtime (although they technically don't create any new objects, they do have some overhead in stack frames).
Alas, this is not a static checker, and while the solution there seems to be using assert isinstance("hello", MyStr)
to ensure that the typechecker can satisfy such guarantees after that line (this works in mypy and pylance at least), it would not trigger any typechecking-time error with assert isinstance("hello", MyInt)
. The code would fail the assertion at runtime.
Ultimately, I would want to be able to have a type T
that I can call as C[T]
so that when I declare a col_name = Column(String, ...)
in a Model, I can then col_name = "value"
or some variation of it that is statically checked.
Is this possible, and if it is, where is the magic?
If you are going to put these columns into a model class in the end, you could use descriptors (which is what e.g. the property
decorator uses under the hood, if you are familiar with that).
A rough sketch:
from typing import TypeVar, Generic, Any
T = TypeVar("T")
class ColumnarValue(Generic[T]):
def __get__(self, instance: object, owner: Any) -> T:
...
def __set__(self, instance: object, value: T) -> None:
...
class Model:
some_str_column: ColumnarValue[str]
some_int_column: ColumnarValue[int]
m = Model()
m.some_str_column = "this line typechecks"
m.some_int_column = "this line does NOT typecheck"
Also have a look at how sqlalchemy handles all this here.
In particular the Mapped
class, with its actual type definition here.