0

I would like to pass generic class to a function so I can return the same type as result. My use case is writing ORM and a read function, which will take a table ORM, and return the same type as result, something like this:

def read<T>(id, fields="*") -> T:

Currently without typing, my code look like this:

def read(table, id, fields="*"):

Is something like this possible?

18
  • It is actually possible, but it's mostly done for type hinting and requires a bit of work to make it really make it really work like generics in C# or Java. Commented Apr 21, 2022 at 19:44
  • 2
    This doesn't really make sense. You can type annotate generics, but that doesn't matter at runtime, that is strictly for static analysis. Python, a dynamically typed language, doesn't need generics, so it is really difficult to understand what you are asking for. Commented Apr 21, 2022 at 19:45
  • @juanpa.arrivillaga, there are cases for that (e.g. repository pattern with base repository) Commented Apr 21, 2022 at 19:47
  • 2
    @sudden_appearance a case for what? What exactly are you describing in Python that would necessitate "generics" (again, something that doesn't make sense unless you are talking about statically typed languages) Commented Apr 21, 2022 at 19:48
  • 1
    @ShadowRanger I suspect something like this is the correct answer, but as it stands, I don't think the exact use-case is clear from the way the question is written Commented Apr 21, 2022 at 19:49

2 Answers 2

2

TL;DR Python doesn't need generics because it is a dynamically typed langauge. This allows us to use so-called duck typing instead of explicit generics.

The first thing to note is that type annotations in python are only suggestions. They help static analysis tools such as code completion while you are writing code but are not enforced at run-time.

With that in mind, let's look at a non-annotated version of your function:

def read(table, fields="*"):
    pass

Here, python doesn't care what you pass for table as long as it supports the operations that are used inside of read(). This is what we call duck-typing: if it quacks like a duck, it's a duck. You see this in functions that act on sequeneces. For example:

def print_list(my_list):
    for el in my_list:
       print(el)

We can call print_list() with a list or a tuple:

print_list([1, 2, 3, 4])
print_list((1, 2, 3, 4))

In fact, anything that implements __iter__() and __next__() will work. This is the contract of the type and doesn't need to be specified with an interface, superclass, or generic type.

Now back to type hinting: there is a way to specify the "duck type contract" with type hinting, but I don't know the details off the top of my head. You could do some research to find out. As @juanpa.arrivillaga posted in a comment, you can look at typing.Protocol if you are interested in learning how to do the type hinting for cases like this.

Sign up to request clarification or add additional context in comments.

3 Comments

I think you are referring to typing.Protocol for structural subtyping, which is sort of the static typing equivalent of duck typing.
@juanpa.arrivillaga Thanks for that detail...in fact, "protocol" is probably a more accurate term than "contract" in my wording.
Simplest duck type contracts are made from typing.Protocols; if the child class of the Protocol is decorated with @typing.runtime_checkable, you can use it for isinstance/issubclass checks at runtime of arbitrary instances/classes to see if they have the methods the Protocol expects, in which case they count as virtual subclasses of the Protocol class in question; if undecorated, they're purely for static type checkers.
0

Ok, I recently was digging in Python type hinting abilities and came up with something like that

DBModel = TypeVar('DBModel')

class BaseRepository(Generic[DBModel]):

    def __init__(self):
        self.model: Type[DBModel] = get_args(self.__orig_bases__[0])[0]

    def get_where(self, *args) -> DBModel
        ...  # any logic with self.model

as a base class and

class UsersRepository(BaseRepository[User]):
    pass

UsersRepository will now have already implemented get_where() with User model and will also have reference for User inside self.model

I guess that's what you want. Github with source code (small project actually.)

8 Comments

So you are introspecting type hints.
@juanpa.arrivillaga, it's not actually type hints but typed generics, but yes. Pydantic uses <class_name>.__annotations__ dict for introspecting typehints and get_args in some cases
I don't understand the distinction you are making really. typing.Generic exists for the purpose of creating type hints. But I understand what you were getting at now
@juanpa.arrivillaga, yea, that code looks 0 pythonic way, I guess)
I mean, using type annotations this way is new enough that whether or not it is "pythonic" (i.e. idiomatic in Python) remains to be established. In any case, what you are doing here could have been done with a metaclass, so your API would be something like class UserRepository(BaseReporsitory, model=User) or something like that (note, prior to recent versions of Python, typing.Generic used a metaclass to do a lot of it's magic, but recent versions have allowed you to do it with special hooks, i.e. special "dunder methods" the runtime now recognizes, from the class itself)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.