2025, Dec 01 01:00

Fix Pyright/Pylance incompatible override errors from aiocache @cached on overridden async methods

Learn why aiocache @cached breaks overridden async method typing in Python, triggering Pyright/Pylance incompatible override errors, plus pragmatic fixes.

Decorating an overridden async method with aiocache’s @cached can look perfectly fine at runtime, yet it immediately trips type checkers. The symptom is a method override flagged as incompatible, even though the return types match. If you’ve seen pylance or pyright complain that your subclass method no longer matches the base class signature right after adding @cached, you’re in the right place.

Minimal example that reproduces the issue

The following repository-style abstraction illustrates the problem. The base class defines an async method with a type-parameterized return value. The subclass overrides it with the same return type and adds caching via @cached.

from typing import Generic, TypeVar, List
from aiocache import cached
X = TypeVar("X")
# Base repository
class RepoBase(Generic[X]):
    async def fetch_all(self) -> List[X]:
        ...
# Concrete repository
class CatalogRepo(RepoBase[CategoryModel]):
    @cached(ttl=600, key_builder=clazzaware_builder)
    async def fetch_all(self) -> List[CategoryModel]:
        return await super().fetch_all()

After adding the decorator, the type checker raises an override error. One typical message looks like this:

"get_all" overrides method of same name in class "IBaseRepository" with incompatible type "_Wrapped[..., Unknown, ..., CoroutineType[Any, Any, Unknown]]"

Even if caching is moved into a separate service function that is itself decorated, the decorator changes the function’s shape from the checker’s perspective, and the complaint persists. The backend may be Redis, but the behavior stems from the decorator.

What’s actually going on

This runs afoul of the Liskov Substitution Principle. The subclass method, once wrapped by @cached, is no longer substitutable for the parent method in every way the type checker expects. A quick runtime probe makes the mismatch visible by comparing method attributes that exist on the original function but not on the wrapped callable.

from typing import Generic, TypeVar
from aiocache import cached
S = TypeVar("S")
class Root(Generic[S]):
    async def fetch_all(self) -> list[S]:
        ...
class Leaf(Root[int]):
    @cached
    async def fetch_all(self) -> list[int]:
        return await super().fetch_all()
print(Root.fetch_all.__code__)   # OK
print(Leaf.fetch_all.__code__)   # Runtime error
# AttributeError: 'cached' object has no attribute '__code__'. Did you mean: '__call__'?

The wrapped attribute in the subclass is a descriptor that doesn’t present the same function attributes as the base method. That breaks substitutability from the type checker’s point of view and triggers the override error. Descriptor overrides are useful at runtime, but they make static analysis and introspection unhappy here.

Practical fix when you need @cached

If you are using pyright or pylance and want to keep the decorator on an overridden method, the straightforward, typing-friendly move is to silence the diagnostic at the site. Add a type ignore on the decorated definition and proceed.

from typing import Generic, TypeVar, List
from aiocache import cached
Y = TypeVar("Y")
class StorageBase(Generic[Y]):
    async def pull_all(self) -> List[Y]:
        ...
class TopicRepo(StorageBase[TopicEntity]):
    @cached(ttl=600, key_builder=clazzaware_builder)  # type: ignore
    async def pull_all(self) -> List[TopicEntity]:
        return await super().pull_all()

This acknowledges the limitation while keeping the runtime behavior intact. If you continue to rely on runtime introspection of the method object, avoid assuming attributes like __code__ are present once @cached has wrapped the function.

A note about type information in the library

There’s another contributing factor. The aiocache package does not ship a py.typed marker even though there is one in the GitHub repository. In practice this means the library is treated as untyped by type checkers and may surface additional typing errors beyond the override issue.

Why this matters for type-checked Python

In a codebase that enforces method signature compatibility across inheritance hierarchies, decorators that replace functions with non-function descriptors change the shape in ways static tools correctly flag. Knowing that this is a deliberate trade-off helps avoid chasing false leads in your own typing setup. The behavior is rooted in substitutability, not in your specific domain classes or backend cache store.

Takeaways

If @cached on an overridden async method triggers an incompatible override error, you are looking at an LSP violation caused by the wrapper object replacing the function. The pragmatic resolution for pyright or pylance users is to add a type ignore on that definition. Be aware that aiocache currently doesn’t publish a py.typed marker, so additional typing friction is possible. If you need to introspect methods, avoid leaning on attributes like __code__ on decorated overrides. With those constraints in mind, you can keep caching in place without letting the type checker block your build.