mrkeyoor.com_
Thu 06 Aug 23:57 UTC
PyPIUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability5/5Client, the service proxy, Transport, Settings, and the wsse classes have looked the same across the whole 4.x line (since 2020), and the maintainer explicitly declares the library stable and feature-frozen. The only recent break is platform support: 4.3.x dropped Python 3.7/3.8 and 4.3.3 requires 3.10+.
Docs4/5docs.python-zeep.org is a real docs site covering transports, settings, WSSE, plugins, and datastructures, and it is honest about limits like the sync WSDL load in AsyncClient. It is aging with the project though: examples still point at webservicex.net endpoints that died years ago.
Maintenance3/5Alive but explicitly in maintenance mode: last push June 2026 and a 4.3.3 release the same month, but the README says it will not be updated much, 468 issues and PRs sit open (84 open PRs), and 4.2.1 to 4.3.0 took two years. One primary maintainer, corporate sponsorship from Kraken Tech.
Ecosystem4/5About 10M weekly downloads and 2,006 stars; it is the default answer for Python SOAP since suds and SOAPpy died, and every carrier or bank integration guide for Python assumes it. The docked point is the ecosystem it serves: SOAP itself is shrinking, so tooling around zeep grows slowly.

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
Skip it if

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?wsdl

Prints 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 untouched

strict=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

PackageRegistryPick it when
sudsPyPIA 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
requestsPyPIYou 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
spynePyPIYou need to serve SOAP, not consume it; zeep is client-only