Skip to content

[cache] @cache raises TypeError for unhashable arguments (e.g. Pydantic model inputs) - #110

Merged
manish-wekan merged 2 commits into
nitrocloudofficial:developfrom
AadarshM07:issue#59
Nov 5, 2026
Merged

manish-wekan merged 2 commits into
nitrocloudofficial:developfrom
AadarshM07:issue#59

Conversation

@AadarshM07

Copy link
Copy Markdown
Collaborator

@cache raised an unhandled TypeError when passed unhashable arguments (e.g. Pydantic models).

Changes:

  • Pydantic BaseModel arguments are now serialized via model_dump(mode="json") for a stable cache key.
  • Added optional key= callable to cache() for custom key logic on any argument type.
  • Unhashable non-Pydantic args now raise a descriptive TypeError naming the offending argument.
  • Fixed duplicate import block that caused a Pyright false-positive on the new key parameter.
  • Added tests/test_cache_unhashable.py covering all of the above.

Closes #59

@AadarshM07 AadarshM07 changed the title fix(cache): handle unhashable args and add key= callable [cache] @cache raises TypeError for unhashable arguments (e.g. Pydantic model inputs) Nov 4, 2026
@AadarshM07
AadarshM07 requested a review from ROHIT-JR November 4, 2026 12:09

@ROHIT-JR ROHIT-JR left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. The approach matches issue #59, CI is green on 3.10, 3.11 and 3.12, and the new tests pass on my checkout. I ran the code with extra cases, and two of them can return a wrong cached result. That is worse than the TypeError this PR removes, so I'm asking for changes.

Changes needed

1. A field marked exclude=True is left out of the cache key, so different inputs share one entry.
model_dump(mode="json") drops excluded fields.

class E(BaseModel):
    q: str
    secret: str = Field(default="", exclude=True)

@cache(ttl=60)
async def g(x): return x.secret

await g(E(q="a", secret="alice"))  # 'alice'
await g(E(q="a", secret="bob"))    # 'alice'  <- should be 'bob'

If such a field is something like a user id or tenant, one caller gets another caller's cached result. Please build the key from all model fields. Checked on this checkout: pydantic_core.to_jsonable_python({n: getattr(v, n) for n in type(v).model_fields}) includes the excluded field.

2. The key uses only type(value).__name__, so two different models with the same class name collide.
Two Q models from different modules (or defined inside different functions) share a key. In my run, f(B(q="a")) returned the result cached for A(q="a"). Please use type(value).__module__ and __qualname__, or the class object itself.

Please add a regression test for each. Both fail on the current code.

Non-blocking

  • A custom key= that returns something unhashable (for example a list) fails with the raw cannot use 'list' as a dict key message. The same descriptive error would help here.
  • A model with an arbitrary type raises PydanticSerializationError rather than the descriptive TypeError that points to key=.
  • A list of models, a very common input, still raises TypeError (a tuple of models does too). The message points to key=, so this is acceptable, but a short note in the docstring would help.
  • Tidy-up: cache_lookup_key is just an alias of cache_key, so one name is enough.

Everything else I checked works: a parameter named key on the cached function is fine, and ExecutionContext arguments are still skipped.

@AadarshM07

Copy link
Copy Markdown
Collaborator Author

Hi @ROHIT-JR , thanks for the catch, I have addressed these issues and added a few updated test cases for it

@manish-wekan
manish-wekan merged commit ca3e7b8 into nitrocloudofficial:develop Nov 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cache] @cache raises TypeError for unhashable arguments (e.g. Pydantic model inputs)

3 participants