backend / protocols / soap / 04_python_libraries.md

Python SOAP Libraries

6 interview angles 5 min read source

Python SOAP Libraries

For consuming SOAP services (the 99% case), Zeep is the only reasonable choice. For implementing SOAP servers in Python, options are slim — usually you wouldn’t.

Zeep — the SOAP client

pip install zeep

From a WSDL URL, Zeep generates a client at runtime:

from zeep import Client

client = Client("https://api.example.com/UserService?wsdl")

# Inspect what's available
client.wsdl.dump()                                     # full WSDL summary
print(client.service.__class__.__name__)               # ServiceProxy

# Call an operation
response = client.service.GetUser(Id=42)
print(response.Name, response.Email)

Zeep handles:

  • Parsing the WSDL.
  • Generating Python representations of XSD types.
  • Marshalling Python dicts/objects to XML envelopes.
  • Sending over HTTP.
  • Parsing the response XML back to Python objects.
  • Translating SOAP Faults to Python exceptions.

You write Python; Zeep handles the XML.

Authentication

HTTP Basic / Digest

from requests import Session
from requests.auth import HTTPBasicAuth
from zeep.transports import Transport

session = Session()
session.auth = HTTPBasicAuth("alice", "secret")
client = Client("https://api.example.com/UserService?wsdl", transport=Transport(session=session))

TLS client certificate (mTLS)

session = Session()
session.cert = ("/path/to/client.crt", "/path/to/client.key")
client = Client(WSDL_URL, transport=Transport(session=session))

WS-Security UsernameToken

from zeep.wsse.username import UsernameToken

client = Client(
    WSDL_URL,
    wsse=UsernameToken("alice", "secret"),
)

Adds the <wsse:UsernameToken> to the Security header automatically.

WS-Security with signatures

For signed messages (X.509 + sign body):

from zeep.wsse.signature import Signature

client = Client(
    WSDL_URL,
    wsse=Signature(
        private_key_filename="/path/to/client.key",
        public_key_filename="/path/to/client.crt",
        password=None,
    ),
)

Requires cryptography and xmlsec system libs. The latter is a C library; install via OS package manager (apt install libxmlsec1-dev, brew install libxmlsec1).

Combining auth methods

from zeep.wsse import UsernameToken, BinarySignature, Signature

client = Client(
    WSDL_URL,
    wsse=[
        UsernameToken("alice", "secret"),
        BinarySignature("/key.pem", "/cert.pem"),
    ],
)

WS-Security supports stacked tokens; Zeep handles it.

Type handling

# Operation expects a complex type
response = client.service.CreateUser({
    "Name": "Alice",
    "Email": "a@b.com",
    "Phone": "555-1234",
})

For nested types, use dicts:

response = client.service.CreateOrder({
    "Customer": {"Id": 42, "Name": "Alice"},
    "Items": [
        {"ProductId": 100, "Quantity": 2},
        {"ProductId": 200, "Quantity": 1},
    ],
})

Zeep matches against the XSD. Wrong field name = error before sending.

For explicit type construction:

User = client.get_type("ns0:User")
user = User(Name="Alice", Email="a@b.com")
response = client.service.CreateUser(user)

ns0 is the namespace alias from the WSDL. Zeep’s wsdl.dump() lists them.

Inspecting the WSDL

# All operations
for service in client.wsdl.services.values():
    for port in service.ports.values():
        for operation in port.binding._operations.values():
            print(operation.name)

# All complex types
print(client.wsdl.types.types)

For exploration: client.wsdl.dump() writes a human-readable summary. For programmatic discovery: walk the dicts.

Error handling

from zeep.exceptions import Fault

try:
    response = client.service.GetUser(Id=99999)
except Fault as e:
    print(f"SOAP Fault: {e.message}")
    print(f"Code: {e.code}")
    print(f"Detail: {e.detail}")

Fault.detail is the raw <detail> element (lxml _Element). Parse manually if you need structured info.

For network/HTTP errors, the underlying requests exceptions surface:

from requests.exceptions import RequestException

try:
    response = client.service.GetUser(Id=42)
except Fault as soap_error:
    ...
except RequestException as network_error:
    ...

Caching the WSDL

By default Zeep fetches the WSDL on every Client(...) construction. Cache it:

from zeep.cache import SqliteCache
from zeep.transports import Transport

transport = Transport(cache=SqliteCache(path="/tmp/zeep.db", timeout=3600))
client = Client(WSDL_URL, transport=transport)

Or pin to a local file:

client = Client("/path/to/local.wsdl")

For production: cache aggressively. WSDLs rarely change between deploys, and parsing is slow.

Debugging — see the raw XML

from zeep import Plugin
from lxml import etree

class LoggingPlugin(Plugin):
    def egress(self, envelope, http_headers, operation, binding_options):
        print("REQUEST:")
        print(etree.tostring(envelope, pretty_print=True).decode())
        return envelope, http_headers

    def ingress(self, envelope, http_headers, operation):
        print("RESPONSE:")
        print(etree.tostring(envelope, pretty_print=True).decode())
        return envelope, http_headers

client = Client(WSDL_URL, plugins=[LoggingPlugin()])

Essential when debugging “the server says my message is invalid but I can’t tell why.” See the actual XML.

Async — zeep.asyncio (sort of)

Zeep has an async transport but it’s not as polished as the sync one. For async-heavy apps:

from zeep.asyncio import AsyncTransport
from zeep import AsyncClient

async with AsyncClient(WSDL_URL, transport=AsyncTransport()) as client:
    response = await client.service.GetUser(Id=42)

Use only if you need async. For most SOAP integrations, sync + thread pool is simpler.

Server-side — spyne

If you must implement a SOAP server in Python:

pip install spyne
from spyne import Application, rpc, ServiceBase, Integer, Unicode
from spyne.protocol.soap import Soap11
from spyne.server.wsgi import WsgiApplication

class UserService(ServiceBase):
    @rpc(Integer, _returns=Unicode)
    def get_user_name(ctx, user_id):
        return f"User {user_id}"

application = Application(
    [UserService],
    tns="https://example.com/user/v1",
    in_protocol=Soap11(),
    out_protocol=Soap11(),
)
wsgi_app = WsgiApplication(application)

Run via gunicorn or any WSGI server. Spyne auto-generates the WSDL.

Reality check: building new SOAP servers in Python in 2025+ is rare. Spyne is the most mature option but not actively developed.

When to wrap SOAP behind REST

A common pattern: existing SOAP service → modern REST/GraphQL clients. Build a Python “adapter”:

from fastapi import FastAPI
from zeep import Client

app = FastAPI()
soap_client = Client(LEGACY_WSDL_URL)

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = soap_client.service.GetUser(Id=user_id)
    return {"id": user.Id, "name": user.Name, "email": user.Email}

Frontend / mobile / partner clients talk REST/JSON; internally you call the SOAP service. Pays off when SOAP can’t be changed but you need a modern API surface.

Common pitfalls

  • WSDL fetched on every startup without caching — slow boot times.
  • XML parser eating large messages — for SOAP responses >1 MB, lxml memory use can be significant. Use iterparse for streaming if needed.
  • Namespace bugs — wrong namespace alias when constructing types. Read the WSDL’s xmlns declarations carefully.
  • Date parsing surprises — XSD dateTime can have timezone or not; Zeep handles most cases but verify.
  • Service-side errors that aren’t SOAP Faults — old servers may return HTTP 200 with an error in the body, not a proper <Fault>. Inspect response and add custom handling.
  • libxmlsec1 missing — WS-Security signing fails to install. OS package manager: apt install libxmlsec1-dev (Debian) or brew install libxmlsec1 (macOS).

Common interview confusions

  • “Zeep generates code from WSDL at install time.” — at runtime, when you instantiate Client(). There’s no codegen step.
  • “Zeep is async.” — sync by default; has async transport but less battle-tested.
  • “You can’t do SOAP without lots of XML config.” — Zeep hides the XML almost completely for simple cases.

Interview angle

  • “How would you call a SOAP service from Python?”pip install zeep; client = zeep.Client(WSDL_URL); client.service.OperationName(args). Zeep generates the client from the WSDL at runtime.
  • “How do you do WS-Security with Zeep?”client = Client(WSDL_URL, wsse=UsernameToken("user", "pass")) for username token; Signature(private_key, cert) for X.509 signing. Multiple WSSE plugins can stack.
  • “How do you debug SOAP messages?” — Zeep plugins on egress/ingress log the XML envelopes. client.wsdl.dump() summarizes the contract.
  • “How do you handle SOAP Faults in Python?”try/except zeep.exceptions.Fault. The exception has .message, .code, .detail. For network errors, catch requests.exceptions.RequestException separately.
  • “How would you modernize an existing SOAP service?” — wrap it with a thin REST/GraphQL adapter (Python FastAPI calling Zeep underneath). New clients use the modern API; the SOAP service stays as-is.
  • “Why is Zeep the only real SOAP client option in Python?” — older libs (suds, pysimplesoap) are unmaintained or have limited WSDL support. Zeep is actively developed and covers most real-world SOAP services.