Guide for implementing services with Protocol-based dependency injection in Python. Use when creating services that interact with external systems (APIs, databases, filesystems). Enables clean testing through mock injection while keeping production code simple.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: python-service-with-protocol description: | Guide for implementing services with Protocol-based dependency injection in Python. Use when creating services that interact with external systems (APIs, databases, filesystems). Enables clean testing through mock injection while keeping production code simple. version: 1.0.0 tags:
- python
- testing
- dependency-injection
- architecture
- protocols
Python Service with Protocol Dependency Injection
Overview
This skill provides guidance for implementing Python services using the Protocol pattern for dependency injection. This pattern enables:
- Testability: Easy injection of mock implementations for unit testing
- Flexibility: Swap implementations without changing service code
- Type Safety: Full type checking support via
typing.Protocol - Clean Architecture: Clear separation between interfaces and implementations
When to Use This Pattern
Use this pattern when:
- External Dependencies: Your service calls external APIs, CLIs, or systems
- Unit Testing Required: You need to test business logic in isolation
- Multiple Implementations: Different backends (file vs memory, API vs mock)
- Side Effects: Operations that shouldn't run during tests (network calls, file I/O)
Architecture Pattern
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MyService β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β def __init__( β β
β β self, β β
β β client: Optional[ClientProtocol] = None β β
β β ): β β
β β self.client = client or DefaultClient() β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βββββββββββββ΄ββββββββββββ
β β
βββββββββΌββββββββ βββββββββΌββββββββ
β DefaultClient β β MockClient β
β (production) β β (testing) β
βββββββββββββββββ βββββββββββββββββ
Step 1: Define the Protocol
Create a Protocol that defines the contract for your external dependency:
from typing import Optional, Protocol
class GitHubClient(Protocol):
"""Protocol for GitHub API operations.
Allows for dependency injection and testing.
"""
def check_auth(self) -> bool:
"""Check if authenticated with GitHub."""
...
def check_repo_access(self, repo: str) -> bool:
"""Check if user has access to a repository."""
...
def fetch_file(self, repo: str, path: str) -> Optional[str]:
"""Fetch a file's content from a repository."""
...
Protocol Best Practices
- Use
...(Ellipsis): Protocol methods use...notpass - Add Docstrings: Document what each method should do
- Keep Focused: One protocol per external system
- Return Types: Always specify return types
Step 2: Create the Default Implementation
Implement the protocol for production use:
import subprocess
from typing import Optional
class DefaultGitHubClient:
"""Default GitHub client using gh CLI."""
def check_auth(self) -> bool:
"""Check if GitHub CLI is authenticated."""
try:
result = subprocess.run(
["gh", "auth", "status"],
capture_output=True,
text=True,
)
return result.returncode == 0
except FileNotFoundError:
return False
def check_repo_access(self, repo: str) -> bool:
"""Check if user has access to the repository."""
result = subprocess.run(
["gh", "api", f"/repos/{repo}", "--silent"],
capture_output=True,
)
return result.returncode == 0
def fetch_file(self, repo: str, path: str) -> Optional[str]:
"""Fetch a file's content from a repository."""
try:
result = subprocess.run(
[
"gh", "api",
f"/repos/{repo}/contents/{path}",
"-H", "Accept: application/vnd.github.raw+json",
],
capture_output=True,
text=True,
check=True,
)
return result.stdout
except subprocess.CalledProcessError:
return None
Step 3: Create the Service with Optional DI
Design your service to accept an optional protocol implementation:
from typing import List, Optional
from context_harness.primitives import (
ErrorCode,
Failure,
Result,
Skill,
Success,
)
class SkillService:
"""Service for managing skills.
Example:
# Production usage (default client)
service = SkillService()
# Testing usage (mock client)
mock_client = MockGitHubClient(authenticated=False)
service = SkillService(github_client=mock_client)
"""
def __init__(
self,
github_client: Optional[GitHubClient] = None,
skills_repo: str = "org/skills-repo",
):
"""Initialize the skill service.
Args:
github_client: GitHub client for API operations
skills_repo: Skills repository (owner/repo format)
"""
self.github = github_client or DefaultGitHubClient()
self.skills_repo = skills_repo
def list_remote(self) -> Result[List[Skill]]:
"""List available skills from remote registry."""
# Use self.github - works with either real or mock client
if not self.github.check_auth():
return Failure(
error="GitHub CLI is not authenticated",
code=ErrorCode.AUTH_REQUIRED,
)
# ... rest of implementation
Service Pattern Best Practices
- Type Hint Protocol:
github_client: Optional[GitHubClient] - Default to Real:
self.github = github_client or DefaultGitHubClient() - Store as Attribute: Assign to
self.githubfor use in methods - Document Both Uses: Show production and testing usage in docstring
Step 4: Create Mock for Testing
Create a mock implementation with controllable behavior:
from typing import Optional
class MockGitHubClient:
"""Mock GitHub client for testing."""
def __init__(
self,
authenticated: bool = True,
has_repo_access: bool = True,
registry_content: Optional[str] = None,
username: str = "test-user",
):
"""Initialize mock with controllable behavior.
Args:
authenticated: Whether check_auth() returns True
has_repo_access: Whether check_repo_access() returns True
registry_content: Content to return from fetch_file
username: Username to return from get_username
"""
self._authenticated = authenticated
self._has_repo_access = has_repo_access
self._registry_content = registry_content
self._username = username
self._files: dict[str, str] = {}
def check_auth(self) -> bool:
return self._authenticated
def check_repo_access(self, repo: str) -> bool:
return self._has_repo_access
def fetch_file(self, repo: str, path: str) -> Optional[str]:
if path == "skills.json":
return self._registry_content
return self._files.get(path)
Mock Best Practices
- Constructor Controls: Set behavior via
__init__parameters - Sensible Defaults: Default to "happy path" (authenticated, access granted)
- Stateful Mocks: Use attributes like
_filesfor complex scenarios - Clear Names: Parameter names should explain what they control
Step 5: Write Tests with Mocks
import pytest
from context_harness.primitives import ErrorCode, Failure, Success
from context_harness.services.skill_service import SkillService
class TestSkillServiceListRemote:
"""Tests for SkillService.list_remote()."""
def test_list_remote_returns_skills(self) -> None:
"""Test listing remote skills returns skills from registry."""
registry = '{"skills": [{"name": "skill-a"}]}'
client = MockGitHubClient(registry_content=registry)
service = SkillService(github_client=client)
result = service.list_remote()
assert isinstance(result, Success)
assert len(result.value) == 1
assert result.value[0].name == "skill-a"
def test_list_remote_not_authenticated(self) -> None:
"""Test list_remote fails when not authenticated."""
client = MockGitHubClient(authenticated=False)
service = SkillService(github_client=client)
result = service.list_remote()
assert isinstance(result, Failure)
assert result.code == ErrorCode.AUTH_REQUIRED
Common Patterns in This Codebase
| Service | Protocol | Default | Mock/Memory |
|---|---|---|---|
| SkillService | GitHubClient | DefaultGitHubClient | MockGitHubClient |
| OAuthService | TokenStorageProtocol | FileTokenStorage | MemoryTokenStorage |
| (various) | StorageProtocol | FileStorage | MemoryStorage |
Anti-Patterns to Avoid
β Don't Hard-Code Dependencies
# BAD: Can't test without calling GitHub
class SkillService:
def __init__(self):
self.github = DefaultGitHubClient() # No way to override!
β Don't Require Protocol in Tests
# BAD: Test depends on actual protocol
def test_list_remote():
service = SkillService() # Uses real GitHub client!
result = service.list_remote()
Files in This Pattern
- Protocols:
src/context_harness/storage/protocol.py - Services:
src/context_harness/services/skill_service.py- GitHubClient Protocol exampleoauth_service.py- TokenStorageProtocol example
- Storage:
src/context_harness/storage/file_storage.py- Production implementationmemory_storage.py- Test implementation
- Tests:
tests/unit/services/test_skill_service.py- MockGitHubClient exampletest_oauth_service.py- MemoryTokenStorage example
Skill: python-service-with-protocol v1.0.0 | Last updated: 2025-12-30
More by co-labs-co
View allEnforces conventional commit format for PR titles and commit messages, automating semantic versioning and GitHub releases. Use this skill when writing commit messages, creating PR titles, understanding version bumps, configuring release automation, or troubleshooting commitlint failures.
Configure and manage MCP (Model Context Protocol) servers for AI agent tooling. Use when adding MCP servers, configuring authentication (OAuth 2.1 or API keys), managing opencode.json, implementing token flows, or troubleshooting MCP connections. Covers registry patterns, PKCE authentication, and the Result-based service architecture.
Implement OAuth 2.1 with PKCE for secure authentication flows in CLI applications. This skill provides comprehensive guidance for implementing browser-based OAuth flows with local callback servers, token management with automatic refresh, and secure credential storage. Use this skill when adding new OAuth providers, implementing authentication commands, handling token expiration, or debugging OAuth-related issues.
Patterns for building Click CLI applications with Rich output formatting, shell completion, and interactive pickers. Use when creating new CLI commands, adding shell completion, implementing interactive selection, or testing CLI functionality. Covers command groups, formatters, exit codes, and CliRunner testing.
