zeep
zeep is the de facto Python SOAP client. You point it at a WSDL URL, it downloads and parses the whole contract with lxml, and it hands you a service proxy where every SOAP operation is a plain Python method call: client.service.GetQuote(symbol='AAPL'). It converts between Python values and XML for you, including nested complex types, dates via isodate, SOAP 1.1 and 1.2 bindings, WS-Addressing headers, and WS-Security (UsernameToken and x.509 signing). Transport rides on requests by default, with an httpx-based AsyncClient behind the async extra. It exists because every older Python SOAP library (suds, SOAPpy, pysimplesoap) rotted; zeep replaced them all.
If Python has to speak SOAP, this is the answer, and it works well for a protocol nobody enjoys. Just know you are adopting finished software in maintenance mode with a large open-issue backlog, and if the vendor offers REST, take the REST.
Use it if
- You have to integrate with an enterprise or government SOAP service (payment processors, logistics carriers, banks, ERP systems) and want operations as typed Python calls instead of hand-built XML envelopes
- The service uses the parts of SOAP that hurt to hand-roll: WS-Security UsernameToken or x.509 signatures, SOAP 1.2, WS-Addressing, multi-namespace schemas with imported XSDs
- You need to explore an unknown WSDL fast: python -m zeep <url> dumps every operation signature and type before you write a line of code
- You are stuck on suds or SOAPpy in a legacy codebase; zeep is the maintained path off both and the migration is mostly mechanical
- You expect active development: the README states plainly that the maintainer considers it stable and it will not be updated much since SOAP itself is frozen. The tracker holds 468 open issues and PRs (84 of those are open PRs, some sitting for years), and releases now land roughly once a year, with a two-year gap between 4.2.1 (Nov 2022) and 4.3.0 (Oct 2024). Bugs get fixed when a good PR shows up, not on a schedule
- The provider also offers a REST or JSON API: zeep drags in lxml, requests, requests-toolbelt, requests-file, attrs, isodate, and platformdirs to do what one requests call would
- The SOAP server is badly non-compliant: zeep is strict by design, and while Settings(strict=False) exists, the docs themselves warn it can silently lose data, so fighting a sloppy .NET or PHP endpoint can eat days
- You want a fully async stack: AsyncClient still loads the WSDL synchronously (documented, unfixed), so client construction blocks the event loop no matter what
Setup reality
pip install zeep usually just works because lxml ships wheels for mainstream platforms; on Alpine or unusual architectures lxml compiles and wants libxml2 and libxslt headers, which is the classic slim-Docker surprise. 4.3.3 requires Python 3.10 or newer. The async client is an extra (pip install zeep[async] for httpx) and x.509 signing is another (zeep[xmlsec], which has its own native build pain). The operational gotcha: Client() fetches and parses the WSDL plus every imported XSD on construction with a 300 second default timeout and no cache, so a naive client-per-request design is painfully slow until you add SqliteCache and reuse one client. Old tutorials also bite: plenty of snippets on Stack Overflow still show suds or zeep 2.x APIs.
Patterns
Create a client from a WSDLclient-from-wsdl
from zeep import Client
client = Client("https://example.com/service?wsdl")
result = client.service.GetQuote(symbol="AAPL")Client() downloads and parses the WSDL plus every imported XSD at construction time, with a 300 second default timeout for that fetch. Build one client at startup and reuse it; a client per request is the classic zeep performance bug.
Call operations with kwargs, dicts, and listscall-operation
result = client.service.CreateOrder(
customerId=42,
items=[{"sku": "A-1", "qty": 2}],
)
# operation names that clash with Python syntax:
op = client.service["Get.Balance"]
result = op(accountId=7)Keyword names must match the element names in the WSDL, case included. Plain dicts and lists are accepted wherever the schema expects complex types and arrays, so you rarely need factory objects for simple services.
Dump a WSDL from the command lineinspect-wsdl-cli
python -m zeep https://example.com/service?wsdlPrints the namespace prefixes, global elements, bindings, and the full signature of every operation. Do this before writing any code; it is where you learn the ns0-style prefixes that get_type and type_factory need.
HTTP Basic auth via a requests Sessionhttp-basic-auth
from requests import Session
from requests.auth import HTTPBasicAuth
from zeep import Client
from zeep.transports import Transport
session = Session()
session.auth = HTTPBasicAuth("user", "pass")
client = Client(
"https://example.com/service?wsdl",
transport=Transport(session=session),
)The session is also used for the WSDL fetch, which matters when the WSDL itself sits behind auth. The same hook carries client certificates (session.cert), proxies (session.proxies), and private CA bundles (session.verify).
WS-Security UsernameTokenwsse-usernametoken
from zeep import Client
from zeep.wsse.username import UsernameToken
client = Client(
"https://example.com/service?wsdl",
wsse=UsernameToken("user", "pass", use_digest=True),
)This puts credentials in the SOAP envelope header, not the HTTP layer; enterprise services often demand both at once, which zeep supports (wsse plus a session with auth). Drop use_digest=True if the server wants PasswordText. x.509 signing needs the zeep[xmlsec] extra.
Set real timeouts and cache the WSDLtransport-timeouts-cache
from zeep import Client
from zeep.cache import SqliteCache
from zeep.transports import Transport
transport = Transport(
cache=SqliteCache(),
timeout=10, # WSDL/XSD document loading
operation_timeout=30, # actual SOAP calls
)
client = Client("https://example.com/service?wsdl", transport=transport)The two timeouts trip people up: timeout only covers document loading (default 300s), while operation_timeout covers the calls and defaults to None, meaning a hung server hangs your code forever. SqliteCache keeps WSDL and XSD for 1 hour; there is no caching at all by default.
Build complex types with the factorycomplex-type-construction
factory = client.type_factory("ns0")
address = factory.Address(street="Main St 1", city="Springfield")
Order = client.get_type("ns0:Order")
order = Order(id=1, shipTo=address)
client.service.SubmitOrder(order=order)The ns0 prefix comes from the python -m zeep dump, not from the WSDL text. Factories beat nested dicts when the schema uses xsd:choice or deep nesting, because you get an error at construction instead of a vague validation failure at call time.
Capture sent and received XML with HistoryPlugincapture-raw-xml-history
from lxml import etree
from zeep import Client
from zeep.plugins import HistoryPlugin
history = HistoryPlugin()
client = Client("https://example.com/service?wsdl", plugins=[history])
client.service.Ping()
print(etree.tostring(history.last_sent["envelope"], pretty_print=True).decode())
print(etree.tostring(history.last_received["envelope"], pretty_print=True).decode())This is what you reach for when the vendor's support desk asks for the exact request XML. The plugin keeps only the last exchange by default (maxlen=1). Scrub the envelope before pasting it into a ticket if it carries a UsernameToken.
Survive a non-compliant server with Settingslenient-parsing-settings
from zeep import Client, Settings
settings = Settings(strict=False, xml_huge_tree=True)
client = Client("https://example.com/service?wsdl", settings=settings)
with client.settings(raw_response=True):
resp = client.service.Ping() # requests.Response, XML untouchedstrict=False parses in recover mode and tolerates missing required elements; the docs call it a last resort because it can silently drop data. xml_huge_tree lifts lxml's depth and size limits for oversized responses. The context manager scopes any setting to one call.
Async calls with AsyncClientasync-client
import zeep
client = zeep.AsyncClient("https://example.com/service?wsdl")
result = await client.service.Ping()
await client.transport.aclose()Needs pip install zeep[async] to pull httpx. The documented caveat: the WSDL is still loaded with synchronous requests even here, so construct the client at application startup, never inside an async request handler.
Turn a response object into plain dictsserialize-response
from zeep import helpers
result = client.service.GetReport(reportId=1)
data = helpers.serialize_object(result, dict)Zeep returns lxml-backed objects that json.dumps rejects outright. serialize_object converts recursively, but xsd date and dateTime fields become Python datetime objects via isodate, so JSON output still needs a default= encoder for those.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| suds | PyPI | A legacy codebase already deep in suds idioms; the community fork on PyPI keeps it alive, though zeep is the better target if you can afford the port |
| requests | PyPI | You only call one or two simple operations: posting a hand-written envelope template and parsing the response is fewer moving parts than a full WSDL client |
| spyne | PyPI | You need to serve SOAP, not consume it; zeep is client-only |