# Input Validation Conventions

## Overview

This SDK uses minimal input validation that catches obvious user errors before making network requests, while trusting the server as the authoritative validator for business logic constraints.

## Validation Philosophy

### What to Validate

**Do validate:**
- Required parameters are not None/empty
- Basic type correctness (list vs string, etc.)
- Obvious bounds that prevent network round-trips (e.g., `top_n > 0`)
- Simple range checks using input context (e.g., `top_n <= len(documents)`)

**Don't validate:**
- Model names, dimension ranges, format patterns (server decides)
- Complex business logic constraints
- Exhaustive input checking
- Server-side validation logic

### Rationale

This approach:
- Catches obvious mistakes early with better error messages
- Avoids duplicating server logic that changes over time
- Prevents validation drift between SDK and API
- Keeps SDK lightweight and maintainable
- Saves network round-trips for clear client errors

## Validation Utilities

The SDK provides validation utilities in `pinecone/_internal/validation.py`:

### `require_non_empty(name, value)`

Validates that a value is not None, empty string, or empty list.

```python
from pinecone._internal.validation import require_non_empty

# Validate required string parameter
require_non_empty("model", model)  # Raises if None or ""

# Validate required list parameter
require_non_empty("texts", texts)  # Raises if None or []
```

**Error messages:**
- `"{name} is required"` - when value is None
- `"{name} must be a non-empty string"` - when value is empty string
- `"{name} must be a non-empty list"` - when value is empty list

### `require_positive(name, value, *, allow_none=True)`

Validates that a numeric value is positive (> 0).

```python
from pinecone._internal.validation import require_positive

# Validate optional positive parameter
require_positive("timeout", timeout)  # OK if None (default)

# Validate required positive parameter
require_positive("count", count, allow_none=False)  # Raises if None
```

**Error messages:**
- `"{name} must be positive, got {value}"` - when value <= 0
- `"{name} is required"` - when value is None and `allow_none=False`

### `require_in_range(name, value, min_val, max_val)`

Validates that a value is within a specified range [min, max]. None is always allowed (for optional parameters).

```python
from pinecone._internal.validation import require_in_range

# Validate range with context from inputs
documents = ["Doc 1", "Doc 2", "Doc 3"]
if top_n is not None:
    require_in_range("top_n", top_n, 1, len(documents))
```

**Error message:**
- `"{name} must be between {min_val} and {max_val}, got {value}"`

## Usage Patterns

### Basic Parameter Validation

```python
def embed(
    self,
    model: str,
    *,
    texts: list[str],
    parameters: dict[str, Any] | None = None,
) -> EmbedResponse:
    # Validate required parameters
    require_non_empty("model", model)
    require_non_empty("texts", texts)
    
    # That's it - server validates the rest
    ...
```

### Range Validation with Context

```python
def rerank(
    self,
    model: str,
    *,
    query: str,
    documents: list[str],
    top_n: int | None = None,
) -> RerankResponse:
    # Validate required parameters
    require_non_empty("model", model)
    require_non_empty("query", query)
    require_non_empty("documents", documents)
    
    # Validate optional range using input context
    if top_n is not None:
        require_in_range("top_n", top_n, 1, len(documents))
    
    ...
```

### Optional Numeric Parameters

```python
def query(
    self,
    *,
    vector: list[float],
    top_k: int = 10,
    timeout: float | None = None,
) -> QueryResponse:
    # Validate required parameters
    require_non_empty("vector", vector)
    require_positive("top_k", top_k, allow_none=False)  # Required, must be > 0
    require_positive("timeout", timeout)  # Optional, OK if None
    
    ...
```

## Validation in Different Layers

### Public API Layer

Use validation utilities for all user-facing methods:

```python
# pinecone/inference.py
from pinecone._internal.validation import require_non_empty

def embed(self, model: str, *, texts: list[str]) -> EmbedResponse:
    require_non_empty("model", model)
    require_non_empty("texts", texts)
    ...
```

### Internal/Private Methods

Skip validation in internal methods - assume inputs are already validated:

```python
# pinecone/_internal/http/client.py
def _make_request(self, endpoint: str, data: dict) -> dict:
    # No validation - internal method, inputs trusted
    ...
```

### Configuration Objects

Configuration validation can be more strict since it's not called frequently:

```python
# pinecone/_internal/http/config.py
def __post_init__(self):
    # More exhaustive validation for config objects
    if self.timeout <= 0:
        raise ValueError(f"timeout must be positive, got {self.timeout}")
    if self.max_retries < 0:
        raise ValueError(f"max_retries must be non-negative, got {self.max_retries}")
```

## Error Messages

### Guidelines

- Use consistent format: `"{parameter} must be {constraint}, got {value}"`
- For None checks: `"{parameter} is required"`
- Include parameter name in every message
- Be specific: "non-empty string" not just "non-empty"
- Include actual value when helpful (but not for sensitive data)

### Examples

**Good error messages:**
- `"model is required"`
- `"texts must be a non-empty list"`
- `"top_n must be between 1 and 10, got 15"`
- `"timeout must be positive, got -5.0"`

**Bad error messages:**
- `"Invalid input"` (too vague)
- `"Error: model"` (unclear what's wrong)
- `"Parameter must be specified"` (which parameter?)
- `"The value you provided for top_n (15) exceeds the maximum..."` (too verbose)

## Testing Validation

### Test Each Utility Function

```python
def test_require_non_empty_with_valid_string():
    require_non_empty("model", "multilingual-e5-large")  # Should not raise

def test_require_non_empty_with_empty_string():
    with pytest.raises(ValueError, match="model must be a non-empty string"):
        require_non_empty("model", "")
```

### Test Validation in Methods

```python
def test_embed_missing_model_raises_error(inference):
    with pytest.raises(ValueError) as exc_info:
        inference.embed(model="", texts=["Hello"])
    
    assert "model" in str(exc_info.value)
    assert "non-empty" in str(exc_info.value)
```

### Use Flexible Assertions

Check for key words, not exact error messages:

```python
# ✅ Good: Flexible, resilient to message changes
assert "model" in str(exc_info.value)
assert "non-empty" in str(exc_info.value)

# ❌ Bad: Brittle, breaks on minor message changes
assert str(exc_info.value) == "model must be a non-empty string"
```

## Anti-Patterns

### ❌ Duplicating Server Logic

```python
# Bad: Validating model names (server's job)
VALID_MODELS = ["multilingual-e5-large", "bge-reranker-v2-m3"]
if model not in VALID_MODELS:
    raise ValueError(f"Invalid model: {model}")
```

**Why bad:** Model list changes on server, SDK gets out of sync.

### ❌ Complex Business Rules

```python
# Bad: Complex dimension validation (server's job)
if model == "multilingual-e5-large":
    if dimension not in [256, 512, 1024]:
        raise ValueError("This model only supports dimensions: 256, 512, 1024")
```

**Why bad:** Model capabilities change, SDK would need constant updates.

### ❌ Excessive Type Checking

```python
# Bad: Over-validating types (mypy handles this)
if not isinstance(texts, list):
    raise TypeError("texts must be a list")
if not all(isinstance(t, str) for t in texts):
    raise TypeError("texts must be a list of strings")
```

**Why bad:** Type hints + mypy catch these at development time, no runtime check needed.

### ❌ Inline Validation

```python
# Bad: Inline validation (not reusable, inconsistent messages)
if not model:
    raise ValueError("model must be specified")
if not texts:
    raise ValueError("texts is required")
```

**Why bad:** Inconsistent error messages, duplicated logic, harder to maintain.

**Better:** Use validation utilities for consistency.

## Common Validation Scenarios

### Required String Parameter

```python
require_non_empty("model", model)
```

### Required List Parameter

```python
require_non_empty("documents", documents)
```

### Optional Positive Number

```python
# None is OK (uses default), but if specified must be positive
require_positive("timeout", timeout)
```

### Required Positive Number

```python
require_positive("dimension", dimension, allow_none=False)
```

### Bounded Range Using Input Context

```python
# top_n must be between 1 and the number of documents
if top_n is not None:
    require_in_range("top_n", top_n, 1, len(documents))
```

### Fixed Range

```python
# Unlikely pattern - usually let server decide valid ranges
# But OK for obvious cases like percentiles
if percentile is not None:
    require_in_range("percentile", percentile, 0, 100)
```

## References

- Validation utilities: `pinecone/_internal/validation.py`
- Unit tests: `tests/unit/test_validation.py`
- Usage examples: `pinecone/inference.py`, `pinecone/async_inference.py`
- Error handling conventions: [errors.md](./errors.md)
