Modern, enterprise-grade Python client generator for OpenAPI specifications.
Generate async-first Python clients from OpenAPI specs with complete type safety, automatic field mapping, and zero runtime dependencies.
- Async-First: All operations use
async/awaitwithhttpxfor high performance - Complete Type Safety: Full type hints, dataclass models, and mypy strict mode compatibility
- Truly Independent: Generated clients require no runtime dependency on this package
- Complex Schema Handling: Advanced cycle detection for circular references and deep nesting
- Automatic Field Mapping: Seamless conversion between API naming (snake_case, camelCase) and Python conventions
- Pluggable Authentication: Bearer tokens, API keys, OAuth2, custom auth, or combine multiple strategies
- Streaming Support: Built-in SSE and binary streaming for real-time data
- Rich IDE Support: Full autocomplete, inline docs, and type checking in modern editors
- Tag-Based Organization: Operations automatically grouped by OpenAPI tags for intuitive navigation
- Structured Exceptions: Type-safe error handling with meaningful exception hierarchy
- Easy Testing: Auto-generated Protocol classes for each endpoint enable strict type-safe mocking
pip install pyopenapi-genOr with Poetry:
poetry add pyopenapi-genpyopenapi-gen openapi.yaml \
--project-root . \
--output-package my_api_clientThis creates a complete Python package at ./my_api_client/ with:
- Type-safe models from your schemas
- Async methods for all operations
- Built-in authentication support
- Complete independence from this generator
import asyncio
from my_api_client.client import APIClient
from my_api_client.core.config import ClientConfig
from my_api_client.core.http_transport import HttpxTransport
from my_api_client.core.auth.plugins import BearerAuth
async def main():
# Configure the client
config = ClientConfig(
base_url="https://api.example.com",
timeout=30.0
)
# Optional: Add authentication
auth = BearerAuth("your-api-token")
transport = HttpxTransport(
base_url=config.base_url,
timeout=config.timeout,
auth=auth
)
# Use as async context manager
async with APIClient(config, transport=transport) as client:
# Type-safe API calls with full IDE support
users = await client.users.list_users(limit=10)
# All operations are fully typed
user = await client.users.get_user(user_id=123)
print(f"User: {user.name}, Email: {user.email}")
asyncio.run(main())The generator was designed to work both as a CLI tool and as a Python library. Programmatic usage enables integration with build systems, CI/CD pipelines, code generators, and custom tooling. You get the same powerful code generation capabilities with full Python API access.
from pyopenapi_gen import generate_client
# Simple client generation
files = generate_client(
spec_path="input/openapi.yaml",
project_root=".",
output_package="pyapis.my_client"
)
print(f"Generated {len(files)} files")from pyopenapi_gen import generate_client, GenerationError, NamingStrategy
try:
files = generate_client(
spec_path="input/openapi.yaml",
project_root=".",
output_package="pyapis.my_client",
core_package="pyapis.core", # Optional shared core
force=True, # Overwrite without diff check
no_postprocess=False, # Run Black + mypy
verbose=True, # Show progress
naming_strategy=NamingStrategy.CLEAN, # Strip FastAPI suffixes
)
# Process generated files
for file_path in files:
print(f"Generated: {file_path}")
except GenerationError as e:
print(f"Generation failed: {e}")from pyopenapi_gen import generate_client
from pathlib import Path
# Configuration for multiple clients
clients = [
{"spec": "api_v1.yaml", "package": "pyapis.client_v1"},
{"spec": "api_v2.yaml", "package": "pyapis.client_v2"},
]
# Shared core package
core_package = "pyapis.core"
# Generate all clients
for client_config in clients:
print(f"Generating {client_config['package']}...")
generate_client(
spec_path=client_config["spec"],
project_root=".",
output_package=client_config["package"],
core_package=core_package,
force=True,
verbose=True
)
print("All clients generated successfully!")def generate_client(
spec_path: str,
project_root: str,
output_package: str,
core_package: str | None = None,
force: bool = False,
no_postprocess: bool = False,
verbose: bool = False,
naming_strategy: NamingStrategy = NamingStrategy.OPERATION_ID,
) -> List[Path]Parameters:
spec_path: Path to OpenAPI spec file (YAML or JSON)project_root: Root directory of your Python projectoutput_package: Python package name (e.g.,'pyapis.my_client')core_package: Optional shared core package name (defaults to{output_package}.core)force: Skip diff check and overwrite existing outputno_postprocess: Skip Black formatting and mypy type checkingverbose: Print detailed progress informationnaming_strategy: Strategy for deriving method names (operationId,clean, orpath)
Returns: List of Path objects for all generated files
Raises: GenerationError if generation fails
For advanced use cases requiring more control:
from pyopenapi_gen import ClientGenerator, GenerationError, NamingStrategy
from pathlib import Path
# Create generator with custom settings
generator = ClientGenerator(verbose=True)
# Generate with full control
try:
files = generator.generate(
spec_path="openapi.yaml",
project_root=Path("."),
output_package="pyapis.my_client",
core_package="pyapis.core",
force=False,
no_postprocess=False,
naming_strategy=NamingStrategy.CLEAN,
)
except GenerationError as e:
print(f"Generation failed: {e}")Raised when generation fails. Contains contextual information about the failure:
from pyopenapi_gen import generate_client, GenerationError
try:
generate_client(
spec_path="invalid.yaml",
project_root=".",
output_package="test"
)
except GenerationError as e:
# Exception message includes context
print(f"Error: {e}")
# Typical causes:
# - Invalid OpenAPI specification
# - File I/O errors
# - Type checking failures
# - Invalid project structurepyopenapi-gen openapi.yaml \
--project-root . \
--output-package my_api_clientCreates self-contained client with embedded core dependencies.
pyopenapi-gen openapi.yaml \
--project-root . \
--output-package clients.api_client \
--core-package clients.coreMultiple clients share a single core implementation.
Control how Python method names are derived from OpenAPI operations:
# Default: use operationId from the spec as-is
pyopenapi-gen openapi.yaml --project-root . --output-package myapp \
--naming-strategy operationId
# Clean: strip auto-generated suffixes from frameworks like FastAPI
# e.g. "create_details_details_post" → "create_details"
pyopenapi-gen openapi.yaml --project-root . --output-package myapp \
--naming-strategy clean
# Path: ignore operationId, derive names from HTTP method + path
# e.g. POST /users → "post_users"
pyopenapi-gen openapi.yaml --project-root . --output-package myapp \
--naming-strategy path--force # Overwrite without prompting
--no-postprocess # Skip formatting and type checkingThe generated clients support flexible authentication through the transport layer. Authentication plugins modify requests before they're sent.
from my_api_client.core.auth.plugins import BearerAuth
from my_api_client.core.http_transport import HttpxTransport
auth = BearerAuth("your-api-token")
transport = HttpxTransport(
base_url="https://api.example.com",
auth=auth
)
async with APIClient(config, transport=transport) as client:
# All requests automatically include: Authorization: Bearer your-api-token
users = await client.users.list_users()from my_api_client.core.auth.plugins import ApiKeyAuth
# API key in header
auth = ApiKeyAuth("your-key", location="header", name="X-API-Key")
# API key in query string
auth = ApiKeyAuth("your-key", location="query", name="api_key")
# API key in cookie
auth = ApiKeyAuth("your-key", location="cookie", name="session")
transport = HttpxTransport(
base_url="https://api.example.com",
auth=auth
)from my_api_client.core.auth.plugins import OAuth2Auth
async def refresh_token(current_token: str) -> str:
# Your token refresh logic
# Call your auth server to get a new token
new_token = await get_new_token()
return new_token
auth = OAuth2Auth(
access_token="initial-token",
refresh_callback=refresh_token
)
transport = HttpxTransport(
base_url="https://api.example.com",
auth=auth
)from my_api_client.core.auth.base import CompositeAuth
from my_api_client.core.auth.plugins import BearerAuth, HeadersAuth
# Combine multiple authentication methods
auth = CompositeAuth(
BearerAuth("api-token"),
HeadersAuth({"X-Client-ID": "my-app", "X-Version": "1.0"})
)
transport = HttpxTransport(
base_url="https://api.example.com",
auth=auth
)
# All requests include both Authorization header and custom headersfrom typing import Any
from my_api_client.core.auth.base import BaseAuth
class CustomAuth(BaseAuth):
"""Your custom authentication logic"""
def __init__(self, api_key: str, secret: str):
self.api_key = api_key
self.secret = secret
async def authenticate_request(self, request_args: dict[str, Any]) -> dict[str, Any]:
# Add custom authentication logic
headers = dict(request_args.get("headers", {}))
headers["X-API-Key"] = self.api_key
headers["X-Signature"] = self._generate_signature()
request_args["headers"] = headers
return request_args
def _generate_signature(self) -> str:
# Your signature generation logic
return "signature"
auth = CustomAuth("key", "secret")
transport = HttpxTransport(base_url="https://api.example.com", auth=auth)The generated client raises structured exceptions for all non-2xx responses:
from my_api_client.core.exceptions import HTTPError, ClientError, ServerError
try:
user = await client.users.get_user(user_id=123)
print(f"Found user: {user.name}")
except ClientError as e:
# 4xx errors - client-side issues
print(f"Client error {e.status_code}: {e.response.text}")
if e.status_code == 404:
print("User not found")
elif e.status_code == 401:
print("Authentication required")
except ServerError as e:
# 5xx errors - server-side issues
print(f"Server error {e.status_code}: {e.response.text}")
except HTTPError as e:
# Catch-all for any HTTP errors
print(f"HTTP error {e.status_code}: {e.response.text}")For operations that return streaming data (like SSE or file downloads):
# Server-Sent Events (SSE)
async for event in client.events.stream_events():
print(f"Received event: {event}")
# Binary streaming (files, large downloads)
async with client.files.download_file(file_id=123) as response:
async for chunk in response:
# Process binary chunks
file.write(chunk)Generated models use cattrs with Meta class for seamless API ↔ Python field name conversion:
from my_api_client.models.user import User
# API returns camelCase: {"firstName": "John", "lastName": "Doe"}
# Python uses snake_case automatically
user_data = await client.users.get_user(user_id=1)
print(user_data.first_name) # "John" - automatically mapped
print(user_data.last_name) # "Doe"
# Serialization back to API format works automatically
new_user = User(first_name="Jane", last_name="Smith")
created = await client.users.create_user(user=new_user)
# Sends: {"firstName": "Jane", "lastName": "Smith"}All generated code includes complete type hints:
# Your IDE provides autocomplete for all methods
client.users. # IDE shows: list_users(), get_user(), create_user(), etc.
# All parameters are typed
await client.users.create_user(
user=User( # IDE autocompletes User fields
name="John",
email="john@example.com",
age=30 # Type checking catches wrong types
)
)
# Return types are fully specified
user: User = await client.users.get_user(user_id=1)
# mypy validates the entire chain# Generate clients for internal services
pyopenapi-gen services/user-api/openapi.yaml \
--project-root . \
--output-package myapp.clients.users
pyopenapi-gen services/order-api/openapi.yaml \
--project-root . \
--output-package myapp.clients.orders
# Use in your application
from myapp.clients.users.client import APIClient as UserClient
from myapp.clients.orders.client import APIClient as OrderClient
async def process_order(user_id: int, order_id: int):
async with UserClient(user_config) as user_client:
user = await user_client.users.get_user(user_id=user_id)
async with OrderClient(order_config) as order_client:
order = await order_client.orders.get_order(order_id=order_id)# Generate a distributable SDK
pyopenapi-gen public-api.yaml \
--project-root sdk \
--output-package mycompany_sdk
# Package structure for distribution:
# sdk/
# mycompany_sdk/
# __init__.py
# client.py
# models/
# endpoints/
# core/
# setup.py
# README.md
# Users install: pip install mycompany-sdk
# Users use: from mycompany_sdk.client import APIClient# Generate once, configure per environment
from my_api_client.client import APIClient
from my_api_client.core.config import ClientConfig
from my_api_client.core.http_transport import HttpxTransport
from my_api_client.core.auth.plugins import BearerAuth
# Development
dev_config = ClientConfig(base_url="https://dev-api.example.com")
dev_auth = BearerAuth(os.getenv("DEV_API_TOKEN"))
dev_transport = HttpxTransport(dev_config.base_url, auth=dev_auth)
# Production
prod_config = ClientConfig(base_url="https://api.example.com")
prod_auth = BearerAuth(os.getenv("PROD_API_TOKEN"))
prod_transport = HttpxTransport(prod_config.base_url, auth=prod_auth)
# Use the same client code with different configs
async with APIClient(dev_config, transport=dev_transport) as client:
users = await client.users.list_users()# Point generated client at mock server for testing
import pytest
from my_api_client.client import APIClient
from my_api_client.core.config import ClientConfig
@pytest.fixture
async def api_client(mock_server_url):
"""API client pointing to mock server"""
config = ClientConfig(base_url=mock_server_url)
async with APIClient(config) as client:
yield client
async def test_user_creation(api_client):
# Mock server returns predictable responses
user = await api_client.users.create_user(
user={"name": "Test User", "email": "test@example.com"}
)
assert user.name == "Test User"The generator automatically creates Protocol classes for every endpoint client, enforcing strict type safety through explicit contracts. This enables easy testing with compile-time guarantees.
For each OpenAPI tag, the generator creates:
# Generated automatically from your OpenAPI spec:
@runtime_checkable
class UsersClientProtocol(Protocol):
"""Protocol defining the interface of UsersClient for dependency injection."""
async def get_user(self, user_id: int) -> User: ...
async def list_users(self, limit: int = 10) -> list[User]: ...
async def create_user(self, user: User) -> User: ...
class UsersClient(UsersClientProtocol):
"""Real implementation - explicitly implements the protocol"""
def __init__(self, transport: HttpTransport, base_url: str) -> None:
self._transport = transport
self.base_url = base_url
async def get_user(self, user_id: int) -> User:
# Real HTTP implementation
...Key Point: The real implementation explicitly inherits from the Protocol, ensuring mypy validates it implements all methods correctly!
Your mocks must explicitly inherit from the generated Protocol to get compile-time safety:
import pytest
from my_api_client.endpoints.users import UsersClientProtocol
from my_api_client.endpoints.orders import OrdersClientProtocol
from my_api_client.models.user import User
from my_api_client.models.order import Order
class MockUsersClient(UsersClientProtocol):
"""
Mock implementation that explicitly inherits from the generated Protocol.
CRITICAL: If UsersClientProtocol changes (new method, different signature),
mypy will immediately flag this class as incomplete.
"""
def __init__(self):
self.calls: list[tuple[str, dict]] = [] # Track method calls
self.mock_data: dict[int, User] = {} # Store mock responses
async def get_user(self, user_id: int) -> User:
"""Mock implementation of get_user"""
self.calls.append(("get_user", {"user_id": user_id}))
# Return mock data
if user_id in self.mock_data:
return self.mock_data[user_id]
# Return default mock user
return User(
id=user_id,
name="Test User",
email=f"user{user_id}@example.com"
)
async def list_users(self, limit: int = 10) -> list[User]:
"""Mock implementation of list_users"""
self.calls.append(("list_users", {"limit": limit}))
return [
User(id=1, name="User 1", email="user1@example.com"),
User(id=2, name="User 2", email="user2@example.com"),
][:limit]
async def create_user(self, user: User) -> User:
"""Mock implementation of create_user"""
self.calls.append(("create_user", {"user": user}))
user.id = 123
return user
class MockOrdersClient(OrdersClientProtocol):
"""Mock OrdersClient - explicitly implements the protocol"""
async def get_order(self, order_id: int) -> Order:
return Order(id=order_id, status="completed", total=99.99)
async def create_order(self, order: Order) -> Order:
order.id = 456
order.status = "pending"
return order
# Type checking ensures mocks match protocols at compile time!
# If you forget a method or have wrong signatures:
# mypy error: Cannot instantiate abstract class 'MockUsersClient' with abstract method 'new_method'
@pytest.fixture
def mock_users_client() -> UsersClientProtocol:
"""
Fixture providing a mock users client.
Return type annotation ensures type safety.
"""
return MockUsersClient()
@pytest.fixture
def mock_orders_client() -> OrdersClientProtocol:
"""Fixture providing a mock orders client"""
return MockOrdersClient()Now inject the mocks into your business logic:
async def test_user_service_with_mocks(mock_users_client, mock_orders_client):
"""Test your business logic with mocked API clients"""
# Your business logic that depends on API clients
async def process_user_order(users_client, orders_client, user_id: int):
user = await users_client.get_user(user_id=user_id)
order = await orders_client.create_order(Order(user_id=user.id, items=[]))
return user, order
# Test with mocked clients
user, order = await process_user_order(
mock_users_client,
mock_orders_client,
user_id=123
)
# Assertions on business logic results
assert user.name == "Test User"
assert order.status == "pending"
# Verify interactions with the mock
assert len(mock_users_client.calls) == 1
assert mock_users_client.calls[0] == ("get_user", {"user_id": 123})Structure your code to accept Protocol types, not concrete implementations:
from my_api_client.endpoints.users import UsersClientProtocol
from my_api_client.endpoints.orders import OrdersClientProtocol
class UserService:
"""
Service that depends on Protocol interfaces.
CRITICAL: Accept Protocol types, not concrete classes!
This allows injecting both real clients and mocks.
"""
def __init__(
self,
users_client: UsersClientProtocol, # Protocol type!
orders_client: OrdersClientProtocol # Protocol type!
):
self.users = users_client
self.orders = orders_client
async def get_user_with_orders(self, user_id: int):
user = await self.users.get_user(user_id=user_id)
orders = await self.orders.list_orders(user_id=user_id)
return {"user": user, "orders": orders}
# In production: inject real clients (they implement the protocols)
from my_api_client.client import APIClient
from my_api_client.core.config import ClientConfig
config = ClientConfig(base_url="https://api.example.com")
async with APIClient(config) as client:
service = UserService(
users_client=client.users, # UsersClient implements UsersClientProtocol
orders_client=client.orders # OrdersClient implements OrdersClientProtocol
)
result = await service.get_user_with_orders(user_id=123)
# In tests: inject mocks (they also implement the protocols)
async def test_user_service(mock_users_client, mock_orders_client):
service = UserService(
users_client=mock_users_client, # MockUsersClient implements UsersClientProtocol
orders_client=mock_orders_client # MockOrdersClient implements OrdersClientProtocol
)
result = await service.get_user_with_orders(user_id=123)
assert result["user"].name == "Test User"
assert len(result["orders"]) > 0
# Verify mock was called correctly
assert ("get_user", {"user_id": 123}) in mock_users_client.calls- Automatic Generation: Protocols are generated from your OpenAPI spec - no manual writing
- Compile-Time Safety: mypy catches missing/incorrect methods immediately
- Forced Updates: When API changes, stale mocks break at compile time, not runtime
- Test at Right Level: Mock business operations (get_user, create_order), not HTTP transport
- IDE Support: Full autocomplete and inline errors for protocol implementations
- Refactoring Safety: Rename operations? All implementations must update or fail type checking
- Documentation: Protocol serves as explicit, enforced contract documentation
- No Runtime Overhead: Protocols are pure type-checking, zero runtime cost
Complete example showing protocol-based testing in action:
# my_service.py
from my_api_client.endpoints.users import UsersClientProtocol
from my_api_client.models.user import User
class UserRegistrationService:
"""Business logic for user registration"""
def __init__(self, users_client: UsersClientProtocol):
self.users_client = users_client
async def register_user(self, name: str, email: str) -> User:
"""Register a new user with validation"""
# Business logic
if not email or "@" not in email:
raise ValueError("Invalid email")
# Use API client
user = User(name=name, email=email)
return await self.users_client.create_user(user=user)
# test_my_service.py
import pytest
from my_service import UserRegistrationService
from my_api_client.endpoints.users import UsersClientProtocol
from my_api_client.models.user import User
class MockUsersClient(UsersClientProtocol):
"""Type-safe mock for testing"""
def __init__(self):
self.created_users: list[User] = []
async def get_user(self, user_id: int) -> User:
return User(id=user_id, name="Test", email="test@example.com")
async def list_users(self, limit: int = 10) -> list[User]:
return []
async def create_user(self, user: User) -> User:
user.id = 123 # Simulate server assigning ID
self.created_users.append(user)
return user
@pytest.fixture
def mock_users_client() -> UsersClientProtocol:
return MockUsersClient()
async def test_register_user__valid_data__creates_user(mock_users_client):
"""
When: Registering with valid data
Then: User is created via API
"""
service = UserRegistrationService(mock_users_client)
user = await service.register_user(name="John", email="john@example.com")
assert user.id == 123
assert user.name == "John"
assert len(mock_users_client.created_users) == 1
async def test_register_user__invalid_email__raises_error(mock_users_client):
"""
When: Registering with invalid email
Then: ValueError is raised
"""
service = UserRegistrationService(mock_users_client)
with pytest.raises(ValueError, match="Invalid email"):
await service.register_user(name="John", email="invalid")
# If UsersClientProtocol changes (e.g., create_user signature changes):
# mypy error: Cannot instantiate abstract class 'MockUsersClient' with abstract method 'create_user'
# This forces you to update your mock, keeping tests in sync with API!The generator creates ready-to-use mock helper classes in the mocks/ directory, providing a faster path to testable code.
my_api_client/
├── mocks/
│ ├── __init__.py # Exports MockAPIClient and all endpoint mocks
│ ├── mock_client.py # MockAPIClient with auto-create pattern
│ └── endpoints/
│ ├── __init__.py # Exports MockUsersClient, MockOrdersClient, etc.
│ ├── mock_users.py # MockUsersClient helper
│ └── mock_orders.py # MockOrdersClient helper
Instead of manually creating mock classes, inherit from the generated helpers:
from my_api_client.mocks import MockAPIClient, MockUsersClient
from my_api_client.models.user import User
# Option 1: Override specific methods
class TestUsersClient(MockUsersClient):
"""Inherit from generated mock, override only what you need"""
async def get_user(self, user_id: int) -> User:
return User(id=user_id, name="Test User", email="test@example.com")
# list_users and create_user will raise NotImplementedError with helpful messages
# Option 2: Use MockAPIClient with hybrid auto-create pattern
client = MockAPIClient(users=TestUsersClient())
# Access your custom mock
user = await client.users.get_user(user_id=123)
assert user.name == "Test User"
# Other endpoints auto-created with NotImplementedError stubs
# await client.orders.get_order(order_id=1) # Raises: NotImplementedError: Override MockOrdersClient.get_order()MockAPIClient automatically creates mock instances for all endpoint clients you don't explicitly provide:
from my_api_client.mocks import MockAPIClient, MockUsersClient, MockOrdersClient
from my_api_client.models.user import User
from my_api_client.models.order import Order
# Override only the clients you need for this test
class TestUsersClient(MockUsersClient):
async def get_user(self, user_id: int) -> User:
return User(id=user_id, name="Test User", email="test@example.com")
class TestOrdersClient(MockOrdersClient):
async def get_order(self, order_id: int) -> Order:
return Order(id=order_id, status="completed", total=99.99)
# Create client with partial overrides
client = MockAPIClient(
users=TestUsersClient(),
orders=TestOrdersClient()
# products, payments, etc. auto-created with NotImplementedError stubs
)
# Use your custom mocks
user = await client.users.get_user(user_id=123)
order = await client.orders.get_order(order_id=456)
# Unimplemented endpoints provide clear error messages
# await client.products.list_products() # NotImplementedError: Override MockProductsClient.list_products()Generated mock helpers raise NotImplementedError with helpful messages:
from my_api_client.mocks import MockUsersClient
mock = MockUsersClient()
# Attempting to call unimplemented method:
await mock.get_user(user_id=123)
# NotImplementedError: MockUsersClient.get_user() not implemented.
# Override this method in your test:
# class TestUsersClient(MockUsersClient):
# async def get_user(self, user_id: int) -> User:
# return User(...)Manual Protocol Implementation (always available):
from my_api_client.endpoints.users import UsersClientProtocol
class MockUsersClient(UsersClientProtocol):
"""Full control, implement all methods"""
async def get_user(self, user_id: int) -> User: ...
async def list_users(self, limit: int = 10) -> list[User]: ...
async def create_user(self, user: User) -> User: ...Auto-Generated Helper (faster, less boilerplate):
from my_api_client.mocks import MockUsersClient
class TestUsersClient(MockUsersClient):
"""Override only what you need"""
async def get_user(self, user_id: int) -> User:
return User(id=user_id, name="Test User", email="test@example.com")
# Other methods inherited with NotImplementedError stubsUse auto-generated mocks when:
- You want to quickly get started with testing
- You only need to override specific methods
- You prefer helpful NotImplementedError messages over abstract method errors
Use manual Protocol implementation when:
- You need complete control over all mock behavior
- You're building reusable test fixtures
- You want explicit tracking of all method calls
Both approaches are type-safe and provide compile-time validation!
The generator maps OpenAPI format specifiers to appropriate Python types with automatic serialisation/deserialisation:
| OpenAPI Format | Python Type | Notes |
|---|---|---|
date-time |
datetime.datetime |
ISO 8601 parsing |
date |
datetime.date |
ISO 8601 parsing |
time |
datetime.time |
ISO 8601 parsing |
duration |
datetime.timedelta |
ISO 8601 duration |
uuid |
uuid.UUID |
Standard UUID format |
binary |
bytes |
Raw binary data |
byte |
bytes |
Base64 encoded (auto-decoded) |
ipv4 |
ipaddress.IPv4Address |
IPv4 address validation |
ipv6 |
ipaddress.IPv6Address |
IPv6 address validation |
uri / url |
str |
No special handling |
email |
str |
No special handling |
hostname |
str |
No special handling |
password |
str |
No special handling |
int32 / int64 |
int |
Python int (unlimited precision) |
float / double |
float |
Python float |
💡 For
byteformat, the generated client automatically handles base64 encoding/decoding via cattrs hooks.
Some OpenAPI features have simplified implementations:
| Feature | Current Behavior | Workaround |
|---|---|---|
| Parameter Serialization | Uses httpx defaults (not OpenAPI style/explode) |
Manually format complex parameters |
| Response Headers | Only body is returned, headers are ignored | Use custom transport to access full response |
| Multipart Forms | Basic file upload only | Complex multipart schemas may need manual handling |
| Parameter Defaults | Schema defaults not in method signatures | Pass defaults explicitly when calling |
| WebSockets | Not currently supported | Use separate WebSocket library |
💡 These limitations rarely affect real-world usage. Most APIs work perfectly with the current implementation.
PyOpenAPI Generator uses a sophisticated three-stage pipeline designed for enterprise-grade reliability:
graph TD
A[OpenAPI Spec] --> B[Loading Stage]
B --> C[Intermediate Representation]
C --> D[Unified Type Resolution]
D --> E[Visiting Stage]
E --> F[Python Code AST]
F --> G[Emitting Stage]
G --> H[Generated Files]
H --> I[Post-Processing]
I --> J[Final Client Package]
subgraph "Key Components"
K[Schema Parser]
L[Cycle Detection]
M[Reference Resolution]
N[Type Service]
O[Code Emitters]
end
Complex Schema Handling: Modern OpenAPI specs contain circular references, deep nesting, and intricate type relationships. Our architecture handles these robustly.
Production Ready: Each stage has clear responsibilities and clean interfaces, enabling comprehensive testing and reliable code generation.
Extensible: Plugin-based authentication, customizable type resolution, and modular emitters make the system adaptable to various use cases.
- Architecture Guide - Deep dive into the system design
- Type Resolution - How types are resolved and generated
- Contributing Guide - How to contribute to the project
- API Reference - Complete API documentation
We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.
For Contributors: See our Contributing Guide for:
- Development setup and workflow
- Testing requirements (85% coverage, mypy strict mode)
- Code quality standards
- Pull request process
Quick Links:
- Architecture Documentation - System design and patterns
- Issue Tracker - Report bugs or request features
MIT License - see LICENSE file for details.
Generated clients are self-contained and can be distributed under any license compatible with your project.