In Rust there is this misterious type called std::marker::PhantomData<T>.
The documentation shows some nice examples especially regarding lifetimes.
I wanted something similar in python because I ran into a little problem and I wanted mypy or any other static analysis tool to help me so I dont dynamically shoot myself in the foot.
The problem goes like this.
Say we have some classes which implement a Protocol P. The Protocol defines a run method which returns some dataclass Result:
# Setup
from typing import Protocol
from dataclasses import dataclass
@dataclass
class Result():
some_int: int
some_float: float
some_string: str
class P(Protocol):
def run(self) -> Result: ...
class A(P):
def run(self) -> Result:
return Result(some_int=1, some_float=1.0, some_string="Hello")
class B(P):
def run(self) -> Result:
return Result(some_int=2, some_float=2.0, some_string="World!")
# Some more implementations...
Now, I defined the Protocol because I needed to have a collection of B, run each and then further process the Results.
And here is the issue: I need to know where the result came from
One solution would be to add another field to Result pointing to the origin. However, serialization becomes annoying and the whole thing feels weird.
But Phantom to the rescue!
We can just do it like this:
# this is python 3.11, in 3.12 you do not need the `Generic`
from __future__ import annotations
from typing import Protocol, Generic, TypeVar, Any
from dataclasses import dataclass
Phantom = TypeVar("Phantom")
@dataclass
class Result(Generic[Phantom]):
some_int: int
some_float: float
some_string: str
class P(Protocol):
def run(self) -> Result[Any]: ...
class A(P):
def run(self) -> Result[A]:
return Result(some_int=1, some_float=1.0, some_string="Hello")
class B(P):
def run(self) -> Result[B]:
return Result(some_int=2, some_float=2.0, some_string="World!")
and viola, we are done!
We can now safely define function which take Result[SomeType] as argument and be totally save (at least as save as it gets with python):
def process_a(result: Result[A]):
print("A", result)
result = Result[B](some_int=2, some_float=2.0, some_string="World!")
process_a(result) # <- type Error here