Skip to content

feat(function_schema): add enforce_type_annotations flag for stricter schema validation with type enforcement, clearer errors, and updated docstrings #1092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

mshsheikh
Copy link
Contributor

Problem:
Previously, the function_schema function silently defaulted unannotated parameters to Any, leading to:

  • Ambiguous JSON schemas ("type": "any")
  • Potential runtime errors due to invalid inputs
  • Reduced reliability of LLM tool calls This made it harder to ensure type safety without manual validation.

Solution:

This PR introduces the enforce_type_annotations flag to function_schema, allowing developers to:

  1. Enforce type annotations by setting enforce_type_annotations=True, which raises a ValueError for unannotated parameters.
  2. Maintain backward compatibility by defaulting to Any when enforce_type_annotations=False (default).

Example error message:

raise ValueError(
    f"Parameter '{name}' must be type-annotated. Example: def func({name}: str)"
)

Changes Made

  1. New Parameter:
   def function_schema(
       func: Callable[..., Any],
       enforce_type_annotations: bool = False,  # New parameter
       ...
   ):
  1. Validation Logic:
  • If enforce_type_annotations=True, unannotated parameters now raise a ValueError with a clear example.
  • Falls back to Any if disabled (default behavior preserved).
  1. Docstring Updates:
  • Added detailed documentation for enforce_type_annotations:
     Args:
         enforce_type_annotations: If True, raises a ValueError for any unannotated parameters.
             If False (default), unannotated parameters are assumed to be of type `Any`.
  1. Error Message Improvements:
  • Clear guidance for developers: Example: def func(x: str)

Backward Compatibility:

  • Preserved: Existing code continues to work as-is (default enforce_type_annotations=False).
  • Opt-in: Type annotation enforcement is optional, allowing gradual adoption.

Testing Guidance:
To verify the change:

def test_enforce_type_annotations():
    def func(x): ...
    with pytest.raises(ValueError):
        function_schema(func, enforce_type_annotations=True)
    # Should not raise
    function_schema(func, enforce_type_annotations=False)

Impact:

  • Type Safety: Prevents ambiguous schemas and improves LLM tool reliability.
  • Developer Experience: Clear error messages guide users to fix missing annotations.
  • Flexibility: Maintains backward compatibility while enabling stricter validation.

Example Usage:

# Strict mode: raises error for unannotated params
schema = function_schema(my_func, enforce_type_annotations=True)

# Default mode: works as before
schema = function_schema(my_func)  # silently defaults to Any

… schema validation with type enforcement, clearer errors, and updated docstrings

Problem:
Previously, the `function_schema` function silently defaulted unannotated parameters to `Any`, leading to:
- Ambiguous JSON schemas (`"type": "any"`)
- Potential runtime errors due to invalid inputs
- Reduced reliability of LLM tool calls
This made it harder to ensure type safety without manual validation.


Solution:

This PR introduces the `enforce_type_annotations` flag to `function_schema`, allowing developers to:
1. Enforce type annotations by setting `enforce_type_annotations=True`, which raises a `ValueError` for unannotated parameters.
2. Maintain backward compatibility by defaulting to `Any` when `enforce_type_annotations=False` (default).

Example error message:
```python
raise ValueError(
    f"Parameter '{name}' must be type-annotated. Example: def func({name}: str)"
)
```


Changes Made

1. New Parameter:
```python
   def function_schema(
       func: Callable[..., Any],
       enforce_type_annotations: bool = False,  # New parameter
       ...
   ):
```

2. Validation Logic:
- If `enforce_type_annotations=True`, unannotated parameters now raise a `ValueError` with a clear example.
- Falls back to `Any` if disabled (default behavior preserved).

3. Docstring Updates:
- Added detailed documentation for `enforce_type_annotations`:
```python
     Args:
         enforce_type_annotations: If True, raises a ValueError for any unannotated parameters.
             If False (default), unannotated parameters are assumed to be of type `Any`.
```


4. Error Message Improvements:
- Clear guidance for developers:
`Example: def func(x: str)`


Backward Compatibility:
- Preserved: Existing code continues to work as-is (default `enforce_type_annotations=False`).
- Opt-in: Type annotation enforcement is optional, allowing gradual adoption.


Testing Guidance:
To verify the change:
```python
def test_enforce_type_annotations():
    def func(x): ...
    with pytest.raises(ValueError):
        function_schema(func, enforce_type_annotations=True)
    # Should not raise
    function_schema(func, enforce_type_annotations=False)
```


Impact:
- Type Safety: Prevents ambiguous schemas and improves LLM tool reliability.
- Developer Experience: Clear error messages guide users to fix missing annotations.
- Flexibility: Maintains backward compatibility while enabling stricter validation.


Example Usage:
```python
# Strict mode: raises error for unannotated params
schema = function_schema(my_func, enforce_type_annotations=True)

# Default mode: works as before
schema = function_schema(my_func)  # silently defaults to Any
```
@seratch seratch added enhancement New feature or request feature:core labels Jul 14, 2025
Copy link
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

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

having unit tests would be appreciated too

@@ -186,6 +186,7 @@ def generate_func_documentation(

def function_schema(
func: Callable[..., Any],
enforce_type_annotations: bool = False,
Copy link
Member

Choose a reason for hiding this comment

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

adding this option may be a good one, but adding here (=2nd arg) is a breaking change. new arguments must be added as the last one when a method accepts both args and keyword args.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback! You're absolutely right, the enforce_type_annotations parameter has been moved to the end of the argument list in the latest update to preserve backward compatibility.

@mshsheikh mshsheikh requested a review from seratch July 14, 2025 15:05
Adds a new enforce_type_annotations flag to function_schema to improve type safety:

- When enabled (True): Raises an error for unannotated parameters (e.g., def func(x): ...).
- When disabled (False): Falls back to Any (default behavior remains unchanged).
- Backward compatibility: Positional arguments still work (e.g., function_schema(my_func, "sphinx")).

Example Error Message:
Parameter 'x' must be type-annotated. Example: def func(x: str)

Test Coverage:
https://github.com/openai/openai-agents-python/blob/main/tests/test_function_schema.py
@mshsheikh
Copy link
Contributor Author

having unit tests would be appreciated too

@seratch
Unit tests have been added to validate the enforce_type_annotations behavior, including:

  • Enforcement of type hints (True/False cases)
  • Fallback to Any when disabled
  • Backward compatibility with positional args

Tests are here:

Adds a new enforce_type_annotations flag to function_schema to improve type safety:

- When enabled (True): Raises an error for unannotated parameters (e.g., def func(x): ...).
- When disabled (False): Falls back to Any (default behavior remains unchanged).
- Backward compatibility: Positional arguments still work (e.g., function_schema(my_func, "sphinx")).

Example Error Message:
Parameter 'x' must be type-annotated. Example: def func(x: str)

Test Coverage:
https://github.com/openai/openai-agents-python/blob/main/tests/test_function_schema.py
Copy link
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

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

this looks good to me, but @rm-openai if you have a different opinion, please share it

@seratch seratch requested a review from rm-openai July 15, 2025 03:48
Copy link
Collaborator

@rm-openai rm-openai left a comment

Choose a reason for hiding this comment

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

My general stance is to be careful about adding new options/settings bc each one makes it that much harder to understand the code. In this case, my thought is -- not using types is your choice, why would someone not use type annotations but also use enforce_type_annotations?

@mshsheikh mshsheikh requested a review from rm-openai July 15, 2025 15:59
@mshsheikh
Copy link
Contributor Author

My general stance is to be careful about adding new options/settings bc each one makes it that much harder to understand the code. In this case, my thought is -- not using types is your choice, why would someone not use type annotations but also use enforce_type_annotations?

Thank you for the thoughtful feedback! The enforce_type_annotations flag is designed as an opt-in tool for teams gradually adopting type safety, not a default requirement. This allows:

  • Progressive enforcement : New code can opt into stricter validation without breaking legacy functions.
  • Explicit error prevention : Catches unannotated parameters early, reducing ambiguous schemas and runtime issues.
    The default (False) preserves backward compatibility, minimizing overhead for existing users.
    Would this approach address your concerns while balancing flexibility and safety?

@mshsheikh
Copy link
Contributor Author

Thanks for the thorough feedback @seratch and @rm-openai!
The latest changes address the positional argument issue and include comprehensive tests. I’ve also revised the flag’s documentation to clarify its purpose as an opt-in tool for stricter validation. Let me know if further adjustments are needed!

Key Updates:

  • enforce_type_annotations added as the last parameter to avoid breaking existing usage.
  • Tests validate enforcement, fallback behavior, and backward compatibility.
  • Error messages include actionable examples for developers.

This balances flexibility and safety while respecting existing workflows. Let me know your thoughts!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request feature:core
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants