Search code examples
pythongenericspython-typing

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.


Solution

  • 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.