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
iterparsefor streaming if needed. - Namespace bugs — wrong namespace alias when constructing types. Read the WSDL’s
xmlnsdeclarations 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. libxmlsec1missing — WS-Security signing fails to install. OS package manager:apt install libxmlsec1-dev(Debian) orbrew 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/ingresslog 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, catchrequests.exceptions.RequestExceptionseparately. - “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.