Skip to content

Technology

Generating a Python SDK from an OpenAPI Specification

If your API ships an OpenAPI spec, OpenAPI Generator can build the Python client from it. You don't have to hand-write models and endpoint wrappers.

Most tutorials stop at generate. After that you're still choosing generator options, picking a spec with enough surface area to matter, locking dependencies, running smoke tests, and keeping the repo sane when the spec changes again.

This walkthrough uses a compact OpenAPI 3.1 example. I use Astral uv for lock, sync, test, and build because that's what I run locally. The generator config and generate steps are the same with pip or Poetry.

Prerequisites

You need three things:

Start with a Useful Specification

Start with an API specification that'll actually exercise the generator: tags, pagination, request bodies, enums, timestamps, and a reusable error schema, not one of the generic "Pet Store" examples.

Here is a compact OpenAPI 3.1 example:

openapi: 3.1.0
info:
  title: Dispatch API
  version: 1.0.0
  summary: A compact task-tracking API for SDK generation examples.
  description: |
    This example stays small enough for a blog post while still looking like a real API.
    It includes pagination, reusable schemas, enums, timestamps, bearer auth, and a
    structured error response.
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
servers:
  - url: https://api.example.com/v1
    description: Production
tags:
  - name: Tasks
    description: Manage tracked work items.
paths:
  /tasks:
    get:
      tags: [Tasks]
      operationId: listTasks
      summary: List tasks
      parameters:
        - name: status
          in: query
          required: false
          description: Filter tasks by workflow state.
          schema:
            $ref: "#/components/schemas/TaskStatus"
        - name: limit
          in: query
          required: false
          description: Maximum number of tasks to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          description: Opaque pagination cursor from a previous response.
          schema:
            type: string
      responses:
        "200":
          description: A page of tasks.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskListResponse"
        "401":
          description: Authentication failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Create a task
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
      responses:
        "201":
          description: Task created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /tasks/{taskId}:
    get:
      tags: [Tasks]
      operationId: getTask
      summary: Get a task by ID
      parameters:
        - $ref: "#/components/parameters/TaskId"
      responses:
        "200":
          description: The requested task.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "404":
          description: Task not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Tasks]
      operationId: updateTask
      summary: Update a task
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTaskRequest"
      responses:
        "200":
          description: Task updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "404":
          description: Task not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    TaskId:
      name: taskId
      in: path
      required: true
      description: Stable task identifier.
      schema:
        type: string
        format: uuid
  schemas:
    TaskStatus:
      type: string
      enum:
        - queued
        - running
        - completed
        - failed
    Task:
      type: object
      required:
        - id
        - title
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type:
            - string
            - "null"
        status:
          $ref: "#/components/schemas/TaskStatus"
        labels:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateTaskRequest:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type:
            - string
            - "null"
        labels:
          type: array
          items:
            type: string
    UpdateTaskRequest:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type:
            - string
            - "null"
        status:
          $ref: "#/components/schemas/TaskStatus"
        labels:
          type: array
          items:
            type: string
    TaskListResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        nextCursor:
          type:
            - string
            - "null"
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
        requestId:
          type:
            - string
            - "null"
security:
  - bearerAuth: []

This example is small enough to walk through without pasting pages of YAML.

Configure the Python Generator

Create a generator configuration file and make the build system choice explicit:

{
  "packageName": "python_dispatch_sdk",
  "projectName": "pythn-dispatch-sdk",
  "packageVersion": "1.0.0",
  "packageUrl": "https://example.com/dispatch-sdk",
  "description": "Python SDK for the Dispatch API.",
  "author": "The Authors",
  "authorEmail": "[email protected]",
  "license": "MIT",
  "library": "httpx",
  "buildSystem": "hatchling",
  "hideGenerationTimestamp": true
}

In that configuration, these fields matter most:

  • packageName: the import name, so use snake_case
  • projectName: the distribution name, so use hyphens
  • library: httpx: the default I'd pick today unless you need urllib3 (see Requests vs. HTTPX)
  • buildSystem: hatchling: keeps the generated pyproject.toml aligned with current Python packaging practice
  • hideGenerationTimestamp: true: removes a useless source of churn from regenerated files

Don't flip disallowAdditionalPropertiesIfNotPresent without reading the docs first. OpenAPI Generator's Python generator docs call true the old behavior and false the spec-compliant one. It's a compatibility switch, not a free hardening flag.

Generate the SDK

With the spec and config in place, generate the client:

openapi-generator-cli generate \
  -i snippets/example-openapi-3.1-dispatch-api.yaml \
  -g python \
  -o ./client \
  -c snippets/openapi-generator-config-example.json

That produces a Python package, API classes, models, tests, and a pyproject.toml. The point is to treat that output as generated source, not something you hand-edit line by line.

When the upstream specification changes, rerun the same command and review the diff. If you're hand-editing generated files, you'll fight every regeneration.

Manage the Generated Package with uv

The generator hands you a pyproject.toml. Below I use uv to compile a lockfile, sync the environment, run tests, and build artifacts. Swap in pip or Poetry if that's what you already use.

Lock the generated dependencies:

cd client
uv pip compile pyproject.toml -o requirements.txt --upgrade

Create a local environment and sync it exactly to the lockfile:

uv venv
source .venv/bin/activate
uv pip sync requirements.txt

On Windows, activate with .venv\Scripts\activate instead.

Install the generated package plus test tools:

uv pip install -e . pytest pytest-cov
python -m pytest test -v

uv pip compile writes a lock you can review in requirements.txt. uv pip sync installs exactly that lock instead of whatever was already on the machine.

Build the Distribution

Once the smoke tests pass, build the package artifacts:

uv build --no-sources

That writes the source distribution and wheel into dist/.

Keep Your Repository Clean

Generated SDK repositories can collect a lot of junk unless you make the boundaries explicit.

At a minimum, ignore local environments, caches, build output, and generator metadata:

.venv/
__pycache__/
*.py[cod]
dist/
build/
.pytest_cache/
.coverage
htmlcov/
.openapi-generator/
git_push.sh

I'd also rewrite the generated README.md before publishing. The scaffold is fine for a first pass, but it's not documentation you should put your name on.

Optional: Template Overrides

If you need custom copyright or SPDX headers, use template overrides instead of editing generated files after the fact:

openapi-generator-cli author template -g python -o ./templates

From there, edit the copied Mustache templates and regenerate with -t ./templates. Keep the override directory small. The more templates you fork, the more work you'll inherit on generator upgrades.

Here is the generated workflow in one pass:

OpenAPI Python SDK Workflow Demo

Let the generator handle the scaffolding.

The specification quality, package boundaries, and how you ship releases are where the maintenance cost actually lives.

References

vmcli: A Practical Guide for the VMware Fusion and Workstation Command Line

The command line is still the fastest way to script power state, disks, snapshots, and guest operations on with VMware Fusion (macOS) and VMware Workstation (Windows and Linux).

vmcli ships with both desktop hypervisors so you can drive the same lifecycle steps without opening the GUI.. Same lifecycle steps, no GUI.

This post covers the main modules with copy-and-paste examples.

Always confirm flags on your build with vmcli <module> --help and vmcli <vmx> <module> <command> --help, since Fusion and Workstation revisions do not always stay in lockstep.

Basic Syntax

vmcli is the cross-platform CLI for Fusion and Workstation. Each functional area (power, snapshots, storage, networking, guest operations) is a module with its own sub-commands. The usual shape is:

vmcli <vmx location> <module> <command> [args]

Important

The <vmx location> is required for commands working with VMs, and can be the first or last argument on the command line.

Global Options

Flag Description
-v, --version Display the version information.
--verbose Enable verbose logging.
-h, --help Display the help information.

Default Install Locations

CI runners and daemons often lack the same PATH as an interactive shell. Point scripts at the real binary when vmcli is not on PATH:

Platform Typical path
macOS (Fusion) /Applications/VMware Fusion.app/Contents/Public/vmcli
Windows (Workstation) C:\Program Files\VMware\VMware Workstation\vmcli.exe (or the x86 variant on some installs)

On Linux hosts, resolve the Workstation package path for your distro, or install location, then call that absolute path from automation.

If your host responds with vmcli: command not found, the binary is probably fine, but your PATH needs updating.

JSON Output for Scripting

Several query style commands accept -f json so you can parse output with jq or your language's JSON decoder instead of scraping plain text. Sub-command spelling is case sensitive (query vs Query), so match vmcli <module> --help on your build.

Example Purpose
vmcli <vmx> Power query -f json Structured power state.
vmcli <vmx> Snapshot query -f json Snapshot tree, UIDs, and current marker when the build supports it.
vmcli <vmx> Guest query -f json Guest and IP-style fields only while the VM is powered on with VMware Tools running (offline returns an error).
vmcli <vmx> Tools Query -f json Tools install and version status in JSON form.
vmcli <vmx> USB Query -f json Only on builds that list USB under vmcli --help; many Fusion vmcli trees omit USB entirely.

If -f json is missing on a sub-command, fall back to the default text layout or upgrade the desktop hypervisor build.

Core Modules and Commands

1. Power Operations (Power module)

Stop, Suspend, and Reset need -o <opType>; other rows below do not.

Command Required Args Description
vmcli <vmx> Power Start N/A Start the virtual machine.
vmcli <vmx> Power Stop -o <opType> Stop the virtual machine using the specified operation type.
vmcli <vmx> Power Pause N/A Pause the virtual machine execution.
vmcli <vmx> Power Unpause N/A Resume a paused virtual machine.
vmcli <vmx> Power Suspend -o <opType> Suspend the virtual machine using the specified operation type.
vmcli <vmx> Power Reset -o <opType> Reset the virtual machine using the specified operation type.
vmcli <vmx> Power Query N/A Query the current power state of the VM.

Power Start optional flags:

Flag Description
-p, --paused Power on in paused mode.
-s, --soft Power on in soft mode.

Valid -o opType values for Stop, Suspend, and Reset:

Value Description
hard Immediately cut power, the same as pulling the plug.
trySoft Attempt a graceful OS shutdown; fall back to hard if the guest doesn't respond.
soft Send an ACPI shutdown signal and wait for the guest to respond.
configDefault Use the operation type defined in the VM's configuration.

2. VM Management (VM & VMTemplate modules)

To create a new VM, use the VM module. Note that -g accepts a numeric preset and -c accepts any custom guest OS string.

Flag Required Description
-n, --name Yes Virtual machine name.
-d, --dirpath Yes Directory path where the VM will be created.
-g, --guesttype No Guest OS preset (numeric enum: 1–10).
-c, --custom-guesttype No Any custom guest OS type string (e.g., arm-ubuntu-64, ubuntu-64).

Example: Custom Guest OS Type

Use -c for named guest OS types such as arm-ubuntu-64 or ubuntu-64:

vmcli VM Create -n myVM -d ~/Desktop/ -c arm-ubuntu-64

The VMTemplate module is used for creating and deploying VM templates natively from the command line.

3. Snapshot Management (Snapshot module)

Snapshots give you quick checkpoints for testing and rollbacks. Commands that target an existing snapshot take a <uid> integer. Run Snapshot query first to list UIDs.

Command Required Args Description
vmcli <vmx> Snapshot Take <name> <name> Creates a snapshot. Use -d for a description and -m to include memory state.
vmcli <vmx> Snapshot Revert <uid> <uid> Revert the VM to the snapshot with the given UID.
vmcli <vmx> Snapshot Clone <uid> <filePath> <name> <uid>, <filePath>, <name> Clone a snapshot to a new .vmx file. Use -l for a linked clone.
vmcli <vmx> Snapshot Delete <uid> <uid> Removes the snapshot with the given UID. Use -d to also delete child snapshots.
vmcli <vmx> Snapshot query N/A Lists all snapshots and their UIDs.

!!! warning "Deleting and Reverting Snapshots" The VM must be powered off or suspended before using Snapshot Delete. Always run Snapshot query first to confirm the correct UID.

4. Hardware and Storage (Disk, Ethernet, Nvme, Sata modules)

Disk module. Create, extend, query, and configure virtual disks:

Sub-command Description
Disk Create -f <path> -a <adapter> -s <size> -t <type> Create a new virtual disk file.
Disk Extend <diskLabel> <newNumSectors> Extend an attached disk to a new size (in sectors).
Disk query Query the state of all disks attached to the VM.
Disk ConnectionControl Connect or disconnect a disk device.
Disk SetMode Set the persistence mode of the disk.
Disk SetReadOnly Mark a disk as read-only.
Disk SetBandwidthCap Set a bandwidth cap on the disk (bytes per second).
Disk Branch Create a new disk branch from the current VM state.

Valid -a adapter types for Disk Create: ide, buslogic, lsilogic (use lsilogic for all other types).

Valid -t disk types for Disk Create:

Type Description
0 Single growable virtual disk.
1 Growable virtual disk split into multiple files.
2 Pre-allocated virtual disk (single file).
3 Pre-allocated virtual disk split into multiple files.

Ethernet module. Configure virtual network adapters:

Sub-command Description
Ethernet query Query the current ethernet configuration.
Ethernet SetNetworkName <deviceLabel> <networkName> Connect an adapter to a named virtual network.
Ethernet SetConnectionType <deviceLabel> <connectionType> Set the connection type (e.g., nat, bridged, hostonly).
Ethernet SetVirtualDevice <deviceLabel> <deviceType> Set the virtual NIC type (e.g., vmxnet3, e1000e).
Ethernet SetPresent <deviceLabel> <bool> Add or remove a network adapter.

Other Hardware Modules:

Module Typical starting point
Nvme & Sata Controller and attachment sub-commands vary by build; see --help.
Serial Port presence and backing (file, pipe, network) via --help.

5. Configuration (ConfigParams module)

Read and write raw .vmx keys (RAM, CPU, nested virt, display name, and many hardware toggles).

Sub-command Description
ConfigParams query Print all current configuration entries for the VM.
ConfigParams SetEntry <name> <value> Write or overwrite a configuration entry.

Set bios.forceSetupOnce to TRUE for a one-time visit to firmware or EFI setup on the next boot; the hypervisor clears it after that cycle.

6. Shared Folders (HGFS module)

The Host Guest File System (HGFS) module manages the Shared Folders feature between host and guest. VMware Tools must be installed and running in the guest.

Sub-command Description
HGFS query Query the current shared folder configuration.
HGFS SetPresent <shareLabel> <bool> Add or remove a shared folder slot (for example sharedFolder0).
HGFS SetHostPath <shareLabel> <hostPath> Set the host path for that slot.
HGFS SetGuestName <shareLabel> <guestName> Name the guest sees (for example projects).
HGFS SetEnabled <shareLabel> <bool> Enable or disable a share without removing it.
HGFS SetReadAccess <shareLabel> <bool> Control read access on the share.
HGFS SetWriteAccess <shareLabel> <bool> Control write access on the share.

On many Fusion vmcli builds, HGFS sub-commands use the .vmx slot label (sharedFolder0, sharedFolder1, …), not an arbitrary string. Create the slot with SetPresent sharedFolder0 true (or matching ConfigParams entries), then set host path, guest name, and ACLs. Arbitrary labels such as projects alone are rejected until that slot exists.

7. Guest Operations (Guest module)

The Guest module executes operations directly inside a running VM. VMware Tools must be running for guest-side calls. Commands that start programs or touch the filesystem need -u <username> and -p <password>. Guest query omits -u / -p on supported builds, but the VM must still be powered on (offline returns an error). Confirm with vmcli <vmx> Guest query --help on your SKU.

Sub-command Description
Guest query Read guest state (for example OS string and networking) while the VM is on; combine with -f json for parsers.
Guest run Start a program inside the guest OS.
Guest ps List running processes in the guest.
Guest kill Terminate a process in the guest by PID.
Guest copyTo Copy a file from the host into the guest.
Guest copyFrom Copy a file from the guest back to the host.
Guest ls List directory contents inside the guest.
Guest mkdir Create a directory inside the guest.
Guest rm / rmdir Remove a file or directory inside the guest.
Guest mv / mvdir Move a file or directory inside the guest.
Guest env Show the guest's environment variables.
Guest createTempFile Create a temporary file in the guest.
Guest createTempDir Create a temporary directory in the guest.
Guest toolsproperties Show VMware Tools properties.

8. VMware Tools (Tools module)

Sub-command Description
Tools Query Query the state and version of VMware Tools in the guest.
Tools Install Mount the VMware Tools installer in the guest.
Tools Upgrade Upgrade VMware Tools to the latest available version.

9. Other Modules (MKS, Chipset, VProbes)

Module Description
MKS Mouse, keyboard, and screen (MKS) operations, including MKS captureScreenshot <output.png> while the VM displays video.
Chipset Configure low-level virtual chipset options.
VProbes Setup instrumentation probes inside the guest for performance analysis.

10. USB Devices (USB module)

Some Workstation (and occasional Fusion) builds expose a USB module. vmcli --help must list USB under Available modules; otherwise every vmcli … USB … invocation fails with Invalid/unrecognized argument "USB"`.

When the module exists:

Sub-command Description
USB Query Inspect USB devices visible to the VM; -f json when supported.
USB Connect <deviceId> Attach a host USB device to the guest.
USB Disconnect <deviceId> Release a device from the guest.

Device identifiers come from USB Query output; treat them as opaque strings.

11. Full VM clones (Clone module)

Separate from Snapshot Clone, some Workstation builds expose a top-level Clone module. Confirm with vmcli --help: many Fusion vmcli builds do not list Clone, so vmcli <vmx> Clone … fails the same way as a missing USB module.

Where Clone exists, sub-command names and flags have shifted across releases (CreateLinked, Create -t linked, and similar). Run vmcli Clone --help on the machine that will execute the job, then pin your automation to that text.

Practical Examples

Examples use VMX for the .vmx path and dirname "$VMX" for sibling files (extra disk, screenshots, logs). Use zsh or bash. On Windows, set VMX in the environment your script uses before each vmcli invocation.

!!! note "Where the .vmx path is" VM Create layout differs by product (flat folder vs *.vmwarevm bundle). Set VMX to the real .vmx after creation.

1. Create, tune, power on, optional firmware boot

vmcli VM Create -n test_vm -d ~/Desktop/ -c ubuntu-64
export VMX="$HOME/Desktop/test_vm.vmx"

vmcli "$VMX" ConfigParams SetEntry memsize 8192
vmcli "$VMX" ConfigParams SetEntry numvcpus 4
vmcli "$VMX" ConfigParams SetEntry vhv.enable TRUE
vmcli "$VMX" ConfigParams query | grep -E "memsize|numvcpus|vhv"

vmcli "$VMX" Power Start

# Optional: firmware or EFI setup on the *next* boot only (cleared after that boot)
# vmcli "$VMX" ConfigParams SetEntry bios.forceSetupOnce TRUE
# vmcli "$VMX" Power Start

2. Extra disk, JSON checks, screenshot

vmcli "$VMX" Disk Create \
  -f "$(dirname "$VMX")/test_vm-data.vmdk" \
  -a lsilogic \
  -s 50GB \
  -t 0
vmcli "$VMX" Disk query

# Same `-f json` calls as the table under "JSON output for scripting"
vmcli "$VMX" Power query -f json
vmcli "$VMX" Snapshot query -f json
vmcli "$VMX" Guest query -f json
vmcli "$VMX" Tools Query -f json
vmcli "$VMX" MKS captureScreenshot "$(dirname "$VMX")/test_vm-console.png"

3. Network (NAT to Bridged)

vmcli "$VMX" Ethernet query
vmcli "$VMX" Ethernet SetConnectionType ethernet0 bridged
vmcli "$VMX" Ethernet SetNetworkName ethernet0 "VMnet0"

4. Shared folders (HGFS)

shareLabel is the sharedFolderN slot (see the HGFS module table above).

vmcli "$VMX" HGFS SetPresent sharedFolder0 true
vmcli "$VMX" HGFS SetHostPath sharedFolder0 ~/Projects
vmcli "$VMX" HGFS SetGuestName sharedFolder0 projects
vmcli "$VMX" HGFS SetEnabled sharedFolder0 true
vmcli "$VMX" HGFS SetReadAccess sharedFolder0 true
vmcli "$VMX" HGFS SetWriteAccess sharedFolder0 true

5. VMware Tools

vmcli "$VMX" Tools Query
vmcli "$VMX" Tools Install
vmcli "$VMX" Tools Upgrade

6. Snapshots

vmcli "$VMX" Snapshot Take pre-upgrade -d "State before upgrading kernel"
vmcli "$VMX" Snapshot query
vmcli "$VMX" Power Stop -o trySoft
vmcli "$VMX" Snapshot Revert 1
# VM must be off or suspended to delete snapshots
vmcli "$VMX" Snapshot Delete 1
# When you need to drop child snapshots too, use -d before the UID (see `Snapshot Delete --help` on your build)
# vmcli "$VMX" Snapshot Delete -d 1

7. Guest Operations (Credentials)

Guest run, file copy, and ps need -u and -p (see the Guest module above). Export GUEST_PASS yourself; don't paste real passwords into shell history.

!!! warning "Credential Handling" Yes, -p on the command line is ugly. So is storing the password in a script file. For automation, use environment variables or a secrets manager and pass them at runtime instead of hard-coding them.

# Set GUEST_PASS in your environment before running these lines (never log real values).

vmcli "$VMX" Guest run -u root -p "$GUEST_PASS" \
  /bin/bash -c "apt-get update && apt-get upgrade -y"

vmcli "$VMX" Guest copyTo -u admin -p "$GUEST_PASS" \
  /path/on/host/nginx.conf /etc/nginx/nginx.conf

vmcli "$VMX" Guest copyFrom -u admin -p "$GUEST_PASS" \
  /var/log/syslog "$(dirname "$VMX")/guest-syslog.txt"

vmcli "$VMX" Guest ps -u admin -p "$GUEST_PASS"

vmcli "$VMX" Guest run -u admin -p "$GUEST_PASS" -nw \
  /usr/bin/python3 /opt/scripts/bootstrap.py

Demos

Two short screen recordings from a local Fusion Ubuntu guest (paths match a *.vmwarevm bundle on macOS). Keep "$VMCLI" quoted so VMware Fusion.app paths don't split at the space.

Snapshots, Tools, Power and Guest Query

Snapshots, Tools, Power and Guest Query

Copy Host File to Guest, then List

opy Host File to Guest, then List


Treat vmcli as a thin wrapper over desktop hypervisor operations: set VMX once per script or session, call one module at a time, and lean on --help when a flag changes between Fusion and Workstation builds.

!!! tip "Quick Reference" Run vmcli <module> --help for module-level syntax, and vmcli <vmx> <module> <sub-command> --help for a specific operation.

Disclaimer

This is not an official VMware by Broadcom document. This is a personal blog post.

The information is provided as-is with no warranties and confers no rights.

Please, refer to official documentation for the most up-to-date information.

Requests vs. HTTPX: Choosing a Python HTTP Client

requests earned its reputation honestly. For a long time, if you needed to call an API, download a file, or glue two services together in Python, requests was usually the obvious answer.

Async web frameworks, high-concurrency services, and I/O-heavy workloads changed what many teams need from an HTTP client. That's where httpx starts to matter.

This post compares requests and httpx from the point of view of a Python developer who already knows requests, wants a clear explanation of what httpx adds, and doesn't want hand-wavy "modernization" advice.

The Champion: requests

requests became the standard because it removed friction. Small API. Sensible defaults. Session when you need reuse.

  • The API is small and easy to learn.
  • The defaults are simple enough for scripts and internal tools.
  • Session gives you connection reuse and shared configuration without much ceremony.
  • The ecosystem around it is huge: tutorials, examples, blog posts, Stack Overflow answers, and third-party integrations all assume you know requests.

For synchronous Python code, it's still a good library. A lot of production code doesn't need async support, doesn't need HTTP/2, and doesn't need a broader transport model. In those cases, requests remains a sensible choice.

That's why this comparison is not really about replacing a bad tool. It's about recognizing where the older tool stops fitting as cleanly.

The Challenger: httpx

httpx looks familiar on purpose. The synchronous API feels close enough to requests that most Python developers can read it immediately. That compatibility is one of its best features, because it lowers the cost of trying it.

Its two biggest advantages are:

  • Native async and await support through httpx.AsyncClient
  • Optional HTTP/2 support on both sync and async clients

Those aren't cosmetic differences.

Async support means httpx fits naturally into frameworks like FastAPI, Starlette, and any other async application that should not block the event loop on outbound network calls. HTTP/2 support means one client connection can handle multiplexed requests when the remote server supports it, which can matter for high-concurrency workloads.

You opt in with httpx.Client(http2=True) or httpx.AsyncClient(http2=True), and the server still has to support HTTP/2 for you to get it.

httpx also pushes users toward safer networking defaults. The biggest example is timeouts: requests doesn't time out unless you ask it to, while httpx enforces timeouts by default.

Nginx: A Practical Deep Dive

Nginx ends up in front of a surprising amount of web traffic. You might not think much about it until you have to troubleshoot a redirect loop, a broken certificate renewal, or an application that only fails once it sits behind a proxy.

Static sites, application servers, APIs, internal tools, container platforms, and CDN origins often have Nginx somewhere out front. People keep using it because it's fast, it's predictable, and it makes you declare what the server should do.

This guide walks through the part that matters in practice: what Nginx is good at, how its config model works, how to install it, how to serve a site, how to terminate TLS, how to replace .htaccess style behavior the Nginx way, and how to use it as a reverse proxy and a basic load balancer.

Nginx is a web server, reverse proxy, and load balancer. In a modern stack it usually does one or more of these jobs:

  • Serves static files directly.
  • Terminates HTTPS connections.
  • Redirects HTTP to HTTPS.
  • Proxies requests to application servers.
  • Balances traffic across multiple backend services.
  • Adds request and response headers.
  • Buffers slow clients away from backend applications.
  • Enforces simple access control rules.
  • Caches upstream responses when configured to do so.

It's especially good at static assets, reverse proxying, and a lot of concurrent connections. It also works well as the boring edge layer in front of applications written in Node.js, Python, Go, Ruby, PHP, Java, or anything else that can listen on a port.

The simplest mental model:

graph TD
    A[Client Browser] -->|HTTPS Request| B[Nginx]
    B -->|HTTP Request to Local App| C[Backend Application]

Nginx does not need to understand your application. It just needs to accept the request, apply the rules you gave it, and either serve the response or pass the request upstream.

Nginx became popular because it handles several common infrastructure problems without much drama:

  • It handles many concurrent connections efficiently.
  • It serves static files quickly.
  • It's excellent as a reverse proxy.
  • It's good at shielding backend apps from awkward client behavior.
  • It centralizes TLS, redirects, headers, and access control.
  • It can load balance traffic without a separate appliance.
  • Its configuration is explicit and reviewable.

Nginx expects configuration in known files, not hidden per-directory overrides inside the document root. That makes behavior easier to inspect, version, test, and deploy.

VMware Workstation and Fusion 26H1 Release

VMware Workstation Pro and Fusion Pro 26H1 shipped recently and was a fairly quiet release.

The biggest change in Workstation Pro 26H1 is the move to a fully 64-bit architecture on Windows. Operating systems and hardware moved on from 32-bit limitations a long time ago, but parts of Workstation's underlying components were still dragging legacy dependencies forward.

The 26H1 release finally drops them. Installers, services, libraries, and application binaries now operate entirely in a 64-bit environment. It's a cleanup that eliminates years of technical debt and gets out of the way of heavy edge-case workloads on the Windows side.

In 26H1, you can finally see when a virtual machine was created and when it was last powered on.

If you manage a handful of virtual machines, you probably won't care. If you manage dozens, or maintain a sprawling cybersecurity lab, this is a massive quality-of-life fix. It's significantly easier to find dormant lab machines without having to boot them or guess based on filesystem modification dates.

The most impactful part of 26H1 isn't technical. Since Workstation Pro and Fusion Pro are now free for personal, educational, and commercial use, the economics of local virtualization are completely different. You don't have to justify a license request or fall back to an open-source alternative with fewer features. You can just download the premier desktop hypervisor and run your lab.

The 26H1 release includes a typical collection of bug fixes and security updates to improve platform stability. When you rely on virtual machines as part of your daily workflow, predictable execution matters more than new knobs to turn.

Disclaimer

This is not an official VMware by Broadcom document. This is a personal blog post.

The information is provided as-is with no warranties and confers no rights.

Please, refer to official documentation for the most up-to-date information.

How I'm Preparing for GH-600, GitHub's Agentic AI Developer Certification

GitHub Certification

When I published Branching Out: GitHub Certification Path, GitHub had five certifications. That list is already out of date, and so is my own exam roadmap.

GitHub has now added a sixth exam: GH-600, GitHub Certified: Agentic AI Developer (beta). As of June 6, 2026, this one is still in beta, and it fills a different role than the existing Copilot certification. The Copilot exam is about using GitHub's AI tooling well. GH-600 is about building, operating, constraining, and evaluating agents inside real software delivery workflows.

I'm treating this post as my preparation plan for GH-600. These are the official materials, docs, and hands-on exercises I'm using to get ready for it.

GitHub is treating agentic AI as an engineering discipline: tool access, MCP configuration, execution boundaries, memory, observability, evaluation, and guardrails. That's the right frame for the technology, and it's the reason this exam is worth taking seriously.

Versioning Documentation with ProperDocs and mike

If you're using ProperDocs, versioned publishing should be straightforward. Build the docs for a release, publish them under a stable version label, and leave older versions alone. Until mike v2.2.0, that simple workflow still carried a nagging question: did the rest of the tooling really understand ProperDocs, or was it still assuming MkDocs under the hood?

mike publishes each supported release as its own directory tree on a deployment branch, usually gh-pages. Old versions stay put even when today's main branch moves on. In the mike v2.2.0 release, that process got simpler for ProperDocs users: when the properdocs package is installed, mike runs properdocs instead of mkdocs. No separate workflow branch. No wrapper script. No extra switch to remember.

The release notes include:

Add support for ProperDocs.

In practice, that means mike now checks for the properdocs package first. When it's present, mike uses the properdocs command and includes properdocs.yml and properdocs.yaml in its default configuration file search order.

That is exactly the kind of compatibility work you want in a documentation pipeline. Install the generator you actually use, install mike, and let tool detection do the boring part correctly.

ProperDocs exists because some teams need the MkDocs 1.x operating model to keep working. They have plugins, theme overrides, snippets, macros, and workflow assumptions built around that ecosystem. Versioned publishing shouldn't force those teams into a second set of build logic.

Before this change, the friction wasn't that versioned docs were impossible. The friction was that ProperDocs users had to think too much about whether the surrounding tooling still assumed MkDocs. That extra uncertainty is how CI jobs grow unnecessary conditionals and side paths.

With mike v2.2.0, the command flow is really simple:

python -m pip install "properdocs" "mike>=2.2.0"
mike deploy 1.0 latest --push

PowerShell Command Naming: Approved Verbs, Clear Nouns, and Better APIs

PowerShell PowerShell

The difference between a PowerShell command that feels native and one that feels bolted on is often not the implementation. It's the name. In PowerShell, names are part of the interface contract: they drive discoverability, shape user expectations, and determine whether your module behaves like a first-class citizen or like a private script someone published by accident.

Official References

Start with the official documentation for approved verbs, Get-Verb, and the PSScriptAnalyzer rule UseApprovedVerbs.

If you maintain PowerShell modules long enough, you start to see the same mistakes over and over:

  • Functions named with verbs PowerShell doesn't recognize
  • Plural nouns where PowerShell expects singular object names
  • Verb choices that imply one behavior while the function performs another
  • "Do everything" verbs like Invoke used as a substitute for design
  • State-changing commands that don't support -WhatIf and -Confirm

This post is about avoiding those mistakes and building command names that feel idiomatic, predictable, and maintainable.

A bad command name spreads farther than you might expect. It shows up in documentation, examples, tab completion, issue reports, code review comments, CI logs, and muscle memory. Once users learn the wrong name, you either carry it forward forever or you break them later when you fix it.

PowerShell isn't just a shell. It's a command discovery environment. Users don't need to remember your entire module if your names fit the ecosystem. They can find commands through patterns:

Get-Command -Verb Get
Get-Command -Module My.Module
Get-Command -Noun VcfCluster
Get-Help Get-VcfCluster -Full
Get-Verb

That only works well when command names follow the conventions the engine, the help system, and the user all expect.

The best PowerShell command names are easy to guess. If I know your module has a cluster object, I should be able to guess that retrieval is probably Get-Thing, creation is probably New-Thing, mutation is probably Set-Thing, deletion is probably Remove-Thing, and validation is probably Test-Thing. If your module breaks that expectation, you increase the amount of documentation the user must read before they can do anything useful.

Maintainer Burnout in Open Source

Most of the internet runs on digital duct tape: small libraries, CLI tools, and frameworks held together by maintainers who never billed you for the install. Your CI pipeline, your container base image, and your app's transitive dependencies all assume someone will answer the issue, cut the release, and patch the CVE on a timeline that looks suspiciously like "when they have a free evening."

Maintainer burnout is what you get when that assumption keeps landing. It's not a bad week or a need for better time management. You still care about the project. You still know what a good fix looks like. You just can't get there anymore without paying a cost you didn't use to pay. The code might take twenty minutes. The replies take the rest of your life.

Why Maintainer Burnout Happens

The public story of open source is commits and stars. The private story is triage, judgment calls, release notes, duplicate issues, security advisories, and the careful tone you need when you close a thread in public. Reply-shaped debt piles up in that gap. That's where maintainers break.

Volume Without Staff

Growth rarely brings reviewers, security rotation, or funded operations. It brings more issues, more pull requests, more "quick questions," and more combinations you'll never reproduce on your laptop. A star is a bookmark for a user. For a maintainer, it's a rough guess at how many strangers will show up in your inbox next quarter.

Entitlement as Urgency

Many users interact with free software as if it were a paid product: instant fixes, roadmap influence, enterprise response times, and frustration when you say "not yet." You inherit on-call dread without an on-call team. Every open thread reads like it's due now. Only you know which ones can wait until Saturday and which ones turn into someone else's emergency if you don't move tonight.

Guides like Please Format Your Code Blocks and Writing Practical Contribution Guidelines exist because unclear intake isn't rudeness. It's unpaid labor you pay before the real work starts.

Value Created, Compensation Delayed (or Never)

Industry products ship on volunteer maintenance. Sponsorship logos are easier to obtain than guaranteed hours. Maintainers run a second shift after paid work, doing release engineering and community moderation for strangers while their own sleep debt compounds.

The industry takes a lot upstream and sends back gratitude, sometimes money, almost never enough hands on the work.

Isolation and Identity

Solo maintainers carry product, support, security, and governance in one GitHub avatar. When a co-maintainer changes jobs and stops reviewing, the bus factor becomes you. For many people, the project is also résumé, community, and the thing they're known for. Stepping back can feel like disappearing. Staying in can feel like you're drowning. Burnout sometimes looks like silence while you still watch the notification count, unable to reply and unable to walk away.

Using @ Imports in CLAUDE.md

If you already keep agent policy in AGENTS.md, Cursor rules, GitHub Copilot instruction paths, or a long README.md, you usually don't want a second copy that drifts out of sync just for Claude Code.

Claude Code loads a project's CLAUDE.md every session. @path/to/file lines in that file can point at other files. At session start, Claude Code resolves those paths relative to the CLAUDE.md that contains them and includes their contents with the rest of the file.

This provides you a bridge to resuse agent context: One body of markdown on disk, while each integration still uses its own documented entrypoint (CLAUDE.md here, Cursor rules there, Copilot's matrix elsewhere).