From 4dbd8bcbeeca209a5a3bbb70ce2f2573245dcb7e Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:18:43 -0700 Subject: [PATCH 1/2] fix: allow union types in streaming_action.pydantic stream_type parameter Accept types.UnionType in stream_type so Python 3.10+ union syntax (MyModel1 | MyModel2) works with streaming_action.pydantic decorator. Fixes #607 --- burr/core/action.py | 2 +- burr/integrations/pydantic.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/burr/core/action.py b/burr/core/action.py index eb6fceff5..00522d455 100644 --- a/burr/core/action.py +++ b/burr/core/action.py @@ -1511,7 +1511,7 @@ def pydantic( writes: List[str], state_input_type: Type["BaseModel"], state_output_type: Type["BaseModel"], - stream_type: Union[Type["BaseModel"], Type[dict]], + stream_type: Union[Type["BaseModel"], Type[dict], types.UnionType], tags: Optional[List[str]] = None, ) -> Callable: """Creates a streaming action that uses pydantic models. diff --git a/burr/integrations/pydantic.py b/burr/integrations/pydantic.py index 300cbb6a0..db72dd61e 100644 --- a/burr/integrations/pydantic.py +++ b/burr/integrations/pydantic.py @@ -269,7 +269,7 @@ async def async_action_function(state: State, **kwargs) -> State: return decorator -PartialType = Union[Type[pydantic.BaseModel], Type[dict]] +PartialType = Union[Type[pydantic.BaseModel], Type[dict], types.UnionType] PydanticStreamingActionFunctionSync = Callable[ ..., Generator[Tuple[Union[pydantic.BaseModel, dict], Optional[pydantic.BaseModel]], None, None] @@ -290,7 +290,7 @@ async def async_action_function(state: State, **kwargs) -> State: def _validate_and_extract_signature_types_streaming( fn: PydanticStreamingActionFunction, - stream_type: Optional[Union[Type[pydantic.BaseModel], Type[dict]]], + stream_type: Optional[Union[Type[pydantic.BaseModel], Type[dict], types.UnionType]], state_input_type: Optional[Type[pydantic.BaseModel]] = None, state_output_type: Optional[Type[pydantic.BaseModel]] = None, ) -> Tuple[ From 6776e862354254c50254c7328abe7f059665ce8b Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 11 Apr 2026 09:25:49 -0400 Subject: [PATCH 2/2] fix: guard types.UnionType on Python 3.9 to unblock test collection `types.UnionType` was added in Python 3.10 to represent PEP 604 `X | Y` union syntax. Burr supports Python >= 3.9, so on 3.9 any module that references `types.UnionType` at import time hits an AttributeError at collection time, which is why CI was showing: ERROR tests/core/test_action.py - AttributeError: module 'types' has no attribute 'UnionType' ERROR tests/core/test_application.py - AttributeError: module 'types' has no attribute 'UnionType' ERROR tests/core/test_graph.py - AttributeError: module 'types' has no attribute 'UnionType' Introduce a `_UnionType` compatibility alias in `burr/core/action.py`: `types.UnionType` on Python >= 3.10, a placeholder sentinel class on Python < 3.10. Replace every `types.UnionType` reference in `burr/core/action.py` and `burr/integrations/pydantic.py` with that alias. `pydantic.py` imports it from `burr.core.action`, matching the convention used for the other shared symbols imported from that module. Since PEP 604 syntax doesn't exist on Python 3.9, no real union type can reach those call sites on 3.9 anyway, so the sentinel path is never hit at runtime. 3.10+ behavior is unchanged. Verified with full test run under a clean venv: tests/core/test_action.py + test_application.py + test_graph.py: 250 passed Co-Authored-By: Claude Opus 4.6 (1M context) --- burr/core/action.py | 12 +++++++++++- burr/integrations/pydantic.py | 5 +++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/burr/core/action.py b/burr/core/action.py index 00522d455..92144b07d 100644 --- a/burr/core/action.py +++ b/burr/core/action.py @@ -24,6 +24,16 @@ import textwrap import types import typing + +# types.UnionType was added in Python 3.10 to represent PEP 604 `X | Y` +# syntax. Burr supports Python >= 3.9, so on 3.9 we fall back to a sentinel +# type that keeps Union annotations well-formed. No PEP 604 union can ever +# exist on 3.9, so isinstance() checks against it simply never match. +if sys.version_info >= (3, 10): + _UnionType = types.UnionType +else: # pragma: no cover - exercised on Python 3.9 CI only + class _UnionType: # type: ignore[no-redef] + """Placeholder for ``types.UnionType`` on Python < 3.10.""" from collections.abc import AsyncIterator from typing import ( TYPE_CHECKING, @@ -1511,7 +1521,7 @@ def pydantic( writes: List[str], state_input_type: Type["BaseModel"], state_output_type: Type["BaseModel"], - stream_type: Union[Type["BaseModel"], Type[dict], types.UnionType], + stream_type: Union[Type["BaseModel"], Type[dict], _UnionType], tags: Optional[List[str]] = None, ) -> Callable: """Creates a streaming action that uses pydantic models. diff --git a/burr/integrations/pydantic.py b/burr/integrations/pydantic.py index db72dd61e..3ef2a2cee 100644 --- a/burr/integrations/pydantic.py +++ b/burr/integrations/pydantic.py @@ -41,6 +41,7 @@ from burr.core.action import ( FunctionBasedAction, FunctionBasedStreamingAction, + _UnionType, bind, derive_inputs_from_fn, ) @@ -269,7 +270,7 @@ async def async_action_function(state: State, **kwargs) -> State: return decorator -PartialType = Union[Type[pydantic.BaseModel], Type[dict], types.UnionType] +PartialType = Union[Type[pydantic.BaseModel], Type[dict], _UnionType] PydanticStreamingActionFunctionSync = Callable[ ..., Generator[Tuple[Union[pydantic.BaseModel, dict], Optional[pydantic.BaseModel]], None, None] @@ -290,7 +291,7 @@ async def async_action_function(state: State, **kwargs) -> State: def _validate_and_extract_signature_types_streaming( fn: PydanticStreamingActionFunction, - stream_type: Optional[Union[Type[pydantic.BaseModel], Type[dict], types.UnionType]], + stream_type: Optional[Union[Type[pydantic.BaseModel], Type[dict], _UnionType]], state_input_type: Optional[Type[pydantic.BaseModel]] = None, state_output_type: Optional[Type[pydantic.BaseModel]] = None, ) -> Tuple[