OpenAPI Python SDK Creation: Speakeasy vs Open Source

Many of our users have switched from OpenAPI Generator (opens in a new tab) to Speakeasy for their Python SDKs. Learn how to use both SDK creators in this guide, and the differences between them.

Open-source OpenAPI generators are great for experimentation but lack the reliability, performance, and intuitive developer experience required for critical applications. As an alternative, Speakeasy creates idiomatic SDKs that meet the bar for enterprise use.

In this post, we'll focus on Python, but Speakeasy can also create SDKs in Go, TypeScript, Java, Ruby, PHP, and more.

We'll create a Python SDK using Speakeasy, then compare it to SDKs created by OpenAPI Generator; demonstrating how Speakeasy improves upon the open-source generators by offering:

  • Exceptional code readability
  • Improved models
  • Managed CI/CD
  • More supported OpenAPI features

What is OpenAPI Generator?

OpenAPI Generator, as opposed to an OpenAPI generator, is a specific community-run open-source SDK generator for the OpenAPI specification, focusing on version 3 (opens in a new tab). The Swagger Codegen (opens in a new tab) tool is similar, but run by the company Smartbear. OpenAPI Generator was forked from Swagger Codegen.

Preparing the SDK Generators

For our comparison, we ran Speakeasy and OpenAPI Generator each in its own Docker container, which works on Windows, macOS, or Linux. Using Docker instead of running code directly on your physical machine is safer, as the code cannot access files outside the folder you specify.

We used the PetStore 3.1 YAML schema file from the Swagger editor (opens in a new tab) examples menu in File > Load Example > OpenAPI 3.1 Petstore. To follow along with this guide, save the file to a subfolder called app in your current path, such as app/petstore31.yaml. We changed the schema to use the server version 3.1:


servers:
- url: https://petstore31.swagger.io/api/v31

OpenAPI Generator provides a Docker image, but Speakeasy does not. To install the Speakeasy CLI, you can either install the Go binary directly on your computer following the steps in the Speakeasy Getting Started guide, or run it in Docker, which we did.

To use Docker yourself, first create a Docker file called se.dockerfile with the content below, replacing YourApiKey with your key from the Speakeasy website.


FROM alpine:3.19
WORKDIR /app
RUN apk add bash go curl unzip sudo nodejs npm
RUN curl -fsSL https://raw.githubusercontent.com/speakeasy-api/speakeasy/main/install.sh | sh;
ENV GOPATH=/root/go
ENV PATH=$PATH:$GOPATH/bin
ENV SPEAKEASY_API_KEY=YourApiKey

Then build the Speakeasy image with the command below.


docker build -f se.dockerfile -t seimage .

Validating the Schemas

Both OpenAPI Generator and the Speakeasy CLI can validate an OpenAPI schema. We'll run both and compare the output.

Validation Using OpenAPI Generator

To validate petstore31.yaml using OpenAPI Generator, run the following in the terminal:


docker run --rm -v "./app:/local" openapitools/openapi-generator-cli validate -i /local/petstore31.yaml

OpenAPI Generator returns two warnings:


Warnings:
- Unused model: Address
- Unused model: Customer
[info] Spec has 2 recommendation(s).

Validation Using Speakeasy

Validate the schema with Speakeasy by running the following in the terminal:


docker run --rm -v "./app:/app" seimage speakeasy validate openapi -s /app/petstore31.yaml

The Speakeasy validator returns 72 hints about missing examples, seven warnings about missing responses, and three warnings about unused components. Each warning includes a detailed JSON-formatted error with line numbers.

Since both validators returned only warnings and not errors, we can assume both generators will create SDKs without issues.

Creating the SDKs

First, we'll generate an SDK with OpenAPI Generator, and then we'll create one with Speakeasy.

Generating an SDK With OpenAPI Generator

OpenAPI Generator includes two different Python SDK generators (and four server generators):

The only difference between the two is that python is the latest version, which uses Pydantic V2. You can ignore Pydantic V1.

To generate an SDK from the schema file in OpenAPI Generator, we ran the command given in the OpenAPI Generator readme (opens in a new tab) below.


docker run --rm -v "./app:/local" openapitools/openapi-generator-cli generate -i /local/petstore31.yaml -g python -o /local/og --additional-properties=packageName=petstore_sdk,projectName=petstore-sdk-python

This command gives one warning that looks like it could cause errors, o.o.codegen.utils.ModelUtils - Failed to get the schema name: null, but OpenAPI Generator successfully created three folders:

FolderContent
docsDocumentation in .md files for each object.
petstore_sdkPython code to call the API on the server, containing a models folder for each object in the schema. If you don't pass parameters to the build command, this folder will be called openapi_client.
testUnit tests for all objects and operations. These tests are partially or entirely stubs and require you to manually add specific pet instances or operations and the logic to check them.

The generator warned us in the terminal that Generation using 3.1.0 specs is in development and is not officially supported yet. The OpenAPI Generator roadmap (opens in a new tab) hasn't been updated in almost two years.

Creating an SDK With Speakeasy

Next, we'll create an SDK using the Speakeasy CLI with the command below.


docker run --rm -v "./app:/app" seimage speakeasy generate sdk --schema /app/petstore31.yaml --lang python --out /app/se

Speakeasy creates the following folders.

FolderContent
docsDocumentation in .md files for each component and operation.
srcPython code to call the API on the server, containing a models folder for each object in the schema.

Unlike the OpenAPI Generator output, the Speakeasy output includes no tests.

Comparing the SDK Code: Package Structure

We'll start our comparison by looking at the structure of each SDK: the OpenAPI Generator SDK and the Speakeasy SDK.

To count the lines of code in the SDKs, we'll run cloc for each (ignoring documentation and test folders):


cloc ./app/og/petstore_sdk
cloc ./app/se/src/openapi

Below are the results for each SDK.

ProjectFilesBlankCommentCodeTotal linesTotal non-blank lines
OpenAPI Generator1911152243447978376722
Speakeasy691386498516170455659

We see that the Speakeasy SDK has about the same number of lines of code and lines of comments as OpenAPI Generator, but OpenAPI Generator leaves more blank lines.

The following commands output the files of each SDK.


tree ./app/og/petstore_sdk # exclude docs and tests
tree ./app/se/src/openapi

Below is the output for OpenAPI Generator.


├── api
│   ├── __init__.py
│   ├── pet_api.py
│   ├── store_api.py
│   └── user_api.py
├── api_client.py
├── api_response.py
├── configuration.py
├── exceptions.py
├── __init__.py
├── models
│   ├── address.py
│   ├── api_response.py
│   ├── category.py
│   ├── customer.py
│   ├── __init__.py
│   ├── order.py
│   ├── pet.py
│   ├── tag.py
│   └── user.py
├── py.typed
└── rest.py

The folder structure is simple and clear with nothing unexpected. Files are separated at the API level (pet, store, and user) and by model. There are a few helper files, like exceptions.py.

Below is the output for Speakeasy.


├── basesdk.py
├── _hooks
│   ├── __init__.py
│   ├── registration.py
│   ├── sdkhooks.py
│   └── types.py
├── httpclient.py
├── __init__.py
├── models
│   ├── components
│   │   ├── apiresponse.py
│   │   ├── category.py
│   │   ├── httpmetadata.py
│   │   ├── __init__.py
│   │   ├── order.py
│   │   ├── pet.py
│   │   ├── security.py
│   │   ├── tag.py
│   │   └── user.py
│   ├── errors
│   │   ├── __init__.py
│   │   └── sdkerror.py
│   ├── __init__.py
│   └── operations
│   ├── addpet_form.py
│   ├── addpet_json.py
│   ├── addpet_raw.py
│   ├── createuser_form.py
│   ├── createuser_json.py
│   ├── createuser_raw.py
│   ├── createuserswithlistinput.py
│   ├── deleteorder.py
│   ├── deletepet.py
│   ├── deleteuser.py
│   ├── findpetsbystatus.py
│   ├── findpetsbytags.py
│   ├── getinventory.py
│   ├── getorderbyid.py
│   ├── getpetbyid.py
│   ├── getuserbyname.py
│   ├── __init__.py
│   ├── loginuser.py
│   ├── logoutuser.py
│   ├── placeorder_form.py
│   ├── placeorder_json.py
│   ├── placeorder_raw.py
│   ├── updatepet_form.py
│   ├── updatepet_json.py
│   ├── updatepet_raw.py
│   ├── updatepetwithform.py
│   ├── updateuser_form.py
│   ├── updateuser_json.py
│   ├── updateuser_raw.py
│   └── uploadfile.py
├── pet.py
├── sdkconfiguration.py
├── sdk.py
├── store.py
├── types
│   ├── basemodel.py
│   └── __init__.py
├── user.py
└── utils
├── enums.py
├── eventstreaming.py
├── forms.py
├── headers.py
├── __init__.py
├── metadata.py
├── queryparams.py
├── requestbodies.py
├── retries.py
├── security.py
├── serializers.py
├── url.py
└── values.py

The Speakeasy SDK is more complex and has more features. Files are separated at a lower level than OpenAPI Generator — at the operation level – and further split into media types of the operation, like addpet_json.py. There are more helper files bundled with the SDK in the utils folder. The _hooks folder allows you to insert custom code into the SDK.

Calling the Server

Swagger provides a complete test server for the PetStore OpenAPI version 3.1 server at https://petstore31.swagger.io (opens in a new tab). (There is a version 3.0 server too, but that gives 500 errors when called.)

To check that both SDKs contain code that actually works, we called the pet operations given in the readme files against the test server.

We used a Docker Python 3.8 container, as 3.8 is supported by both OpenAPI Generator and Speakeasy.

Calling the Server With OpenAPI Generator

For OpenAPI Generator, we ran the command below, with a successful response. First, the command installs the Python dependencies in the Docker container as recommended in the SDK README.md file, then it runs the sample main.py script to call the server using the SDK.


docker run --rm -v "./app/og:/app" -w "/app" python:3.8.19-alpine3.20 sh -c "pip install . && python setup.py install && python main.py"
# The response of PetApi->add_pet:
# Pet(id=10, name='doggie', category=None, photo_urls=['string'], tags=[], status='available')

The README.md does not give clear instructions for installing all dependencies. After running the installation commands above, pytest was not installed, even though it was mentioned in README.md.

Below is the main.py script to call the API.


import petstore_sdk
from petstore_sdk.models.pet import Pet
from petstore_sdk.rest import ApiException
from pprint import pprint
configuration = petstore_sdk.Configuration(
host = "https://petstore31.swagger.io/api/v31"
)
with petstore_sdk.ApiClient(configuration) as api_client:
api_instance = petstore_sdk.PetApi(api_client)
pet = Pet.from_json('''{
"id": 10,
"name": "doggie",
"photoUrls": [
"string"
],
"status": "available"
}''')
try:
api_response = api_instance.add_pet(pet)
print("The response of PetApi->add_pet:\n")
pprint(api_response)
except ApiException as e:
print("Exception when calling PetApi->add_pet: %s\n" % e)

The example code was partially given in README.md, but we needed to add the pet JSON sample from https://petstore31.swagger.io/#/pet/addPet (opens in a new tab).

Calling the Server With Speakeasy

Speakeasy also called the server successfully. The command below is almost identical to the one for OpenAPI Generator in the previous section, except that the Speakeasy SDK has more dependencies than OpenAPI Generator. Poetry needs packages that are not included with a minimal Linux installation.


docker run --rm -v "./app/se:/app" -w "/app" python:3.8.19-alpine3.20 sh -c "apk update && apk add --no-cache gcc musl-dev libffi-dev openssl-dev python3-dev py3-cffi py3-cryptography make && pip install --upgrade pip &&
pip install . && pip install poetry && poetry install && python main.py"
# Updated Pet Name: doggie

Before poetry install would work however, we had to comment out the invalid line in pyproject.toml:


[tool.poetry.group.extraDependencies.dependencies]
# dev = "[object Object]"

Below is the main.py script to call the API. The code comes straight from the README.md file (except the print line), including the correct JSON to create a pet.


from openapi import SDK
s = SDK(petstore_auth="<YOUR_PETSTORE_AUTH_HERE>",)
res = s.pet.update_pet_json(request={
"name": "doggie",
"photo_urls": [
"<value>",
],
"id": 10,
"category": {
"id": 1,
"name": "Dogs",
},
})
if res.pet is not None:
print("Updated Pet Name:", res.pet.name)

Models, Data Containers, and Typing

Both SDKs use strong typing in their models.

The OpenAPI Generator and Speakeasy SDK models inherit from the Pydantic BaseModel class. Pydantic (opens in a new tab) validates data at runtime and provides clear error messages when data is invalid.

For example, creating a Pet object with the name field set to an integer value will result in a validation error:


# Python Nextgen SDK
import petstore_sdk
pet = petstore_sdk.Pet(id=1, name="Archie", photoUrls=[])
pet2 = petstore_sdk.Pet(id=2, name=2, photoUrls=[])
# > pydantic.error_wrappers.ValidationError: 1 validation error for Pet
# > name
# > str type expected (type=type_error.str)

Both SDKs create a BaseModel pet like below.


class Pet(BaseModel):
name: Annotated[str, FieldMetadata(form=True)]
photo_urls: Annotated[List[str], pydantic.Field(alias="photoUrls"), FieldMetadata(form=True)]
id: Annotated[Optional[int], FieldMetadata(form=True)] = None
category: Annotated[Optional[Category], FieldMetadata(form=FormMetadata(json=True))] = None
tags: Annotated[Optional[List[Tag]], FieldMetadata(form=FormMetadata(json=True))] = None
status: Annotated[Annotated[Optional[Status], PlainValidator(validate_open_enum(False))], FieldMetadata(form=True)] = None
r"""pet status in the store"""

In addition, Speakeasy adds TypedDicts to its components. An example is from Pet.py below.


class PetTypedDict(TypedDict):
name: str
photo_urls: List[str]
id: NotRequired[int]
category: NotRequired[CategoryTypedDict]
tags: NotRequired[List[TagTypedDict]]
status: NotRequired[Status]
r"""pet status in the store"""

These typings provide strong runtime type checking and static type safety, which your IDE can use, too.

Speakeasy also has enums in models, which OpenAPI Generator does not. Below is an example from the pet model.


class Status(str, Enum):
r"""pet status in the store"""
AVAILABLE = 'available'
PENDING = 'pending'
SOLD = 'sold'

Contrast this enum with the string-based specification and validation created by OpenAPI Generator:


class Pet(BaseModel):
id: Optional[StrictInt] = None
...
status: Optional[StrictStr] = Field(default=None, description="pet status in the store")
...
@field_validator('status')
def status_validate_enum(cls, value):
"""Validates the enum"""
if value is None:
return value
if value not in set(['available', 'pending', 'sold']):
raise ValueError("must be one of enum values ('available', 'pending', 'sold')")
return value
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
... many more boilerplate methods below...

The OpenAPI Generator class also has to include many boilerplate methods for Pydantic, which is done for every model in the schema. Speakeasy models are more concise.

Open Enums

Speakeasy allows adding the custom attribute x-speakeasy-unknown-values to an OpenAPI field to allow open enums (opens in a new tab).


status:
type: string
x-speakeasy-unknown-values: allow
description: pet status in the store
enum:
- available
- pending
- sold

The SDK code for an open enum doesn't change much from a standard enum. It's shown below.


class Status(str, Enum, metaclass=utils.OpenEnumMeta):
r"""pet status in the store"""
AVAILABLE = 'available'
PENDING = 'pending'
SOLD = 'sold'

Adding x-speakeasy-unknown-values will result in a Python SDK that allows values for status that are outside the given list of available, pending, and sold. There will no longer be a conflict between a new version of an API sending an unknown enum value to an older version of an SDK.

The disadvantage of this open enum technique is that it allows clients to send mistaken and incorrect values to the server.

The OpenAPI Specification has no way to handle enum conflicts between schema versions currently, and so OpenAPI Generator has no similar feature to open enums. A standard solution is still being debated on GitHub (opens in a new tab).

SDK Dependencies

OpenAPI Generator and Speakeasy use almost identical Python packages.

OpenAPI Generator has the following dependencies in its requirements.txt file.


python_dateutil >= 2.5.3
setuptools >= 21.0.0
urllib3 >= 1.25.3, < 2.1.0
pydantic >= 2
typing-extensions >= 4.7.1

OpenAPI Generator also has a pyproject.toml file, though OpenAPI Generator does not mention this file in the installation instructions.

Speakeasy has the following dependencies in its pyproject.toml file.


httpx = "^0.27.0"
jsonpath-python = "^1.0.6"
pydantic = "^2.7.1"
python-dateutil = "^2.9.0.post0"
typing-inspect = "^0.9.0"

HTTP Client Library

The OpenAPI Generator SDK uses urllib3 in its HTTP clients, while the Speakeasy SDK uses the urllib3 and the HTTPX (opens in a new tab) library.

HTTPX is future-proofed for HTTP/2 and asynchronous method calls. As per the Speakeasy SDK readme, you can call the server using synchronous or asynchronous calls, as shown below.


import asyncio
from openapi import SDK
async def main():
res = await s.pet.update_pet_form_async(request={
"name": "doggie",
...

Supported Python Versions

At the time of compiling this comparison, the Speakeasy SDK required at least Python version 3.8, which is supported until October 2024. The OpenAPI Generator SDK required at least Python version 3.7, which ended support halfway through 2023.

We recommend you use the latest Python version with both SDKs.

Retries

The SDK created by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses. This provides a simpler developer experience for error handling.

To enable this feature, use the Speakeasy x-speakeasy-retries extension in your schema. We'll update the PetStore schema to add retries to the addPet operation as a test.

Edit petstore31.yaml and add the following to the addPet operation:


x-speakeasy-retries:
strategy: backoff
backoff:
initialInterval: 500 # 500 milliseconds
maxInterval: 60000 # 60 seconds
maxElapsedTime: 3600000 # 5 minutes
exponent: 1.5

Add this snippet to the operation:


#...
paths:
/pet:
# ...
post:
#...
operationId: addPet
x-speakeasy-retries:
strategy: backoff
backoff:
initialInterval: 500 # 500 milliseconds
maxInterval: 60000 # 60 seconds
maxElapsedTime: 3600000 # 5 minutes
exponent: 1.5

Now we can rerun the Speakeasy creator to enable retries for failed network requests when creating a new pet. It is also possible to enable retries for the SDK as a whole by adding global x-speakeasy-retries at the root of the schema.

Created Documentation

Both Speakeasy and OpenAPI Generator create a docs directory with documentation and usage examples.

We found the usage examples in the Speakeasy SDK worked flawlessly, while the examples in the OpenAPI Generator SDK don't always include required fields when instantiating objects. For instance, the Pet.md example has the code below.


# TODO update the JSON string below
json = "{}"

The OpenAPI Generator SDK's documentation is especially thorough regarding models and has a table of fields and their types for each model. The Speakeasy SDK's documentation is focused on usage, with a usage example for each operation for each model.

Speakeasy also creates appropriate example strings based on a field's format in the OpenAPI schema.

For example, if we add format: uri to the item for a pet's photo URLs, we can compare each SDK's usage documentation for this field.

The SDK created by Speakeasy includes a helpful example of this field that lists multiple random URLs:


# Speakeasy SDK Usage Example
pet = shared.Pet(
# ...
photo_urls=[
'https://salty-stag.name',
'https://moral-star.info',
'https://present-giggle.info',
]
)

The OpenAPI Generator SDK's documentation uses a single random string in its example:


# Python SDK Usage Example
pet = Pet(
# ...
photo_urls=[
"photo_urls_example"
]
)

Automation

This comparison focuses on installing and using Speakeasy and OpenAPI Generator using the command line, but both tools can also run as part of a CI workflow. For example, you can set up a GitHub Action (opens in a new tab) to ensure your Speakeasy SDK is always up to date when your API schema changes.

Unsupported Features

At the time of writing, OpenAPI Generator does not support:

Speakeasy supports all the features above. Nullable fields in a Speakeasy SDK are marked as Optional[].

Neither Speakeasy nor OpenAPI Generator supports XML requests and responses.

Summary

Open-source tooling can be a great way to experiment, but if you're working on production code, the Speakeasy Python SDK creator will help ensure that you create reliable and performant Python SDKs.

The Speakeasy Python SDK creator uses data classes to provide good runtime performance and exceptional readability, and automatic retries for network issues makes error handling straightforward. The Speakeasy-created documentation includes a working usage example for each operation. Finally, unlike other Python SDK creators, Speakeasy can publish your created SDK to PyPI and run it as part of your CI workflows.