Builder pattern with generic and typehinting

Posted by gidorah5@reddit | Python | View on Reddit | 3 comments

Hello redditors,

I've been playing around with a builder pattern in Python, and I was trying to achieve a builder pattern with correct typehinting, However, I can't seem to get some of my types working.
The goal is to create a pipeline, that can have a variable amout of steps. The pipeline have a TIn and TOut type that should be infered from the inner steps (TIn being the input of the first step, and TOut the output of the last step)

Here is my current implementation:

TIn = TypeVar("TIn")
TOut = TypeVar("TOut")
TNext = TypeVar("TNext")

class Step(ABC, Generic[TIn, TOut]):
    def execute(self, data: TIn) -> TOut:
        ...

def create_process(step: Step[TIn, TOut]) -> "Process[TIn, TOut]":
    return Process.start_static(step)

class Process(Generic[TIn, TOut]):
    def __init__(self, steps: list[Step] | None = None):
        self.steps: list[Step] = steps or []

    @classmethod
    def start_class(cls, step: Step[TIn, TOut]) -> "Process[TIn, TOut]":
        return cls([step])

    @staticmethod
    def start_static(step: Step[TIn, TOut]) -> "Process[TIn, TOut]":
        return Process([step])

    def add_step(self, step: Step[TOut, TNext]) -> "Process[TIn, TNext]":
        return Process(self.steps + [step])

    def execute(self, data: TIn) -> TOut:
        current = data
        for step in self.steps:
            print(type(step))
            current = step.execute(current)
        return cast(TOut, current)


class IntToStr(Step[int, str]):
    def execute(self, data: int) -> str:
        return str(data)


class StrToBool(Step[str, bool]):
    def execute(self, data: str) -> bool:
        return data != ""


process = create_process(IntToStr()).add_step(StrToBool())
# ^^ type Process[int, bool]
process = Process().add_step(IntToStr()).add_step(StrToBool())
# ^^ type Process[Unknown, bool]
process = Process.start_static(IntToStr()).add_step(StrToBool())
# ^^ type Process[Unknown, bool]
process = Process.start_class(IntToStr()).add_step(StrToBool())
# ^^ type Process[Unknown, bool]
process.execute(1)

As you can see, the only way I've been able to correctly infer the input type is by using a method outside of my class.
I'm not sure what is causing this, and I was wondering if anyone knew a workaround this issue, or am I doomed to use a Factory method.
I would believe that the issue is caused because TIn is not defined for the first step, thus issuing an Unknown.

Have a great day y'all !