shimo4228

python-optional-dependencies

"Use when supporting multiple backends/providers without forcing all dependencies on users. pyproject.toml extras pattern."

shimo4228 1 Updated 6mo ago
GitHub

Install

npx skillscat add shimo4228/claude-code-learned-skills/python-optional-dependencies

Install via the SkillsCat registry.

About this skill

We need to produce a 2-3 sentence plain-text summary, objective, factual, no marketing language, no superlatives, no calls to action. Must be at most 60 words. No quotes, no markdown, no bullet points, no headings. Just plain text. Summarize: skill is about using optional dependencies via pyproject.toml extras pattern to support multiple backends without forcing all dependencies. Problem: forcing all dependencies leads to unnecessary installs, version conflicts, slower install.

SKILL.md

Python Optional Dependencies Pattern

Extracted: 2026-02-09
Context: Supporting multiple providers/backends without forcing all dependencies on all users

Problem

You're building a library/CLI that supports multiple backends (e.g., multiple LLM providers, multiple databases, multiple cloud providers), but you don't want to force users to install all dependencies.

Example Scenarios:

  • LLM application supporting both Anthropic and OpenAI
  • Database library supporting PostgreSQL, MySQL, SQLite
  • Cloud SDK supporting AWS, Azure, GCP
  • Analytics tool supporting multiple data sources

Naive Approach:

dependencies = [
    "anthropic>=0.40.0",  # ← Forces everyone to install
    "openai>=1.0.0",      # ← Even if they only use one
    "google-generativeai>=0.3.0",
]

Problem: Users installing 50+ MB of unused SDKs, potential version conflicts, slower installation.


Solution: Optional Dependencies

Use [project.optional-dependencies] in pyproject.toml to let users choose.

Step 1: Define Optional Dependency Groups

[project]
name = "myapp"
dependencies = [
    # Only core dependencies here
    "pydantic>=2.0",
    "typer>=0.15.0",
]

[project.optional-dependencies]
# Individual provider groups
anthropic = ["anthropic>=0.40.0"]
openai = ["openai>=1.0.0"]
google = ["google-generativeai>=0.3.0"]

# Convenience groups
all = [
    "anthropic>=0.40.0",
    "openai>=1.0.0",
    "google-generativeai>=0.3.0",
]

# Dev dependencies
dev = [
    "pytest>=8.0",
    "mypy>=1.11",
]

Step 2: Installation

# Install with specific provider
pip install myapp[anthropic]
pip install myapp[openai]

# Install with multiple providers
pip install myapp[anthropic,openai]

# Install everything
pip install myapp[all]

# Dev installation
pip install myapp[all,dev]

Implementation Pattern

Step 3: Runtime Availability Check

Create a module to check which optional dependencies are available:

# myapp/providers/__init__.py
"""Provider availability checks."""

# Check Anthropic availability
try:
    import anthropic
    ANTHROPIC_AVAILABLE = True
except ImportError:
    ANTHROPIC_AVAILABLE = False

# Check OpenAI availability
try:
    import openai
    OPENAI_AVAILABLE = True
except ImportError:
    OPENAI_AVAILABLE = False

# Check at least one is available
if not (ANTHROPIC_AVAILABLE or OPENAI_AVAILABLE):
    raise ImportError(
        "No LLM provider installed. "
        "Install at least one:\n"
        "  pip install myapp[anthropic]\n"
        "  pip install myapp[openai]\n"
        "  pip install myapp[all]"
    )

Step 4: Provider Factory with Runtime Check

# myapp/providers/factory.py
from myapp.providers import ANTHROPIC_AVAILABLE, OPENAI_AVAILABLE

def create_provider(provider_name: str):
    """Create provider instance with availability check."""

    if provider_name == "anthropic":
        if not ANTHROPIC_AVAILABLE:
            raise RuntimeError(
                "Anthropic provider not installed.\n"
                "Install with: pip install myapp[anthropic]"
            )
        from myapp.providers.anthropic import AnthropicProvider
        return AnthropicProvider()

    elif provider_name == "openai":
        if not OPENAI_AVAILABLE:
            raise RuntimeError(
                "OpenAI provider not installed.\n"
                "Install with: pip install myapp[openai]"
            )
        from myapp.providers.openai import OpenAIProvider
        return OpenAIProvider()

    else:
        raise ValueError(f"Unknown provider: {provider_name}")

Step 5: CLI Help with Availability Info

# myapp/main.py
import typer
from myapp.providers import ANTHROPIC_AVAILABLE, OPENAI_AVAILABLE

app = typer.Typer()

@app.command()
def process(
    provider: str = typer.Option(
        ...,
        help="LLM provider (anthropic/openai)"
    ),
):
    """Process with specified provider."""

    # Show available providers
    available = []
    if ANTHROPIC_AVAILABLE:
        available.append("anthropic")
    if OPENAI_AVAILABLE:
        available.append("openai")

    typer.echo(f"Available providers: {', '.join(available)}")

    if provider not in available:
        typer.secho(
            f"Provider '{provider}' not installed.\n"
            f"Install with: pip install myapp[{provider}]",
            fg=typer.colors.RED,
        )
        raise typer.Exit(1)

    # Continue with processing
    from myapp.providers.factory import create_provider
    llm = create_provider(provider)
    # ...

Testing Strategy

Test with Mocked Imports

# tests/test_providers.py
import sys
from unittest.mock import MagicMock
import pytest

def test_anthropic_provider_unavailable():
    """Test error when Anthropic SDK not installed."""

    # Mock missing anthropic module
    anthropic_module = sys.modules.get('anthropic')
    sys.modules['anthropic'] = None

    try:
        # Reload to trigger ImportError
        import importlib
        import myapp.providers
        importlib.reload(myapp.providers)

        assert not myapp.providers.ANTHROPIC_AVAILABLE

        with pytest.raises(RuntimeError, match="not installed"):
            from myapp.providers.factory import create_provider
            create_provider("anthropic")
    finally:
        # Restore original module
        if anthropic_module:
            sys.modules['anthropic'] = anthropic_module

Test Matrix with tox

# tox.ini
[tox]
envlist = py312-{anthropic,openai,all}

[testenv]
deps =
    pytest
    anthropic: anthropic>=0.40.0
    openai: openai>=1.0.0
    all: anthropic>=0.40.0
    all: openai>=1.0.0

commands =
    pytest tests/

Real-World Examples

FastAPI

pip install fastapi               # Minimal
pip install fastapi[standard]     # All recommended extras

SQLAlchemy

pip install sqlalchemy            # Core only
pip install sqlalchemy[postgresql]
pip install sqlalchemy[mysql]
pip install sqlalchemy[asyncio]

pytest

pip install pytest                # Core
pip install pytest[testing]       # Additional test tools

Requests

pip install requests              # Core
pip install requests[security]    # Security extras
pip install requests[socks]       # SOCKS proxy support

Advanced: Combining Extras

Cross-Product Groups

[project.optional-dependencies]
# Providers
anthropic = ["anthropic>=0.40.0"]
openai = ["openai>=1.0.0"]

# Features
batch = ["aiofiles>=23.0"]
ocr = ["ocrmypdf>=16.0.0"]

# Combinations
anthropic-batch = ["anthropic>=0.40.0", "aiofiles>=23.0"]
openai-batch = ["openai>=1.0.0", "aiofiles>=23.0"]

# All combinations
all = [
    "anthropic>=0.40.0",
    "openai>=1.0.0",
    "aiofiles>=23.0",
    "ocrmypdf>=16.0.0",
]

Recommendation Groups

[project.optional-dependencies]
# Minimal working setup
recommended = [
    "anthropic>=0.40.0",
    "rich>=13.0",  # Better CLI output
]

# Power user setup
complete = [
    "anthropic>=0.40.0",
    "openai>=1.0.0",
    "rich>=13.0",
    "aiofiles>=23.0",
    "ocrmypdf>=16.0.0",
]

Benefits

For Users

  • Minimal installation: Only install what they need
  • Faster installation: Fewer packages to download
  • Less disk space: No unused dependencies
  • Fewer conflicts: Smaller dependency tree
  • Clear choices: Explicit opt-in for features

For Developers

  • Flexibility: Support many backends without forcing all
  • Testing: Can test each backend independently
  • Maintenance: Can deprecate backends gradually
  • Documentation: Clear dependency requirements per feature

Gotchas

1. Circular Imports

Problem: Checking availability in __init__.py can cause circular imports.

Solution: Use separate _availability.py module:

# myapp/_availability.py
try:
    import anthropic
    ANTHROPIC_AVAILABLE = True
except ImportError:
    ANTHROPIC_AVAILABLE = False

# myapp/__init__.py
from myapp._availability import ANTHROPIC_AVAILABLE

2. Type Checking

Problem: mypy complains about imports that might not exist.

Solution: Use TYPE_CHECKING guard:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import anthropic
    import openai

def create_provider(name: str):
    if name == "anthropic":
        import anthropic  # Runtime import
        # ...

3. Documentation

Problem: Users don't know which extras to install.

Solution: Clear documentation in README:

## Installation

Choose the provider(s) you need:

### Anthropic Claude only
\`\`\`bash
pip install myapp[anthropic]
\`\`\`

### OpenAI GPT only
\`\`\`bash
pip install myapp[openai]
\`\`\`

### Both (for comparison)
\`\`\`bash
pip install myapp[all]
\`\`\`

When to Use This Pattern

Use optional dependencies when:

  1. Supporting multiple backends/providers
  2. Some dependencies are large (> 10 MB)
  3. Users typically need only 1-2 of N options
  4. Dependencies might conflict (e.g., different TensorFlow versions)

Don't use optional dependencies when:

  1. All features are commonly used together
  2. Dependencies are small (< 1 MB total)
  3. Complexity outweighs benefits
  4. Users expect "batteries included"

Related Patterns

  • ai-era-architecture-principles.md - When to minimize dependencies
  • cost-aware-llm-pipeline.md - Provider abstraction for LLM costs