Skip to content

Conversation

@nathan-gage
Copy link
Contributor

@nathan-gage nathan-gage commented Nov 11, 2025

ModelRetry is not hashable -- this broke some compatibility with our telemetry sdk (ddtrace specifically).

Am not 100% familiar with the lore, hopefully this is ok!

Summary

  • Added __hash__ method to ModelRetry exception class to make it hashable

Test plan

  • Ran existing retry-related tests - all passed
  • Verified type checking with pyright - no errors

🤖 Generated with Claude Code

This change adds a `__hash__` method to the `ModelRetry` exception class,
making it hashable so it can be used in sets and as dictionary keys. The
hash is based on both the class type and the message attribute, which
aligns with the existing `__eq__` implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@DouweM
Copy link
Collaborator

DouweM commented Nov 11, 2025

@nathan-gage Hey Nathan, the change looks fine (just lacking test coverage), but I'd like the understand the issue you were seeing with ddtrace to know whether we should be doing this on all exception classes. Can you share the exception trace or something?

Tests that all exception classes are hashable and can be used as dict keys and set members.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@nathan-gage
Copy link
Contributor Author

@DouweM

Here is the traceback
builtins.TypeError: TypeError: unhashable type: 'ModelRetry'
Traceback (most recent call last):
  File "/app/venv/lib64/python3.11/site-packages/opentelemetry/trace/__init__.py", line 589, in use_span
    yield span
  File "/app/venv/lib64/python3.11/site-packages/pydantic_graph/beta/graph.py", line 271, in iter
    async with GraphRun[StateT, DepsT, OutputT](
    
  File "/app/venv/lib64/python3.11/site-packages/pydantic_graph/beta/graph.py", line 400, in __aexit__
    await self._async_exit_stack.__aexit__(exc_type, exc_val, exc_tb)
  File "/usr/lib64/python3.11/contextlib.py", line 745, in __aexit__
    raise exc_details[1]
  File "/usr/lib64/python3.11/contextlib.py", line 726, in __aexit__
    cb_suppress = cb(*exc_details)
                  ^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/contextlib.py", line 158, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/app/venv/lib64/python3.11/site-packages/pydantic_graph/beta/graph.py", line 939, in _unwrap_exception_groups
    raise exception
  File "/usr/lib64/python3.11/asyncio/tasks.py", line 277, in __step
    result = coro.send(None)
             ^^^^^^^^^^^^^^^
  File "/app/venv/lib64/python3.11/site-packages/ddtrace/contrib/internal/asyncio/patch.py", line 67, in traced_coro
    return await coro
           ^^^^^^^^^^
  File "/app/venv/lib64/python3.11/site-packages/pydantic_graph/beta/graph.py", line 711, in _run_tracked_task
    result = await self._run_task(t_)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib64/python3.11/site-packages/pydantic_graph/beta/graph.py", line 740, in _run_task
    output = await node.call(step_context)
    
  File "/app/venv/lib64/python3.11/site-packages/pydantic_graph/beta/step.py", line 253, in _call_node
    return await node.run(GraphRunContext(state=ctx.state, deps=ctx.deps))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 544, in run
    async with self.stream(ctx):
  File "/usr/lib64/python3.11/contextlib.py", line 217, in __aexit__
    await anext(self.gen)
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 558, in stream
    async for _event in stream:
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 676, in _run_stream
    async for event in self._events_iterator:
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 637, in _run_stream
    async for event in self._handle_tool_calls(ctx, tool_calls):
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 692, in _handle_tool_calls
    async for event in process_tool_calls(
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 889, in process_tool_calls
    async for event in _call_tools(
    
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 1019, in _call_tools
    if event := await handle_call_or_result(coro_or_task=task, index=index):
    
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 984, in handle_call_or_result
    (await coro_or_task) if inspect.isawaitable(coro_or_task) else coro_or_task.result()
    
  File "/usr/lib64/python3.11/asyncio/futures.py", line 290, in __await__
    return self.result()  # May raise too.
           ^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/asyncio/futures.py", line 203, in result
    raise self._exception.with_traceback(self._exception_tb)
  File "/usr/lib64/python3.11/asyncio/tasks.py", line 279, in __step
    result = coro.throw(exc)
             ^^^^^^^^^^^^^^^
  File "/app/venv/lib64/python3.11/site-packages/ddtrace/contrib/internal/asyncio/patch.py", line 67, in traced_coro
    return await coro
           ^^^^^^^^^^
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_agent_graph.py", line 1038, in _call_tool
    tool_result = await tool_manager.handle_call(tool_call)
    
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_tool_manager.py", line 112, in handle_call
    return await self._call_function_tool(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_tool_manager.py", line 237, in _call_function_tool
    tool_result = await self._call_tool(call, allow_partial, wrap_validation_errors)
    
  File "/app/venv/lib64/python3.11/site-packages/pydantic_ai/_tool_manager.py", line 163, in _call_tool
    except (ValidationError, ModelRetry) as e:
    
  File "/app/venv/lib64/python3.11/site-packages/ddtrace/errortracking/_handled_exceptions/callbacks.py", line 66, in _default_bytecode_exc_callback
    _default_errortracking_exc_callback(span=span, exc=exc)
TypeError: unhashable type: 'ModelRetry'
`ddtrace` config

We are using ddtrace with:

- name: DD_TRACE_OTEL_ENABLED
  value: "true"
- name: DD_LOGS_OTEL_ENABLED
  value: "true"
- name: DD_METRICS_OTEL_ENABLED
  value: "false"
- name: DD_TRACE_PYDANTIC_AI_ENABLED
  value: "false"

and instrumented like:

# ruff: noqa: E402
import ddtrace.auto  # noqa: F401, I001
from opentelemetry import metrics, trace  # noqa: F401

from ddtrace.internal.opentelemetry.metrics import set_otel_meter_provider

set_otel_meter_provider()  # type: ignore[no-untyped-call]

# start app...

I believe the problem is that Exceptions defined with __eq__ also need __hash__. So the other exceptions are fine. Also added tests as requested.

@DouweM DouweM changed the title Make ModelRetry hashable Make ModelRetry hashable Nov 11, 2025
@DouweM DouweM enabled auto-merge (squash) November 11, 2025 23:32
@DouweM
Copy link
Collaborator

DouweM commented Nov 11, 2025

@nathan-gage Thanks Nathan, makes sense.

@DouweM DouweM merged commit b8d2904 into pydantic:main Nov 11, 2025
30 checks passed
@nathan-gage nathan-gage deleted the make-modelretry-hashable branch November 11, 2025 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants