Summary
_call_user_fn_args in braintrust/framework.py calls inspect.signature(fn) without specifying annotation_format. On Python 3.14 (PEP 649), this defaults to Format.VALUE, which eagerly evaluates annotations. If a callback uses TYPE_CHECKING-only imports in its annotations, the signature call raises NameError.
The bare except on line ~467 catches the error, but the fallback behavior (return [], kwargs) passes all kwargs through unfiltered. For task functions, this means Braintrust can't detect whether the task accepts a hooks parameter (line ~1468), so it silently omits hooks — crashing the task on the missing required argument.
Reproduction
# Python 3.14+
from typing import TYPE_CHECKING, Any
from braintrust import EvalCase, EvalHooks, Eval
if TYPE_CHECKING:
from some_package import SomeType # not available at runtime
async def my_task(
input: str,
hooks: EvalHooks[frozenset[SomeType]], # <-- NameError at runtime
) -> dict:
return {"answer": input}
async def my_scorer(
input: str,
output: dict,
expected: frozenset[Any],
**_: object,
) -> float:
return 1.0
Eval(
"repro",
data=[EvalCase(input="hello", expected=frozenset())],
task=my_task,
scores=[my_scorer],
)
inspect.signature(my_task) raises NameError: name 'SomeType' is not defined because PEP 649's lazy annotation evaluation is triggered by inspect.signature in VALUE format.
Suggested fix
Use annotation_format=annotationlib.Format.FORWARDREF (or inspect.Format.FORWARDREF) in the inspect.signature call. FORWARDREF wraps unresolvable names as ForwardRef objects instead of raising, and parameter name/kind inspection (which is all _call_user_fn_args needs) works identically.
import annotationlib
try:
signature = inspect.signature(fn, annotation_format=annotationlib.Format.FORWARDREF)
except:
return [], kwargs
This also applies to the inspect.signature call on line ~1468 that checks whether the task accepts a hooks argument.
Workaround
Annotate the hooks parameter with EvalHooks[Any] instead of a concrete type. Any is always available at runtime.
Environment
- Python 3.14.3
- braintrust 0.14.0
Summary
_call_user_fn_argsinbraintrust/framework.pycallsinspect.signature(fn)without specifyingannotation_format. On Python 3.14 (PEP 649), this defaults toFormat.VALUE, which eagerly evaluates annotations. If a callback usesTYPE_CHECKING-only imports in its annotations, the signature call raisesNameError.The bare
excepton line ~467 catches the error, but the fallback behavior (return [], kwargs) passes all kwargs through unfiltered. For task functions, this means Braintrust can't detect whether the task accepts ahooksparameter (line ~1468), so it silently omitshooks— crashing the task on the missing required argument.Reproduction
inspect.signature(my_task)raisesNameError: name 'SomeType' is not definedbecause PEP 649's lazy annotation evaluation is triggered byinspect.signatureinVALUEformat.Suggested fix
Use
annotation_format=annotationlib.Format.FORWARDREF(orinspect.Format.FORWARDREF) in theinspect.signaturecall.FORWARDREFwraps unresolvable names asForwardRefobjects instead of raising, and parameter name/kind inspection (which is all_call_user_fn_argsneeds) works identically.This also applies to the
inspect.signaturecall on line ~1468 that checks whether the task accepts a hooks argument.Workaround
Annotate the
hooksparameter withEvalHooks[Any]instead of a concrete type.Anyis always available at runtime.Environment