Skip to content

Conversation

@karpetrosyan
Copy link
Collaborator

@karpetrosyan karpetrosyan commented Nov 19, 2025

Fixes typing so it also works with special types like Literal, Union, etc. Currently, we use type[T] and return T to unwrap the class type, telling the type checker that we return the instance, not the type. When special types are passed, the type checker fails because special types are not subclasses of type, and there is nothing to unwrap in this case.

PEP 747 would solve this problem by providing a way to annotate these types. Until then, this workaround can be used:

from __future__ import annotations

from typing import Union, TypeVar, cast, overload

T = TypeVar("T")


class Omit: ...


omit = Omit()


class SomeModel: ...


# This overload should be listed first to prioritize it over generic overload
# so calls with Omit() resolve to ParsedBetaMessage[None] not ParsedBetaMessage[Omit]
@overload
def parse(output_format: None | Omit = omit) -> None: ...
# This should be before the generic overload, so if the type is a class type, we unwrap it.
# Otherwise, something like Union[str, int] would incorrectly resolve to type[str] | type[int].
@overload
def parse(output_format: type[T]) -> T: ... # for classes
@overload
def parse(output_format: T) -> T: ...  # for special types
def parse(output_format: None | type[T] | T | Omit = omit) -> T:
    return cast(T, None)


just_none = parse(omit) # None
special_type = parse(Union[str, int]) # str | int 
model_type = parse(SomeModel) # SomeModel

@karpetrosyan karpetrosyan requested a review from a team as a code owner November 19, 2025 17:26
@karpetrosyan karpetrosyan marked this pull request as draft November 19, 2025 17:29
@karpetrosyan karpetrosyan marked this pull request as ready for review November 19, 2025 17:46
@karpetrosyan karpetrosyan requested a review from Copilot November 19, 2025 17:53
Copilot finished reviewing on behalf of karpetrosyan November 19, 2025 17:57
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds support for special types (like Literal, Union, etc.) in structured outputs by introducing a sophisticated overload pattern. The solution works around current typing limitations (until PEP 747 is supported) by providing three separate overloads for each parsing/streaming method: one for None | Omit, one for class types type[T] that unwraps to instance T, and one for special types that pass through as-is.

Key Changes

  • Introduced three overloads for parse() and stream() methods (both sync and async) to handle different output format types
  • Changed ParseMessageCreateParamsBase.output_format from type[ResponseFormatT] to ResponseFormatT to accept special types
  • Updated snapshot test utilities to handle single responses more gracefully
  • Added comprehensive test coverage for special types using Literal

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/anthropic/resources/beta/messages/messages.py Added three overloads each for parse() and stream() methods (sync and async) to support None, class types, and special types
src/anthropic/types/beta/message_create_params.py Changed output_format type from type[ResponseFormatT] to ResponseFormatT to accommodate special types
tests/lib/_parse/test_beta_messages.py Added new test file with tests for both regular class types and special types (Literal)
tests/lib/snapshots.py Modified snapshot handling to wrap single responses in a list instead of asserting they must be lists
tests/lib/_parse/init.py Added empty init file for new test module

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@RobertCraigie
Copy link
Collaborator

Could you check if we could do something similar to python/mypy#19227 (comment) where we only support special types for Pyright? It'd be ideal if we could avoid using overloads here imo

@karpetrosyan
Copy link
Collaborator Author

I tried it, to be honest, and it doesn't work.
Note that the workaround here is not the overload—the overload we need in any case because even with TypeForm[T] -> T, passing something like omit will not be resolved to None but will be resolved to Omit. The workaround here is to add one more overload that tries to extract the instance type, e.g., (type[int] -> int), and then falls back to just using the same type, e.g., (int | str -> int | str).

# This should be before the generic overload, so if the type is a class type, we unwrap it.
# Otherwise, something like Union[str, int] would incorrectly resolve to type[str] | type[int].
@overload
def parse(output_format: type[T]) -> T: ... # for classes
@overload
def parse(output_format: T) -> T: ...  # for special types

extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
) -> ParsedBetaMessage[ResponseFormatT]: ...
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need a fallback overload for the "you're passing any supported thing" case like we have for streaming?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think in the streaming case we do it so we can annotate cases when the boolean is not statically known, so we just say it's either a streaming or non-streaming response, not just because we need to have an 'any supported thing' case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah do we not need that same thing here? e.g. if you're writing a wrapper function, you would get type errors?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah do we not need that same thing here?

I think this is a slightly different case, so no.

e.g. if you're writing a wrapper function, you would get type errors?

I'm not quite following this. Can you explain the use case?

@fede-kamel
Copy link

@RobertCraigie @karpetrosyan is anthropic participating at all in this conversations? I don't understand their level of participation.

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.

3 participants