Step-by-Step Tutorial to Use Python to Generate OpenAPI Documentation (with PySwagger)

Rebecca Kovács

Rebecca Kovács

12 June 2025

Step-by-Step Tutorial to Use Python to Generate OpenAPI Documentation (with PySwagger)

Generating comprehensive and accurate API documentation is a critical but often tedious part of software development. The OpenAPI Specification (formerly known as Swagger) has emerged as the industry standard for defining RESTful APIs. It provides a machine-readable format that allows both humans and computers to discover and understand the capabilities of a service without access to source code, documentation, or through network traffic inspection.1

While many frameworks offer plugins to generate OpenAPI specs from code annotations (like docstrings), there are scenarios where you might need more direct, programmatic control over the specification's creation. This could be because you're working with a legacy system, a non-standard framework, or you need to generate a spec for an API that is composed of multiple microservices.

This is where pyswagger comes in. It is a powerful Python library that functions as a toolkit for OpenAPI. While it's often used as an API client to consume services defined by an OpenAPI spec, its true power lies in its object model, which allows you to construct, manipulate, and validate a specification programmatically.

In this comprehensive tutorial, we will walk through the process of using pyswagger to manually, yet automatically, generate a complete OpenAPI 3.0 specification for a simple Python web application built with Flask. We will build the specification from the ground up, piece by piece, demonstrating how pyswagger's objects map directly to the components of the OpenAPI standard. By the end, you will not only have a generated openapi.json file but also a live, interactive documentation UI served directly from your application.

💡
Want a great API Testing tool that generates beautiful API Documentation?

Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?

Apidog delivers all your demands, and replaces Postman at a much more affordable price!
button

Part 1: Setting Up the Project Environment

Before we can start generating our specification, we need to set up a proper development environment. This involves creating an isolated Python environment to manage our dependencies and installing the necessary libraries.

Creating Your Workspace ⚙️

First, let's create a directory for our project. Open your terminal or command prompt and run the following commands:Bash

# Create a new directory for our project
mkdir pyswagger-tutorial
cd pyswagger-tutorial

# Create a Python virtual environment
# On macOS/Linux
python3 -m venv venv
# On Windows
python -m venv venv

A virtual environment is a self-contained directory tree that includes a Python installation and a number of supporting files. Using a virtual environment ensures that the packages we install for this project don't conflict with packages installed for other projects.

Now, activate the virtual environment:Bash

# On macOS/Linux
source venv/bin/activate

# On Windows
.\venv\Scripts\activate

Once activated, your terminal prompt should change to show the name of the virtual environment (e.g., (venv)), indicating that you are now working inside it.

Installing Necessary Libraries

With our environment active, we can install the Python libraries we'll need for this tutorial. We need pyswagger itself to build the spec, Flask to create our simple web API, and PyYAML because pyswagger uses it for YAML operations.Bash

pip install "pyswagger[utils]" Flask PyYAML

The [utils] part when installing pyswagger is a good practice as it includes helpful utilities, like a validator that we will use later to check our generated specification for correctness.

For good project management, it's wise to pin your dependencies in a requirements.txt file.Bash

pip freeze > requirements.txt

Your requirements.txt will now contain the libraries and their specific versions, making your project easily reproducible by others.

Creating the Basic Flask Application

Now, let's create a minimal Flask application. This will serve as the foundation of the API that we are going to document.

In your project directory, create a new file named app.py and add the following code:Python

# app.py

from flask import Flask, jsonify

# Initialize the Flask application
app = Flask(__name__)

@app.route("/")
def index():
    """ A simple endpoint to check if the app is running. """
    return jsonify({"message": "API is up and running!"})

if __name__ == "__main__":
    # Runs the Flask app on http://127.0.0.1:5000
    app.run(debug=True)

This code sets up a very simple web server with a single endpoint. To run it, make sure your virtual environment is still active and execute the following command in your terminal:Bash

python app.py

You should see output indicating that the server is running, something like this:

 * Serving Flask app 'app'
 * Debug mode: on
 * Running on http://127.0.0.1:5000 (Press CTRL+C to quit)

You can now open your web browser or use a tool like curl to visit http://127.0.0.1:5000. You should see the JSON response: {"message": "API is up and running!"}.

With our basic environment and application skeleton in place, we can now dive into the core concepts of pyswagger.


Part 2: Understanding the pyswagger Object Model

To effectively use pyswagger to generate a specification, you first need to understand how its object model corresponds to the structure of an OpenAPI document. An OpenAPI specification is essentially a large JSON or YAML object with a specific schema. pyswagger provides Python classes and objects that mirror this schema, allowing you to build the spec in a more intuitive, object-oriented way.

Core Concepts of the OpenAPI 3.0 Specification 📜

An OpenAPI 3.0 document has a few key top-level fields:

Mapping OpenAPI to pyswagger Objects

pyswagger provides a clean mapping from these OpenAPI concepts to Python objects. Let's explore the primary ones we'll be using.

The central object in pyswagger is the App. You can think of an App instance as the root of your OpenAPI document.Python

from pyswagger import App

# The root of the specification document
# We initialize it with a version, but it can also load from a URL or file
root_app = App(version='3.0.0')

Once you have your App object, you can start populating its attributes, which directly correspond to the OpenAPI fields. pyswagger uses a builder pattern, allowing for a fluent and readable syntax.

Info and Servers

The info and servers sections are straightforward to populate.Python

# Populating the 'info' object
root_app.info.title = "User API"
root_app.info.version = "1.0.0"
root_app.info.description = "A simple API to manage users, used for the pyswagger tutorial."

# Populating the 'servers' array
# You create a Server object and append it
server = root_app.prepare_obj('Server', {'url': 'http://127.0.0.1:5000', 'description': 'Local development server'})
root_app.servers.append(server)

Paths and Operations

Paths are defined on the App object. You add new paths and then define the operations (HTTP methods) within them. Each operation is configured with details like a summary, description, parameters, a requestBody, and responses.Python

# Defining a path and an operation
# This doesn't execute anything; it just builds the object structure.
path_item = root_app.define_path('/users')
get_op = path_item.define_op('get')
get_op.summary = "Retrieve a list of all users"

Components: Schemas, Parameters, and Responses

The real power of a well-structured OpenAPI spec comes from reusable components. Instead of defining the structure of a "User" object every time it appears in a response, you define it once in components/schemas and then reference it using a $ref pointer. pyswagger handles this elegantly.

Schema: A Schema object defines a data model. You can specify its type (object, string, integer) and its properties.Python

# Preparing a Schema object for a User
user_schema = root_app.prepare_obj('Schema', {
    'type': 'object',
    'properties': {
        'id': {'type': 'integer', 'format': 'int64'},
        'username': {'type': 'string'},
        'email': {'type': 'string', 'format': 'email'}
    },
    'required': ['id', 'username', 'email']
})

# Add it to the reusable components
root_app.components.schemas['User'] = user_schema

Parameter: A Parameter object defines a single operation parameter. You specify its name, where it is located (in: 'path', 'query', 'header', or 'cookie'), and its schema.Python

# Preparing a Parameter object for a user ID in the path
user_id_param = root_app.prepare_obj('Parameter', {
    'name': 'user_id',
    'in': 'path',
    'description': 'ID of the user to retrieve',
    'required': True,
    'schema': {'type': 'integer'}
})

Response: A Response object defines the structure of a response for a specific HTTP status code. It includes a description and the content, which specifies the media type (e.g., application/json) and its schema.Python

# Preparing a Response object for a 200 OK response returning a single user
# Note the use of '$ref' to point to our reusable User schema
ok_user_response = root_app.prepare_obj('Response', {
    'description': 'Successful retrieval of a user',
    'content': {
        'application/json': {
            'schema': {'$ref': '#/components/schemas/User'}
        }
    }
})

Understanding this mapping is the key to building your specification. You are essentially constructing a Python object graph that pyswagger will later serialize into a valid OpenAPI JSON or YAML file.


Part 3: Building a Simple API with Flask

To make our documentation exercise practical, we need an actual API to document. We'll expand our simple Flask app from Part 1 into a minimal REST API for managing a list of users. This API will serve as the "source of truth" that we will describe with pyswagger.

Designing a Simple "User" API 📝

We will implement four basic endpoints that represent common CRUD (Create, Read, Update, Delete) operations:

  1. GET /users: Retrieve a list of all users.
  2. POST /users: Create a new user.
  3. GET /users/{user_id}: Get a single user by their ID.
  4. DELETE /users/{user_id}: Delete a user by their ID.

For simplicity, we will use a simple Python dictionary as our in-memory "database". In a real-world application, this would be a connection to a database like PostgreSQL or MongoDB.

Implementing the Flask Endpoints

Let's update our app.py file to include the logic for these endpoints. Replace the content of app.py with the following:Python

# app.py

from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# --- In-Memory Database ---
# A simple dictionary to store our users.
# The key is the user_id (integer), and the value is the user data (dict).
USERS_DB = {
    1: {"username": "alice", "email": "alice@example.com"},
    2: {"username": "bob", "email": "bob@example.com"},
    3: {"username": "charlie", "email": "charlie@example.com"},
}
# A counter to simulate auto-incrementing IDs for new users
LAST_INSERT_ID = 3

# --- API Endpoints ---

@app.route("/users", methods=["GET"])
def get_users():
    """ Returns a list of all users. """
    # We need to convert the dictionary to a list of user objects, including their IDs.
    users_list = []
    for user_id, user_data in USERS_DB.items():
        user = {'id': user_id}
        user.update(user_data)
        users_list.append(user)
    return jsonify(users_list)

@app.route("/users", methods=["POST"])
def create_user():
    """ Creates a new user. """
    global LAST_INSERT_ID
    if not request.json or 'username' not in request.json or 'email' not in request.json:
        abort(400, description="Missing username or email in request body.")
    
    LAST_INSERT_ID += 1
    new_user_id = LAST_INSERT_ID
    
    new_user = {
        "username": request.json["username"],
        "email": request.json["email"],
    }
    
    USERS_DB[new_user_id] = new_user
    
    # The response should include the ID of the newly created user
    response_user = {'id': new_user_id}
    response_user.update(new_user)
    
    return jsonify(response_user), 201

@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
    """ Returns a single user by their ID. """
    if user_id not in USERS_DB:
        abort(404, description=f"User with ID {user_id} not found.")
    
    user_data = USERS_DB[user_id]
    user = {'id': user_id}
    user.update(user_data)
    
    return jsonify(user)

@app.route("/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id):
    """ Deletes a user by their ID. """
    if user_id not in USERS_DB:
        abort(404, description=f"User with ID {user_id} not found.")
    
    del USERS_DB[user_id]
    
    # A 204 No Content response is standard for successful deletions
    return '', 204

if __name__ == "__main__":
    app.run(debug=True, port=5000)

Now, if you run python app.py again, you will have a fully functional (though simple) API. You can test it with curl or a similar tool:

With our API implemented, we have a concrete target for our documentation efforts. The next step is to use pyswagger to describe each of these endpoints in detail.


Part 4: Auto-Generating the OpenAPI Spec with pyswagger

This is the core of our tutorial. We will now create a separate Python script that imports pyswagger, defines our API structure using its object model, and then serializes that structure into a complete openapi.json file. This approach decouples the specification generation from the application logic, which can be a very clean and maintainable pattern.

Creating the Specification Generator

In your project directory, create a new file named generate_spec.py. This script will be responsible for building and saving our OpenAPI specification.

Building the Spec, Step-by-Step

Let's build the generate_spec.py script piece by piece.

1. Imports and Initializing the App

First, we need to import the App object from pyswagger and PyYAML to help with dumping the final file. We'll create our root App object and populate the basic info and servers sections, just as we discussed in Part 2.Python

# generate_spec.py
import json
from pyswagger import App
from pyswagger.contrib.client.requests import Client

# --- 1. Initialize the root App object ---
app = App(version='3.0.0')

# --- 2. Populating the Info & Servers sections ---
app.info.title = "User API"
app.info.version = "1.0.0"
app.info.description = "A simple API to manage users, for the pyswagger tutorial."

server = app.prepare_obj('Server', {
    'url': 'http://127.0.0.1:5000',
    'description': 'Local development server'
})
app.servers.append(server)

2. Defining Reusable Components (Schemas)

Good API design avoids repetition. We will define our User data model once and reuse it. We'll also define a generic Error schema for our error responses (like 404 Not Found).

Add the following code to generate_spec.py:Python

# --- 3. Defining Reusable Components (Schemas) ---

# Schema for the Error response
error_schema = app.prepare_obj('Schema', {
    'type': 'object',
    'properties': {
        'code': {'type': 'integer', 'format': 'int32'},
        'message': {'type': 'string'}
    }
})
app.components.schemas['Error'] = error_schema

# Schema for a single User. Note the properties match our USERS_DB structure.
user_schema = app.prepare_obj('Schema', {
    'type': 'object',
    'properties': {
        'id': {
            'type': 'integer',
            'description': 'Unique identifier for the user.',
            'readOnly': True # The client cannot set this value
        },
        'username': {
            'type': 'string',
            'description': 'The user\'s chosen username.'
        },
        'email': {
            'type': 'string',
            'description': 'The user\'s email address.',
            'format': 'email'
        }
    },
    'required': ['id', 'username', 'email']
})
app.components.schemas['User'] = user_schema

# Schema for creating a user (doesn't include the 'id' field)
new_user_schema = app.prepare_obj('Schema', {
    'type': 'object',
    'properties': {
        'username': {
            'type': 'string',
            'description': 'The user\'s chosen username.'
        },
        'email': {
            'type': 'string',
            'description': 'The user\'s email address.',
            'format': 'email'
        }
    },
    'required': ['username', 'email']
})
app.components.schemas['NewUser'] = new_user_schema

3. Documenting the /users Endpoints

Now we will define the /users path and its two operations: GET and POST.

<!-- end list -->Python

# --- 4. Documenting the Paths ---

# -- Path: /users --
path_users = app.define_path('/users')

# Operation: GET /users
op_get_users = path_users.define_op('get')
op_get_users.summary = "List all users"
op_get_users.description = "Returns a JSON array of all user objects."
op_get_users.tags.append('Users')

op_get_users.responses.A('200').description = "A list of users."
op_get_users.responses.A('200').content.A('application/json').schema.A(
    'array', items={'$ref': '#/components/schemas/User'}
)

# Operation: POST /users
op_post_users = path_users.define_op('post')
op_post_users.summary = "Create a new user"
op_post_users.description = "Adds a new user to the database."
op_post_users.tags.append('Users')

op_post_users.requestBody.description = "User object that needs to be added."
op_post_users.requestBody.required = True
op_post_users.requestBody.content.A('application/json').schema.set_ref('#/components/schemas/NewUser')

op_post_users.responses.A('201').description = "User created successfully."
op_post_users.responses.A('201').content.A('application/json').schema.set_ref('#/components/schemas/User')

op_post_users.responses.A('400').description = "Invalid input provided."
op_post_users.responses.A('400').content.A('application/json').schema.set_ref('#/components/schemas/Error')

Notice the use of set_ref and A (which stands for "access") for a more concise syntax to build the nested object structure.

4. Documenting the /users/{user_id} Endpoints

Next, we'll document the path for interacting with a single user. This path includes a path parameter, {user_id}.

<!-- end list -->Python

# -- Path: /users/{user_id} --
path_user_id = app.define_path('/users/{user_id}')

# We can define the parameter once and reuse it for all operations on this path.
user_id_param = app.prepare_obj('Parameter', {
    'name': 'user_id',
    'in': 'path',
    'description': 'ID of the user',
    'required': True,
    'schema': {'type': 'integer'}
})
path_user_id.parameters.append(user_id_param)

# Operation: GET /users/{user_id}
op_get_user_id = path_user_id.define_op('get')
op_get_user_id.summary = "Find user by ID"
op_get_user_id.description = "Returns a single user."
op_get_user_id.tags.append('Users')

op_get_user_id.responses.A('200').description = "Successful operation."
op_get_user_id.responses.A('200').content.A('application/json').schema.set_ref('#/components/schemas/User')

op_get_user_id.responses.A('404').description = "User not found."
op_get_user_id.responses.A('404').content.A('application/json').schema.set_ref('#/components/schemas/Error')

# Operation: DELETE /users/{user_id}
op_delete_user_id = path_user_id.define_op('delete')
op_delete_user_id.summary = "Deletes a user"
op_delete_user_id.description = "Deletes a single user from the database."
op_delete_user_id.tags.append('Users')

op_delete_user_id.responses.A('204').description = "User deleted successfully."

op_delete_user_id.responses.A('404').description = "User not found."
op_delete_user_id.responses.A('404').content.A('application/json').schema.set_ref('#/components/schemas/Error')

5. Validating and Saving the Specification

Finally, the most satisfying step. We will ask pyswagger to validate our constructed object graph against the OpenAPI 3.0 schema. If it's valid, we'll dump it to a JSON file.

Add this final block of code to generate_spec.py:Python

# --- 5. Validate and Save the Specification ---
if __name__ == '__main__':
    try:
        # Validate the generated specification
        app.validate()
        print("Specification is valid.")

        # Save the specification to a JSON file
        with open('openapi.json', 'w') as f:
            f.write(app.dump_json(indent=2))
        print("Successfully generated openapi.json")

    except Exception as e:
        print(f"Validation Error: {e}")

Your generate_spec.py file is now complete. Run it from your terminal:Bash

python generate_spec.py

If everything is correct, you will see the output:

Specification is valid.
Successfully generated openapi.json

You will now have a new file, openapi.json, in your project directory. Open it up and explore its contents. You'll see a perfectly structured OpenAPI 3.0 document that describes your Flask API in minute detail.


Part 5: Serving the Documentation

Having an openapi.json file is great for machines and for generating client SDKs, but for human developers, an interactive UI is far more useful. In this final part, we will integrate our generated spec into our Flask app and serve it using the popular Swagger UI.

Integrating the Spec into Flask

First, we need to create an endpoint in our Flask app that serves the openapi.json file. This allows documentation tools to fetch the spec directly from our running API.

Modify app.py to add a new route:Python

# app.py

# ... (keep all the existing Flask code) ...
import os

# ... (all the routes like /users, /users/{user_id}, etc.) ...

# --- Serving the OpenAPI Specification and UI ---

@app.route('/api/docs/openapi.json')
def serve_openapi_spec():
    """ Serves the generated openapi.json file. """
    # This assumes openapi.json is in the same directory as app.py
    # In a larger app, you might want a more robust way to find the file
    return app.send_static_file('openapi.json')

For this to work, Flask needs to know where to find static files. By default, it looks in a static directory. Let's create one and move our openapi.json file there.Bash

mkdir static
mv openapi.json static/

Now, run python app.py, and navigate to http://127.0.0.1:5000/api/docs/openapi.json. You should see the raw JSON of your specification.

Serving Interactive UI with Swagger UI 🎨

Swagger UI is a dependency-free collection of HTML,2 JavaScript, and CSS assets that dynamically generates beautiful documentation from an OpenAPI-compliant API. We can easily serve it from our Flask application.

Create a Templates Directory: Flask uses a directory named templates to look for HTML templates.Bash

mkdir templates

Create the Swagger UI Template: Inside the templates directory, create a new file called swagger_ui.html. Paste the following HTML code into it. This code loads the Swagger UI assets from a public CDN and configures it to load our spec file.HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>User API - Swagger UI</title>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui.min.css" >
    <style>
        html {
            box-sizing: border-box;
            overflow: -moz-scrollbars-vertical;
            overflow-y: scroll;
        }
        *, *:before, *:after {
            box-sizing: inherit;
        }
        body {
            margin:0;
            background: #fafafa;
        }
    </style>
</head>

<body>
    <div id="swagger-ui"></div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-bundle.js"> </script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-standalone-preset.js"> </script>

<script>

window.onload = function() {

// Begin Swagger3 UI call region

const ui = SwaggerUIBundle({

url: "/api/docs/openapi.json", // The URL for our spec

dom_id: '#swagger-ui',

deepLinking: true,

presets: [

SwaggerUIBundle.presets.apis,

SwaggerUIStandalonePreset4

],

plugins: [

SwaggerUIBundle.plugins.DownloadUrl

],

layout: "StandaloneLayout"

})

// End Swagger UI call region

        window.ui = ui
    }
  </script>
</body>
</html>
```

Add the Flask Route: Finally, add a route to app.py to render this HTML template. You'll need to import render_template from Flask.Python

# At the top of app.py
from flask import Flask, jsonify, request, abort, render_template

# ... (all other routes) ...

@app.route('/api/docs/')
def serve_swagger_ui():
    """ Serves the interactive Swagger UI documentation. """
    return render_template('swagger_ui.html')

Putting It All Together

Your final project structure should look like this:

pyswagger-tutorial/
├── app.py
├── generate_spec.py
├── requirements.txt
├── static/
│   └── openapi.json
├── templates/
│   └── swagger_ui.html
└── venv/

Now for the final result. Make sure your Flask app is running (python app.py). Open your web browser and navigate to:

http://127.0.0.1:5000/api/docs/

You should be greeted with a beautiful, interactive documentation page for your API. You can expand each endpoint, see the schemas you defined, and even use the "Try it out" button to send live requests to your running Flask application directly from the browser.

Conclusion

In this tutorial, we have journeyed from an empty directory to a fully documented API. We successfully used pyswagger to programmatically construct a detailed OpenAPI 3.0 specification by mapping its object model to our API's structure. We saw how to define metadata, servers, reusable schemas, paths, and operations with their parameters and responses.

By separating the spec generation (generate_spec.py) from the application logic (app.py), we created a clean workflow: implement your API, then run the generator to produce the documentation. This process, while more manual than decorator-based approaches, offers unparalleled control and flexibility, making it ideal for complex projects, legacy codebases, or when documentation standards are strict.

Finally, by serving the generated spec and a Swagger UI instance, we provided a polished, professional, and highly usable documentation portal for our API's consumers. You now have a powerful new tool in your Python development arsenal for creating world-class API documentation.

💡
Want a great API Testing tool that generates beautiful API Documentation?

Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?

Apidog delivers all your demands, and replaces Postman at a much more affordable price!
button

Explore more

React Tutorial: A Beginner's Guide

React Tutorial: A Beginner's Guide

Welcome, aspiring React developer! You've made a fantastic choice. React is a powerful and popular JavaScript library for building user interfaces, and learning it is a surefire way to boost your web development skills. This comprehensive, step-by-step guide will take you from zero to hero, equipping you with the practical knowledge you need to start building your own React applications in 2025. We'll focus on doing, not just reading, so get ready to write some code! 💡Want a great API Testing

13 June 2025

How to Use DuckDB MCP Server

How to Use DuckDB MCP Server

Discover how to use DuckDB MCP Server to query DuckDB and MotherDuck databases with AI tools like Cursor and Claude. Tutorial covers setup and tips!

13 June 2025

Top 15 Free Tools for API Documentation

Top 15 Free Tools for API Documentation

Looking for the best free API documentation tools? This guide covers 15 top choices. Explore the features and benefits of each tool and choose the one that is best for your project.

13 June 2025

Practice API Design-first in Apidog

Discover an easier way to build and use APIs