# Usability Principles

## Overview

The SDK should be delightful to use for both humans and AI agents. This document defines 12 core usability principles that make the SDK intuitive and easy to learn.

## The 12 Principles

### 1. Explicit Over Implicit

**Principle**: Make behavior obvious from the code.

**Example**:

```python
# ✅ Good: Explicit parameters
pc.create_index(
    name="my-index",
    dimension=1536,
    metric="cosine"
)

# ❌ Bad: Implicit defaults
pc.create_index("my-index", 1536)  # What metric? Unknown!
```

**Rationale**: Explicit code is self-documenting. No need to check docs.

---

### 2. Consistent Naming Everywhere

**Principle**: Use same words for same concepts.

**Example**:

```python
# ✅ Good: Consistent verbs
pc.create_index(...)
pc.create_namespace(...)
pc.create_collection(...)

# ❌ Bad: Inconsistent verbs
pc.create_index(...)
pc.make_namespace(...)
pc.build_collection(...)
```

**Rationale**: Consistency reduces cognitive load.

---

### 3. Fail Fast with Clear Errors

**Principle**: Detect errors early, explain them clearly.

**Example**:

```python
# ✅ Good: Validate immediately, clear message
def create_index(name: str, dimension: int):
    if not name:
        raise InvalidConfigError(
            "name",
            name,
            "cannot be empty. Provide a non-empty index name."
        )
    if dimension <= 0:
        raise InvalidConfigError(
            "dimension",
            dimension,
            "must be positive. Provide dimension > 0."
        )
    ...

# ❌ Bad: Vague error late in process
def create_index(name: str, dimension: int):
    # ... lots of code ...
    response = http.post("/indexes", json={"name": name, "dimension": dimension})
    # Fails with generic "Bad Request"
```

**Rationale**: Early, clear errors save debugging time.

---

### 4. Sensible Defaults

**Principle**: Defaults should work for 80% of users.

**Example**:

```python
# ✅ Good: Sensible defaults
def create_index(
    name: str,
    dimension: int,
    metric: str = "cosine",      # Most common
    replicas: int = 1,           # Start small
    pods: int = 1                # Start small
):
    ...

# Usage: Most users only specify required params
pc.create_index(name="my-index", dimension=1536)

# ❌ Bad: Everything required
def create_index(
    name: str,
    dimension: int,
    metric: str,                 # Force user to choose
    replicas: int,               # No default
    pods: int,                   # No default
    environment: str,            # No default
    pod_type: str               # No default
):
    ...
```

**Rationale**: Good defaults reduce API surface for beginners.

---

### 5. Type Hints Everywhere

**Principle**: Every function has complete type hints.

**Example**:

```python
# ✅ Good: Complete type hints
def create_index(
    name: str,
    dimension: int,
    metric: str = "cosine"
) -> Index:
    ...

# ❌ Bad: No type hints
def create_index(name, dimension, metric="cosine"):
    ...
```

**Rationale**: Type hints enable IDE autocomplete and catch errors early.

---

### 6. Progressive Disclosure

**Principle**: Simple things simple, complex things possible.

**Example**:

```python
# ✅ Good: Simple case is simple
# Beginner: Just get started
pc = Pinecone(api_key="...")
pc.create_index(name="my-index", dimension=1536)

# Advanced: Full control when needed
pc = Pinecone(
    api_key="...",
    environment="us-east-1",
    http_config=HTTPConfig(
        timeout=60,
        max_retries=5,
        max_connections=200
    )
)

# ❌ Bad: Everything exposed upfront
pc = Pinecone(
    api_key="...",
    environment="...",         # Required
    timeout=30,               # Required
    max_retries=3,           # Required
    max_connections=100,     # Required
    rate_limit_tokens=10,    # Required
    rate_limit_period=1.0    # Required
)
```

**Rationale**: Don't overwhelm beginners, don't limit experts.

---

### 7. Namespaces Group Related Operations

**Principle**: Organize by feature, not by technical detail.

**Example**:

```python
# ✅ Good: Feature-based namespaces
pc.indexes.list()
pc.indexes.create("my-index", 1536)
pc.indexes.delete("my-index")

pc.inference.embed(model="...", text="...")
pc.inference.rerank(model="...", query="...", documents=[...])

# ❌ Bad: Flat namespace
pc.list_indexes()
pc.create_index()
pc.delete_index()
pc.embed()
pc.rerank()
# Everything mixed together
```

**Rationale**: Namespaces provide mental model of SDK capabilities.

---

### 8. Sync and Async Are Parallel

**Principle**: Switching between sync/async should be trivial.

**Example**:

```python
# ✅ Good: Same interface
# Sync
pc = Pinecone(api_key="...")
indexes = pc.list_indexes()

# Async (just add async/await)
pc = AsyncPinecone(api_key="...")
indexes = await pc.list_indexes()

# ❌ Bad: Different interfaces
# Sync
pc = Pinecone(api_key="...")
indexes = pc.get_all_indexes()

# Async (different names!)
pc = AsyncPinecone(api_key="...")
indexes = await pc.list_indexes_async()
```

**Rationale**: Users shouldn't learn two different APIs.

---

### 9. Examples in Docstrings

**Principle**: Every public method has usage example.

**Example**:

```python
# ✅ Good: Example in docstring
def create_index(
    name: str,
    dimension: int,
    metric: str = "cosine"
) -> Index:
    """
    Create a new index.

    Args:
        name: Name of the index
        dimension: Vector dimension
        metric: Distance metric (default: "cosine")

    Returns:
        The created index

    Example:
        >>> client = Client(api_key="...")
        >>> index = client.create_index("my-index", 1536)
        >>> print(index.name)
        'my-index'

    Raises:
        InvalidAPIKeyError: If API key is invalid
        BadRequestError: If parameters are invalid
    """
    ...

# ❌ Bad: No example
def create_index(name: str, dimension: int, metric: str = "cosine") -> Index:
    """Create a new index."""
    ...
```

**Rationale**: Examples show exact usage without reading docs.

---

### 10. Chainable Where It Makes Sense

**Principle**: Allow method chaining for fluent API.

**Example**:

```python
# ✅ Good: Chainable configuration
config = (
    HTTPConfig()
    .with_timeout(60)
    .with_retries(5)
    .with_rate_limit(tokens=10, period=1.0)
)

# Or builder pattern
index_config = (
    IndexConfig()
    .with_dimension(1536)
    .with_metric("cosine")
    .with_replicas(3)
)

# ❌ Bad: Not chainable
config = HTTPConfig()
config.set_timeout(60)          # Returns None
config.set_retries(5)           # Returns None
config.set_rate_limit(10, 1.0)  # Returns None
```

**Rationale**: Chaining enables concise, readable configuration.

---

### 11. Context Managers for Resource Cleanup

**Principle**: Use `with` statement for automatic cleanup.

**Example**:

```python
# ✅ Good: Context manager
with Pinecone(api_key="...") as pc:
    indexes = pc.list_indexes()
    # Client automatically closed

# Async version
async with AsyncPinecone(api_key="...") as pc:
    indexes = await pc.list_indexes()
    # Client automatically closed

# ❌ Bad: Manual cleanup
pc = Pinecone(api_key="...")
try:
    indexes = pc.list_indexes()
finally:
    pc.close()  # Easy to forget
```

**Rationale**: Context managers prevent resource leaks.

---

### 12. One Obvious Way to Do It

**Principle**: Avoid multiple ways to accomplish the same thing.

**Example**:

```python
# ✅ Good: One clear way
pc.create_index(name="my-index", dimension=1536)

# ❌ Bad: Multiple ways
pc.create_index(name="my-index", dimension=1536)
pc.make_index(name="my-index", dimension=1536)
pc.new_index(name="my-index", dimension=1536)
pc.add_index(name="my-index", dimension=1536)
# Too many options!
```

**Rationale**: One way reduces decision fatigue and learning curve.

---

## Usability Patterns

### Quick Start

**Get started in 3 lines**:

```python
from pinecone import Pinecone

pc = Pinecone(api_key="...")
pc.create_index("my-index", 1536)
```

### Error Handling

**Errors are actionable**:

```python
try:
    pc.create_index("my-index", 1536)
except InvalidAPIKeyError:
    print("Set PINECONE_API_KEY environment variable")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after}s")
except PineconeError as e:
    print(f"Error: {e.message}")
```

### Discovery

**IDE autocomplete works**:

```python
pc = Pinecone(api_key="...")
pc.  # IDE shows: indexes, inference, etc.

pc.indexes.  # IDE shows: list, create, get, delete
pc.inference.  # IDE shows: embed, rerank
```

### Interactive Discoverability Pattern

**See comprehensive guide**: [Interactive Discovery Convention](interactive-discovery.md)

The SDK provides rich interactive discovery features for REPL and notebook environments through:

- **`__repr__()`**: Shows configuration without secrets
- **`__dir__()`**: Filters to public API only
- **`__getattr__()`**: Provides helpful error messages with available methods
- **Rich representations** (optional): `_repr_pretty_()` and `_repr_html_()` for Jupyter

#### Error Message Enhancement with `__getattr__()`

Beyond the base interactive discovery patterns, namespace classes inherit from `InteractiveIntrospectionMixin` to provide helpful error messages when users typo method names:

```python
from pinecone._internal.introspection import InteractiveIntrospectionMixin

class Inference(InteractiveIntrospectionMixin, InferenceNamespaceProtocol):
    """Inference namespace with helpful error messages."""

    def embed(self, model: str, *, texts: list[str]) -> EmbeddingResponse:
        """Generate embeddings."""
        ...
```

**REPL experience**:

```python
>>> from pinecone import Pinecone
>>> pc = Pinecone(api_key="...")
>>> dir(pc.inference)
['embed', 'list_models', 'rerank']  # Clean public API only
>>> pc.inference.embed()  # Typo!
AttributeError: Inference has no attribute 'embedd'.
Available methods: embed, list_models, rerank.
Use dir(inference) or help(inference) to explore the API.
```

The mixin provides both `__dir__()` and `__getattr__()` using dynamic introspection, ensuring the methods stay current as the API evolves without manual maintenance.

**When to use**: All user-facing namespace classes (Inference, Index, Collection, etc.)

**Mixin location**: `pinecone/_internal/introspection.py`

### Type Safety

**Type checker catches errors**:

```python
# mypy catches this
index: Index = client.list_indexes()  # Error: Returns list[str], not Index

# Correct
indexes: list[str] = client.list_indexes()
```

## Documentation Usability

### Docstring Format

**Follow Google style**:

```python
def create_index(
    name: str,
    dimension: int,
    metric: str = "cosine"
) -> Index:
    """
    Create a new index for storing vectors.

    This creates a new Pinecone index with the specified dimension
    and distance metric. The index is ready to use immediately.

    Args:
        name: Unique name for the index. Must be lowercase, alphanumeric,
            and hyphens. Example: "my-index"
        dimension: Number of dimensions for vectors. Must match your
            embedding model. Example: 1536 for OpenAI embeddings.
        metric: Distance metric for similarity. Options: "cosine",
            "euclidean", "dotproduct". Default: "cosine".

    Returns:
        Index object with name, dimension, and host information.

    Example:
        Create an index for OpenAI embeddings:

        >>> client = Client(api_key="...")
        >>> index = client.create_index(
        ...     name="my-index",
        ...     dimension=1536,
        ...     metric="cosine"
        ... )
        >>> print(index.name)
        'my-index'

    Raises:
        InvalidAPIKeyError: If API key is missing or invalid
        BadRequestError: If name/dimension/metric is invalid
        RateLimitError: If rate limit is exceeded
        ServerError: If Pinecone service error occurs

    See Also:
        - :meth:`list_indexes`: List all indexes
        - :meth:`delete_index`: Delete an index
    """
    ...
```

### README Structure

**Organize by user journey**:

```markdown
# Pinecone Python SDK

## Installation

```bash
pip install pinecone-client
```

## Quick Start

```python
from pinecone import Pinecone

pc = Pinecone(api_key="...")
pc.create_index("my-index", 1536)
```

## Core Concepts

### Indexes
...

### Vectors
...

### Namespaces
...

## Common Tasks

### Creating an index
...

### Upserting vectors
...

### Querying vectors
...

## Advanced Usage

### Async/Await
...

### Custom Configuration
...

## Troubleshooting

### Rate Limits
...

### Connection Errors
...
```

## Testing Usability

### Usability Checklist

- [ ] Can install in one command
- [ ] Can get started in 3 lines
- [ ] All public methods have type hints
- [ ] All public methods have examples
- [ ] Errors are clear and actionable
- [ ] IDE autocomplete works
- [ ] Type checker catches errors
- [ ] Sync/async APIs parallel
- [ ] Context managers work
- [ ] Documentation is complete

### User Testing

**Test with real users**:

```python
# Give users a task, observe
# Task: "Create an index and add a vector"

# Good: User completes in < 5 minutes without docs
# Bad: User needs to read docs or ask questions
```

## Anti-Patterns

### ❌ Too Many Options

```python
# Bad: Overwhelming options
def create_index(
    name: str,
    dimension: int,
    metric: str = "cosine",
    replicas: int = 1,
    pods: int = 1,
    pod_type: str = "p1.x1",
    environment: str = "us-east-1",
    metadata_config: dict | None = None,
    source_collection: str | None = None,
    timeout: int = 30,
    ...  # 20 more parameters
):
    ...
```

**Solution**: Use config objects for advanced options.

```python
# Good: Simple with progressive disclosure
def create_index(
    name: str,
    dimension: int,
    metric: str = "cosine",
    config: IndexConfig | None = None
):
    ...
```

### ❌ Unclear Parameter Names

```python
# Bad: What do these mean?
pc.query(v=[0.1, 0.2], k=10, f=True)

# Good: Clear names
pc.query(vector=[0.1, 0.2], top_k=10, include_metadata=True)
```

### ❌ Returning None for Errors

```python
# Bad: Silent failure
def get_index(name: str) -> Index | None:
    try:
        return self._fetch_index(name)
    except NotFoundError:
        return None  # User must check for None

# Good: Explicit error
def get_index(name: str) -> Index:
    return self._fetch_index(name)  # Raises NotFoundError
```

### ❌ Inconsistent Terminology

```python
# Bad: Different words for same concept
pc.create_index(...)
pc.make_vector(...)
pc.build_namespace(...)

# Good: Consistent verbs
pc.create_index(...)
pc.create_vector(...)
pc.create_namespace(...)
```

## AI Agent Friendliness

### Clear Patterns

**Make patterns obvious**:

```python
# ✅ AI can recognize: All create methods follow same pattern
pc.create_index(name="...", dimension=...)
pc.create_namespace(name="...")
pc.create_collection(name="...")

# ❌ AI struggles: Inconsistent patterns
pc.create_index(name="...", dimension=...)
pc.make_namespace(ns="...")
collection = Collection.new(title="...")
```

### Type Information

**Types guide AI**:

```python
# ✅ AI knows return type
def list_indexes() -> list[str]:
    """List all index names."""
    ...

# ❌ AI guesses
def list_indexes():
    """List all index names."""
    ...
```

### Docstring Examples

**Examples show exact usage**:

```python
# ✅ AI can copy example
def create_index(name: str, dimension: int) -> Index:
    """
    Example:
        >>> client = Client(api_key="...")
        >>> index = client.create_index("my-index", 1536)
    """
    ...

# ❌ AI must figure it out
def create_index(name: str, dimension: int) -> Index:
    """Create an index."""
    ...
```

## Usability Metrics

### Measure Success

**Track these metrics**:

- Time to first successful API call
- Number of errors before success
- Documentation page views
- Support questions per user
- API method discoverability

### User Feedback

**Collect feedback**:

```python
# Optional feedback mechanism
pc.provide_feedback(
    rating=5,
    comment="Super easy to use!"
)
```

## References

- See [naming.md](./naming.md) for consistent naming
- See [errors.md](./errors.md) for clear error messages
- See [types.md](./types.md) for type hints
- See [async.md](./async.md) for sync/async parallelism
- See [api-stability.md](./api-stability.md) for predictable APIs
