# API Stability Principles

## Overview

API stability is critical for production use. This document defines 11 core principles that ensure the SDK's public interface evolves without breaking user code.

## The 11 Principles

### 1. Never Remove Public Methods

**Principle**: Once published, public methods stay forever (or until MAJOR version).

**Example**:

```python
# ✅ Good: Deprecate, don't remove
class Client:
    def list_indexes(self) -> list[str]:
        """
        .. deprecated:: 3.1.0
            Use :meth:`list_index_names` instead.
        """
        warnings.warn("Use list_index_names", DeprecationWarning)
        return self.list_index_names()

    def list_index_names(self) -> list[str]:
        """List all index names."""
        ...

# ❌ Bad: Removing without deprecation
class Client:
    # list_indexes() just deleted!
    def list_index_names(self) -> list[str]:
        ...
```

**Rationale**: Users depend on public methods. Removing them breaks production code.

---

### 2. Never Change Method Signatures

**Principle**: Don't change existing parameters or return types (without MAJOR version).

**Example**:

```python
# ✅ Good: Add optional parameters
# v3.1
def create_index(name: str, dimension: int) -> Index:
    ...

# v3.2 (backward compatible)
def create_index(
    name: str,
    dimension: int,
    metric: str = "cosine"  # New optional parameter
) -> Index:
    ...

# ❌ Bad: Changing required parameters
# v3.1
def create_index(name: str, dimension: int) -> Index:
    ...

# v3.2 (breaks existing code!)
def create_index(config: IndexConfig) -> Index:  # Breaking change
    ...
```

**Rationale**: Changing signatures breaks all call sites.

---

### 3. Only Add, Never Remove Configuration

**Principle**: Configuration options stay forever (or until MAJOR version).

**Example**:

```python
# ✅ Good: Add new config options
class HTTPConfig(Struct):
    timeout: int = 30
    max_retries: int = 3
    # v3.2: New option added
    max_connections: int = 100

# ❌ Bad: Removing config options
class HTTPConfig(Struct):
    timeout: int = 30
    # max_retries removed! (breaks user code)
    max_connections: int = 100
```

**Rationale**: Users configure clients. Removing options breaks their configurations.

---

### 4. Return Types Never Become More Specific

**Principle**: Return types can become more general, never more specific.

**Example**:

```python
# ✅ Good: More general return type
# v3.1
def get_data() -> dict[str, str]:  # Specific
    ...

# v3.2
def get_data() -> dict[str, Any]:  # More general (backward compatible)
    ...

# ❌ Bad: More specific return type
# v3.1
def get_data() -> dict[str, Any]:  # General
    ...

# v3.2
def get_data() -> dict[str, str]:  # More specific (breaking change)
    ...
```

**Rationale**: More specific types reject previously valid code.

---

### 5. Exceptions Stay in Hierarchy

**Principle**: Don't change exception hierarchy or add new exceptions to existing methods (without MAJOR version).

**Example**:

```python
# ✅ Good: Documented exceptions stay same
def create_index(name: str) -> Index:
    """
    Raises:
        InvalidAPIKeyError: If API key invalid
        BadRequestError: If parameters invalid
        ServerError: If server error
    """
    # Same exceptions as v3.0
    ...

# ❌ Bad: New exception type
def create_index(name: str) -> Index:
    """
    Raises:
        InvalidAPIKeyError: If API key invalid
        BadRequestError: If parameters invalid
        ServerError: If server error
        NewError: New exception (breaks error handling!)
    """
    ...
```

**Rationale**: Users catch specific exceptions. New exceptions break error handling.

---

### 6. Defaults Never Change Behavior

**Principle**: Default parameter values never change behavior.

**Example**:

```python
# ✅ Good: New parameter with explicit default
# v3.1
def query(vector: list[float]) -> list[Match]:
    ...

# v3.2 (backward compatible)
def query(
    vector: list[float],
    top_k: int = 10  # Explicit default
) -> list[Match]:
    ...

# ❌ Bad: Changing default behavior
# v3.1
def query(vector: list[float], top_k: int = 10) -> list[Match]:
    ...

# v3.2 (changes behavior!)
def query(vector: list[float], top_k: int = 100) -> list[Match]:  # Breaking
    ...
```

**Rationale**: Changing defaults changes behavior of existing code.

---

### 7. Properties Are Read-Only by Default

**Principle**: Properties should be read-only unless explicitly mutable.

**Example**:

```python
# ✅ Good: Read-only property
class Index:
    @property
    def name(self) -> str:
        """Index name (read-only)."""
        return self._name

# ❌ Bad: Making property writable later
class Index:
    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str) -> None:  # Don't add setters later
        self._name = value
```

**Rationale**: Adding setters changes expectations about mutability.

---

### 8. Namespace Boundaries Are Stable

**Principle**: Don't move methods between namespaces.

**Example**:

```python
# ✅ Good: Stable namespace
class Client:
    @property
    def indexes(self) -> IndexManager:
        """Index management namespace."""
        ...

# Usage stays same across versions
pc.indexes.list()
pc.indexes.create("my-index")

# ❌ Bad: Moving methods
# v3.1
pc.indexes.list()

# v3.2 (moved method!)
pc.list_indexes()  # Breaking change
```

**Rationale**: Users learn where methods live. Moving them breaks muscle memory and code.

---

### 9. Synchronous and Async Stay Parallel

**Principle**: Sync and async APIs must have identical interfaces.

**Example**:

```python
# ✅ Good: Parallel APIs
class Client:
    def list_indexes(self) -> list[str]:
        ...

class AsyncClient:
    async def list_indexes(self) -> list[str]:  # Same signature
        ...

# ❌ Bad: Different interfaces
class Client:
    def list_indexes(self) -> list[str]:
        ...

class AsyncClient:
    async def list_all_indexes(self) -> list[Index]:  # Different name/type
        ...
```

**Rationale**: Users expect to switch between sync/async easily.

---

### 10. Internal Changes Don't Affect Public API

**Principle**: Refactor internals freely without changing public behavior.

**Example**:

```python
# ✅ Good: Internal refactoring
# v3.1
class Client:
    def list_indexes(self) -> list[str]:
        return self._http.get("/indexes").json()

# v3.2 (internal change, same behavior)
class Client:
    def list_indexes(self) -> list[str]:
        # Refactored internals, same result
        return self._fetch_indexes()

    def _fetch_indexes(self) -> list[str]:
        return self._http.get("/indexes").json()

# ❌ Bad: Internal changes leak to public
# v3.1
def list_indexes(self) -> list[str]:
    return self._http.get("/indexes").json()

# v3.2 (user needs to change code!)
def list_indexes(self, http_client: HTTPClient) -> list[str]:  # Leaky abstraction
    return http_client.get("/indexes").json()
```

**Rationale**: Users shouldn't care about internal details.

---

### 11. Error Messages Are Not Part of API

**Principle**: Error message text can change, exception types cannot.

**Example**:

```python
# ✅ Good: Improve error messages
# v3.1
raise NotFoundError("Not found")

# v3.2 (better message, same exception type)
raise NotFoundError(
    f"Index '{name}' not found. "
    f"Use pc.list_indexes() to see available indexes."
)

# ❌ Bad: Changing exception type
# v3.1
raise NotFoundError("Not found")

# v3.2 (breaks error handling!)
raise IndexNotFoundError("Not found")  # New exception type
```

**Rationale**: Users catch exception types, not message content.

---

### 12. Type Hints Support API Evolution

**Principle**: Choose type hints that allow the API to evolve without SDK updates.

**Example**:

```python
# ✅ Good: Literal | str for API-controlled values
InputType = Literal["passage", "query"] | str

class EmbedParameters(TypedDict, total=False):
    input_type: InputType  # Autocompletes known values, accepts new ones

def embed(
    model: str,
    texts: list[str],
    parameters: EmbedParameters | dict[str, Any] | None = None
) -> EmbedResponse:
    ...

# When API adds "document" input type:
embed(model="...", texts=["..."], parameters={"input_type": "document"})  # ✅ Works immediately

# ❌ Bad: Enum restricts to known values
from enum import Enum

class InputType(Enum):
    PASSAGE = "passage"
    QUERY = "query"

def embed(
    model: str,
    texts: list[str],
    input_type: InputType = InputType.PASSAGE
) -> EmbedResponse:
    ...

# When API adds "document" input type:
embed(model="...", texts=["..."], input_type="document")  # ❌ Type error until SDK update
```

**Rationale**: 
- **Enum**: Creates coupling between API and SDK versions. Users can't use new API features until SDK release.
- **Literal | str**: Provides IDE autocomplete for known values while accepting new values immediately.
- **dict[str, Any]**: Maximum flexibility but loses discoverability.

**Guidelines**:
- **API-controlled values** (model parameters, modes): Use `Literal | str`
- **SDK-controlled values** (SDK behavior flags): Use `Enum`  
- **Stable closed sets** (HTTP methods): Use pure `Literal`
- **Parameter objects**: Use `TypedDict` with `total=False` for optional fields

See [types.md](./types.md#enum-vs-literal-why-literal-wins-for-api-parameters) for detailed comparison.

---

## Stability Levels

### Public API (Stable)

**Guaranteed stable**:
- Classes exported in `__all__`
- Public methods (no `_` prefix)
- Public properties
- Exception types
- Type hints

```python
# pinecone/__init__.py
__all__ = [
    "Client",         # Stable
    "AsyncClient",    # Stable
    "Index",          # Stable
    "PineconeError",  # Stable
]
```

### Internal API (Unstable)

**Can change freely**:
- Modules in `_internal/`
- Private methods (`_method`)
- Implementation details
- Internal classes

```python
# pinecone/_internal/http/client.py
class HTTPClient:  # Internal, can change
    def _make_request(self): ...  # Private, can change
```

### Experimental API (Unstable)

**Marked as experimental**:
- New features in beta
- Opt-in functionality
- Clearly marked in docs

```python
def experimental_feature():
    """
    .. warning::
        This is an experimental feature and may change in future versions.
    """
    ...
```

## Versioning Strategy

### When to Increment MAJOR

**Breaking changes requiring MAJOR version**:
- Removed public methods/properties
- Changed method signatures
- Changed return types
- Renamed public classes
- Removed configuration options
- Changed exception hierarchy
- Moved methods between namespaces

### When to Increment MINOR

**Backward-compatible changes**:
- Added new methods/properties
- Added optional parameters (with defaults)
- Added new classes/modules
- Added new exceptions to new methods
- Improved error messages
- Performance improvements

### When to Increment PATCH

**Bug fixes only**:
- Fixed incorrect behavior
- Fixed memory leaks
- Fixed race conditions
- Documentation updates

## Testing Stability

### Stability Tests

**Test that APIs don't break**:

```python
def test_client_signature_stable():
    """Client.list_indexes signature must not change."""
    import inspect

    sig = inspect.signature(Client.list_indexes)

    # Check parameters
    assert len(sig.parameters) == 1  # Just self

    # Check return type
    assert sig.return_annotation == list[str]

def test_exception_hierarchy_stable():
    """Exception hierarchy must not change."""
    from pinecone import PineconeError, APIError, NotFoundError

    # Check hierarchy
    assert issubclass(APIError, PineconeError)
    assert issubclass(NotFoundError, APIError)
```

### Compatibility Tests

**Test backward compatibility**:

```python
def test_backward_compatible_with_v3_1():
    """Code written for v3.1 must work in v3.2."""
    # This is v3.1 code
    pc = Pinecone(api_key="test")
    indexes = pc.list_indexes()

    # Must still work in v3.2
    assert isinstance(indexes, list)
    assert all(isinstance(name, str) for name in indexes)
```

## Documentation

### Stability Guarantees

**Document stability in README**:

```markdown
## Stability Guarantees

The Pinecone Python SDK follows semantic versioning:

- **MAJOR**: Breaking changes (v3.x → v4.0)
- **MINOR**: New features, backward compatible (v3.1 → v3.2)
- **PATCH**: Bug fixes only (v3.2.0 → v3.2.1)

### What's Stable

✅ Public methods and signatures
✅ Return types
✅ Exception types
✅ Configuration options

### What Can Change

⚠️ Internal implementation
⚠️ Error message text
⚠️ Performance characteristics
⚠️ Dependency versions
```

### Migration Guides

**Provide guides for MAJOR versions**:

```markdown
## Migrating from 2.x to 3.0

### Breaking Changes

1. **list_indexes() return type**
   ```python
   # Before (2.x)
   names: list[str] = pc.list_indexes()

   # After (3.x)
   indexes: list[Index] = pc.list_indexes()
   names = [idx.name for idx in indexes]
   ```

2. **Removed old_method()**
   Use `new_method()` instead.
```

## Anti-Patterns

### ❌ Silently Changing Behavior

```python
# Bad: Same method, different behavior
# v3.1
def list_indexes() -> list[str]:
    return sorted(indexes)  # Sorted

# v3.2 (silent change!)
def list_indexes() -> list[str]:
    return indexes  # No longer sorted
```

### ❌ Breaking Changes in MINOR

```python
# Bad: Breaking change in MINOR version
# v3.1
def get_data() -> dict:
    ...

# v3.2 (should be 4.0!)
def get_data() -> list:  # Breaking change
    ...
```

### ❌ No Deprecation Period

```python
# Bad: Removing without warning
# v3.1
def old_method(): ...

# v3.2
# old_method removed immediately!
```

## Stability Checklist

Before releasing:

- [ ] No removed public methods
- [ ] No changed method signatures
- [ ] No removed configuration options
- [ ] Return types same or more general
- [ ] Exception hierarchy unchanged
- [ ] Defaults don't change behavior
- [ ] Properties remain read-only
- [ ] Namespaces unchanged
- [ ] Sync/async APIs parallel
- [ ] Deprecation warnings for removals
- [ ] Migration guide for breaking changes
- [ ] Stability tests passing
- [ ] Compatibility tests passing

## References

- See [versioning.md](./versioning.md) for version numbering
- See [errors.md](./errors.md) for exception hierarchy stability
- See [architecture.md](./architecture.md) for public vs internal APIs
- See [testing.md](./testing.md) for stability testing
