# How to Use needle.Field with Annotated for Precise Tool Constraints in Python

> Learn to use needle.Field with Annotated in Python for precise tool constraints. Automatically generate OpenAI-compatible tool descriptions from function parameters.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: how-to-guide
- Published: 2026-08-14

---

**`needle.Field` combined with `typing.Annotated` lets you attach JSON Schema constraints directly to function parameters, automatically generating OpenAI-compatible tool descriptions without manual schema writing.**

The needle library streamlines the creation of LLM tool interfaces by inspecting Python type hints and metadata. By pairing `needle.Field` with `typing.Annotated`, you define validation rules, descriptions, and value constraints that compile into complete JSON Schema definitions for AI agents.

## Understanding the Field Descriptor

The `Field` class in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) acts as a lightweight container for JSON Schema constraints. Implemented in lines 17-21, its `__init__` method accepts arbitrary keyword arguments representing schema properties:

```python

# Conceptual implementation from needle/agent/tools.py

class Field:
    def __init__(self, **kwargs):
        self.constraints = kwargs
    
    def apply(self, schema: dict) -> None:
        # Lines 35-48: Merges constraints into the parameter schema

        schema.update(self.constraints)

```

When decorated with `@tool`, your function undergoes **signature inspection** via the `build_schema` function. This process iterates over each parameter, extracts type hints including `Annotated` metadata, and constructs the final tool definition compatible with OpenAI's function-calling specification.

## Detecting Annotated Metadata

The extraction logic relies on `_field_of`, defined in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) lines 84-91. This helper inspects each parameter annotation for `typing.Annotated` types and retrieves any embedded `Field` instances:

1. **Signature analysis** – `build_schema` retrieves the function's type hints with `include_extras=True` to preserve metadata
2. **Metadata scanning** – For each parameter, `_field_of` checks if the annotation is an `Annotated` type and searches its metadata tuple for a `Field` instance
3. **Constraint application** – If found, `Field.apply` (lines 35-48) merges the stored constraints into that parameter's JSON schema

The system automatically determines **required fields** by checking for default values. A parameter becomes required only when it lacks both a function default and a `Field`-provided default, and when its type is not `Optional`.

## Practical Implementation Example

Here is a complete implementation demonstrating precision constraints for a translation tool:

```python
from typing import Annotated
from needle.agent.tools import Field, tool

@tool
def translate(
    text: Annotated[
        str,
        Field(
            description="The text to translate",
            min_length=1,
            max_length=500,
        ),
    ],
    target_lang: Annotated[
        str,
        Field(
            description="ISO-639-1 language code",
            enum=["en", "es", "fr", "de"],
        ),
    ] = "en",
) -> str:
    """Translate text into target_lang."""
    return f"Translated '{text}' to {target_lang}"

```

When you access `translate._needle_tool` (the internal schema registry), needle generates the following OpenAI-compatible structure:

```json
{
  "name": "translate",
  "description": "Translate text into target_lang.",
  "parameters": {
    "type": "object",
    "properties": {
      "text": {
        "type": "string",
        "description": "The text to translate",
        "minLength": 1,
        "maxLength": 500
      },
      "target_lang": {
        "type": "string",
        "description": "ISO-639-1 language code",
        "enum": ["en", "es", "fr", "de"]
      }
    },
    "required": ["text"]
  }
}

```

Notice that `text` appears in the `required` array because it has no default value, while `target_lang` remains optional due to its default of `"en"`.

## Supported Constraint Types

The `Field` class supports any JSON Schema-valid constraint passed as keyword arguments. Common validations include:

- **`description`** – Human-readable explanation of the parameter
- **`enum`** – Restricts values to a specific list
- **`min_length`/`max_length`** – String length boundaries (rendered as `minLength`/`maxLength` in JSON Schema)
- **`pattern`** – Regular expression validation for string formats
- **`ge`/`le`/`gt`/`lt`** – Numeric range constraints (greater/less than or equal)
- **`default`** – Static default value injected into the schema

These constraints map directly to JSON Schema specifications, ensuring compatibility with OpenAI's function-calling API and other LLM tool interfaces.

## Summary

- **`needle.Field`** stores validation metadata in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) and applies it via the `apply` method (lines 35-48)
- **`_field_of`** (lines 84-91) extracts `Field` instances from `typing.Annotated` metadata during schema generation
- **Required detection** automatically occurs based on the absence of default values and non-optional types
- **JSON Schema output** includes all constraints (descriptions, enums, ranges) without manual dictionary construction
- The **`@tool`** decorator in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) orchestrates the entire inspection and registration process

## Frequently Asked Questions

### What is the difference between using Field metadata and Python default values?

**`Field` metadata defines schema constraints and descriptions for the LLM, while Python default values define runtime behavior.** You can use both simultaneously: place constraints inside `Field(...)` and runtime defaults after the type annotation. If you specify a default within the `Field` object itself, it populates the JSON Schema but does not affect the Python function signature.

### Can I use needle.Field without typing.Annotated?

**No, `needle.Field` requires `typing.Annotated` to attach metadata to type hints without altering the runtime type.** The `_field_of` helper specifically searches the `__metadata__` tuple of `Annotated` types. Using `Field` directly as a default value (e.g., `param: str = Field(...)`) would conflict with Python's runtime expectations for string parameters.

### How does needle handle Optional types with Field constraints?

**Optional types (e.g., `Optional[str]` or `str | None`) are automatically detected and excluded from the `required` array in the generated schema.** The `build_schema` function checks for `None` compatibility in the type hint. Even if you attach a `Field` with constraints to an Optional parameter, it remains optional in the JSON Schema unless you explicitly provide a non-None default value in the function signature.

### Where is the generated tool schema stored after decoration?

**The schema is stored in the `_needle_tool` attribute of the decorated function.** After applying `@tool`, you can inspect the complete OpenAI-compatible definition via `your_function._needle_tool`. This dictionary contains the function name, description, and parameters object suitable for passing directly to LLM API calls.