1

Okay, I have a set of dictionaries that contain a key/ID and a function. Said dictionaries are used as identifiers that point to a specific cache key and function to update said cache key's associated data.

from typing import TypedDict, List

class CacheIdentifier(TypedDict):
   key: str
   func: Callable

def function_to_fetch_cache_data() -> List[str]:
   return ["a", "b", "c"]

IDENTIFIER: CacheIdentifier = {
   "key": "my_cache_key",
   "func": function_to_fetch_cache_data
}

Now, the thing is, I have a function called load_or_set_cache which takes an identifier object and, like the name says, checks if there's data associated with a cache key and fetches the data associated with the cache key if it exists, otherwise it uses the func argument of the CacheIdentifier object provided to fetch new data.

def load_or_set_cache(identifier: CacheIdentifier) -> Any:
   # Logic to check if the data exists in the cache or not
   
   if not cached:
      cache_data = identifier["func"]()
      cache_key = identifier["key"]

      cache.set(cache_key, cache_data, TTL = 3600)

   else:
      cache_key = identifier["key"]
      cache_data = cache.get(cache_key)     

   return cache_data

cache_data = load_or_set_cache(IDENTIFIER)

The thing is, the load_or_set_function function returns the data that was fetched and stored in the cache, but as you can expect, the type of said data varies depending on the return type of the function of each identifier. In my example above, if the function_to_fetch_cache_data has a return type of List[str] then the load_or_set_cache function will have the same return type, causing cache_data to have a List[str] type.

Currently the output type of the load_or_set_cache function is just set to Any, is there any way I could dynamically change the output type of the function depending on the output type of the associated func argument found in each cache identifier?

I've tried playing with TypeVars but dont feel like they really suit what I want to do

Eddysanoli
  • 481
  • 2
  • 8
  • 17
  • What's the point of doing this? Static type checking won't know the dynamic value of the function, so it can't perform any type checking. – Barmar Jan 05 '23 at 18:32
  • What's this "load/set anything cache" for? I feel like there's probably some other, better way to do this. If it really can store _anything_, you might as well just store blobs of data like strings. – Kache Jan 05 '23 at 18:37
  • I am a little confused, because you refer to two additional functions `load_or_set_function` and `function_to_fetch_cache_data`, but you have not shown how they are defined or what they are supposed to return. I don't think that you can or should dynamically change type hints, because they are just documentation for humans to better understand the code and are not enforced by Python. If a function returns multiple types of data, Any is ok or if you know that it will return only certain types of data, Union is better. – Eduardo Matsuoka Jan 05 '23 at 18:47
  • You mentioned a whole lot of functions, but none of them are present in the code you presented. Please provide a [minimal reproducible example](https://stackoverflow.com/help/mcve) instead. That means, reduce your actual use case to those components that are absolutely essential to reproduce your problem and allow people to copy, paste and play around with that code. – Daniil Fajnberg Jan 05 '23 at 18:47
  • @Kache: My idea was to centralize the cache keys used across my apps. Cause sometimes people were using for example the "names" cache key, but others were using "usernames" as the cache key for the same data. I wanted to centralize said keys as "identifier" variables that the user can just look up in a file – Eddysanoli Jan 05 '23 at 19:16
  • @Barmar I just want to be sure if its possible for me to get automatic typehints on the output value of my `load_or_set_cache` function. That way I could know that using a specific identifier, I would get a type of `List[str]` for example, without having to go and look at the definition of the associated function – Eddysanoli Jan 05 '23 at 19:22
  • I still don't understand the usecase of `CacheIdentifier`. If I want to cache `Foo.expensive_fetch(foo_key)`, I wouldn't want to go through the trouble of defining a `CacheIdentifier`, which isn't even serializable b/c of the Callable. I'd want something like `Foo.cached_fetch(key)` via `@lru_cache`. – Kache Jan 05 '23 at 19:50
  • Why is the fact of it not being "serializable" a problem? – Eddysanoli Jan 05 '23 at 20:10
  • 1
    Should be able to store or pass a cache key around for it to be useful for fetching data from a cache. I'll write up a fuller standalone answer. – Kache Jan 05 '23 at 20:44
  • Oh I think I see what you mean. Yeah, the identifiers are not meant to be used as cache keys (basically the name under which you store data). These are meant to be more of an abstraction that groups the key name, the TTL and the function that fetches the data that will be stored in the cache, in order to not have to specify all of those parameters when retrieving data from the cache. – Eddysanoli Jan 05 '23 at 21:19
  • I think your `CacheIdentifier` is trying to do three different things in one: 1) map `str` to types. 2) either a [`dict` with callable default](https://stackoverflow.com/a/43598132/234593) or a common `fetch()` across those types. 3) have those callables be cached, i.e. @lru_cache – Kache Jan 05 '23 at 22:44
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250878/discussion-between-eddysanoli-and-kache). – Eddysanoli Jan 05 '23 at 23:17

1 Answers1

1

I think Pedro has the right idea. Making your CacheIdentifier generic in terms of the func return type works, but not with a TypedDict:

from collections.abc import Callable
from typing import Generic, TypeVar


T = TypeVar("T")


class CacheIdentifier(Generic[T]):
    key: str
    func: Callable[..., T]

    def __init__(self, key: str, func: Callable[..., T]) -> None:
        self.key = key
        self.func = func


def load_or_set_cache(identifier: CacheIdentifier[T]) -> T:
    return identifier.func()


def main() -> None:
    def function_to_fetch_cache_data() -> list[str]:
        return ["a", "b", "c"]

    ident = CacheIdentifier("my_cache_key", func=function_to_fetch_cache_data)
    cache_data = load_or_set_cache(ident)
    reveal_type(cache_data)

Mypy output:

note: Revealed type is "builtins.list[builtins.str]"

Python currently does not support defining generic TypedDict subclasses.


I think your question was confusing some people, not least because you were talking about "dynamically" setting the output type, yet have that be understood by a static type checker. That makes no sense. And nothing about this is dynamic. The types are all known before the program is run.

I recommend you read through this section of PEP 484 (but really the entire thing).

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
  • Oh, awesome. This answers my question perfectly. So either I upgrade to Python 3.11 (or 3.12 since I dont know if 3.11 actually implemented the ability for TypedDicts to inherit for a Generic) or switch to using plain ol' classes. Will probably choose the latter – Eddysanoli Jan 05 '23 at 19:38
  • 1
    Maybe it will be backported to `typing_extensions`. But I think it may take a while for type checkers to catch on anyway, so I still recommend the custom class. – Daniil Fajnberg Jan 05 '23 at 19:42
  • I have modified my CacheIdentifier object according to this example and now I get proper typehints. Thank you! – Eddysanoli Jan 05 '23 at 20:14