Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python typing: return type with generics like Clazz[T] as in Java Clazz<T>

So I am aware of Python's typing.Optional. But I wrote my own crude PyOptional (code here) and would like to combine Optional[T] with my PyOptional to PyOptional[T].

I am currently using Python 3.7 and tried extending typing.Optional.

Some of my PyOptional:

class PyOptional:
    T: TypeVar = TypeVar("T")

    def __init__(self, obj: T):
        self.value: Any = obj

    def get(self) -> Optional[T]:
        return self.value

    def or_else(self, default) -> T:
        return self.value or default

Pseudo-code of what I want:

def find_user_by_id(id: int) -> PyOptional[User]:
    return PyOptional(db.find_user_by_id(id))

The goal is for my IDE to be able to check what return type to expect and still be able to invoke my methods on the returned object. So it would have to be PEP-compliant.

like image 871
bobrossofcoding Avatar asked Oct 15 '25 14:10

bobrossofcoding


2 Answers

You should review the documentation on generics -- specifically, user-defined generics. The mypy docs also have a thorough overview of generics that can be useful to reference.

In this particular case, you want to make the entire class generic by adding in a Generic[T] as a class base. Just using T in the individual function signatures will make each individual function generic, but not the class as a whole:

from typing import TypeVar, Generic, Optional

T = TypeVar("T")

class PyOptional(Generic[T]):
    def __init__(self, obj: Optional[T]) -> None:
        self.value = obj

    def get(self) -> Optional[T]:
        return self.value

    def or_else(self, default: T) -> T:
        return self.value or default

Some additional notes:

  1. Don't add an annotation for any TypeVar variable. Here, T is a sort of meta-type construct that serves as "hole"/can represent any number of types. So, assigning it a fixed type doesn't really make sense, and will confuse type checkers.

  2. Never use a TypeVar only once in any given signature -- the whole point of using TypeVars is so that you can declare two or more types are always going to be the same.

    Note that the fixed PyOptional class above also obeys this rule. For example, take get. Now that we made the whole class generic, the type signature for this function is now basically something like def get(self: PyOptional[T]) -> Optional[T]. Before, it was more like def get(self: PyOptional) -> Optional[T].

  3. For your class to make sense, you probably want your constructor to accept an Optional[T] instead of just T.

  4. Making self.value Any is probably unnecessary/is unnecessarily too vague. We can leave off the type hint, and now it'll have an inferred type of Optional[T].

  5. If you want to more thoroughly check whether or not your class is PEP 484 compliant and will likely be understood by IDEs such as PyCharm, consider type-checking your class + some code using your class via mypy, a PEP 484 type checker.

    This won't guarantee that your IDE will fully understand your class (since it might not fully implement everything about PEP 484/you might run into a bug in either mypy or your IDE), but it should help you get pretty close.

like image 110
Michael0x2a Avatar answered Oct 18 '25 07:10

Michael0x2a


Since Python 3.12, type parameter lists offer simpler syntax over directly using TypeVar.

class PyOptional[T]:
    def __init__(self, obj: T | None):
        self.value = obj

    def get(self) -> T | None:
        return self.value

    def or_else(self, default: T) -> T:
        return default if self.value is None else self.value
like image 29
Unmitigated Avatar answered Oct 18 '25 06:10

Unmitigated



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!