Skip to content

feat: high-level pydantic API for classification #3

Description

@emiliano-go

Proposal: High-level pydantic API for TypeSafe classification

Problem

The current SDK requires constructing question objects manually and using a context manager, which is verbose for common classification tasks:

with TypeSafeClient() as client:
    response = client.system_one(
        state="I was charged twice",
        questions={
            "billing": Noul(instructions="Is this billing?"),
            "tone": Choice(instructions="Tone?", criteria={"calm": None, "angry": None}),
        },
    )
print(response.choices["tone"].choice)

Proposed solution

A pydantic-based schema layer with three API shapes:

1. Simple function (single call):

from typesafe_sdk import typesafe

result = typesafe("I was charged twice", billing="Is this billing?", tone="calm|angry")
result["billing"]  # 0.98
result["tone"]     # "calm"

2. Typed schema (structured results):

from typesafe_sdk import typesafe, TypeSafeSchema
from typing import Literal

class Classification(TypeSafeSchema):
    billing: bool
    tone: Literal["calm", "angry"]

result = typesafe("I was charged twice", model=Classification)
result.billing  # True
result.tone     # "calm"

3. Decorator (function signature as schema):

from typesafe_sdk import typesafe

@typesafe
def classify(ticket: str, *, billing: bool, tone: Literal["calm", "angry"]):
    pass

result = classify("I was charged twice")
result.billing  # True
result.tone     # "calm"

Design decisions

  • msgspec stays for wire serialization. It handles tagged unions, Raw deferred parsing, and key coercion in ways pydantic cannot. The two libraries serve different layers: msgspec for the wire, pydantic for the user-facing schema.
  • _core/ untouched. New code sits on top, reusing TypeSafeClient internally.
  • Backwards compatible. Existing TypeSafeClient API works unchanged.

Type mapping

Python type Question type Answer type
bool noul bool or float
str noul str ("yes"/"no")
Literal[...] choice str
float / int score float
Optional[X] (unwrapped) depends on X

Implementation

A working implementation is available at https://github.com/emiliano-go/typesafe-sdk-python with two PRs:

All 656 existing tests pass. _core/ is completely unaffected.

Questions for maintainers

  1. Is pydantic an acceptable new dependency, or should it be an optional extra?
  2. Any preference on naming (e.g. typesafe() vs classify())?
  3. Should this target main or a feature branch?

Activity

  1. danielgafni commented on Sep 17, 2026

    @danielgafni
    Contributor

    Hi!

    That's a great proposal! I think overall it makes sense.

    We currently do not accept PRs (this will change in the future), so I will try to cook something in our internal repo tomorrow.

    Also it's like we would not want this as part of the SDK library, it will be a separate project.


    Edit: apparently we already have something like this in progress 🤣

  2. emiliano-go commented on Sep 17, 2026

    @emiliano-go
    Author

    Glad the proposal is useful!

    My working implementation is available at https://github.com/emiliano-go/typesafe-sdk-python with the two PRs linked above, in case it helps as a reference for your internal work. Happy to answer any questions about the design decisions (the msgspec vs pydantic split, the type mapping logic, the edge case handling, etc.).

    Feel free to AMA.

  3. danielgafni commented on Sep 17, 2026

    @danielgafni
    Contributor

    We'll likely be fully switching to Pydantic anyway by the way. I'm intending to get this out tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions