pythonsoaptutorialweb-services

How to Call a SOAP API from Python: zeep, requests, and suds Compared

SOAPless Team8 min read

Python developers hitting a SOAP API for the first time face an immediate question: which library should I use? The ecosystem offers several options, each with different tradeoffs around ease of use, WSDL support, and maintenance status.

This guide covers three approaches — zeep (the modern choice), requests with hand-built XML (maximum control), and suds-community (the legacy option) — with complete code examples, error handling patterns, and a clear recommendation for each scenario.

Quick Comparison

Featurezeeprequests + XMLsuds-community
WSDL parsingAutomaticManualAutomatic
Type safetyYes (from WSDL)NoYes (from WSDL)
Python 3 supportFullFullCommunity fork
MaintenanceActiveN/A (stdlib)Low activity
WS-SecurityPlugin supportManualLimited
Learning curveLowHighLow
Complex typesHandled automaticallyManual XML constructionHandled automatically
Best forMost projectsSimple one-off callsLegacy codebases

zeep is the most widely used Python SOAP library. It parses the WSDL, generates Python objects for all types, and handles XML serialization and deserialization automatically.

Installation

pip install zeep

Basic Usage

from zeep import Client

# Initialize client from WSDL URL
client = Client("https://weather.example.com/soap?wsdl")

# Call an operation — zeep handles XML envelope construction
result = client.service.GetCurrentWeather(
    city="Tokyo",
    countryCode="JP"
)

# Result is a Python object, not XML
print(f"Temperature: {result.temperature}°{result.unit}")
print(f"Condition: {result.condition}")
print(f"Humidity: {result.humidity}%")

zeep reads the WSDL, discovers the GetCurrentWeather operation, constructs the proper SOAP envelope with correct namespaces, sends the HTTP request, parses the XML response, and returns a Python object. All in three lines of code.

Handling Complex Types

When a SOAP operation returns nested or repeated structures, zeep maps them to Python objects:

client = Client("https://weather.example.com/soap?wsdl")

# GetForecast returns a list of ForecastDay objects
result = client.service.GetForecast(city="Tokyo", days=5)

for day in result.forecasts:
    print(f"{day.date}: {day.low}°-{day.high}° ({day.condition})")

To create complex input types, use zeep's type factory:

# Create a complex type instance
factory = client.type_factory("ns0")

address = factory.Address(
    street="1-1 Marunouchi",
    city="Tokyo",
    postalCode="100-0005",
    country="JP"
)

result = client.service.CreateOrder(
    customerId="CUST-001",
    shippingAddress=address,
    items=[
        factory.OrderItem(productId="PROD-A", quantity=2),
        factory.OrderItem(productId="PROD-B", quantity=1),
    ]
)

WS-Security Authentication

from zeep import Client
from zeep.wsse.username import UsernameToken

# Username/password authentication
client = Client(
    "https://secure.example.com/soap?wsdl",
    wsse=UsernameToken("api_user", "api_password")
)

result = client.service.GetAccountBalance(accountId="ACC-12345")

For timestamp-based tokens:

from zeep.wsse.username import UsernameToken
from datetime import timedelta

client = Client(
    "https://secure.example.com/soap?wsdl",
    wsse=UsernameToken(
        "api_user",
        "api_password",
        use_digest=True,
        timestamp_token=True,
        timestamp_freshness=timedelta(minutes=5)
    )
)

Error Handling

from zeep import Client
from zeep.exceptions import Fault, TransportError, ValidationError
from requests.exceptions import ConnectionError, Timeout

client = Client("https://weather.example.com/soap?wsdl")

try:
    result = client.service.GetCurrentWeather(city="Tokyo")

except Fault as e:
    # SOAP Fault — the server returned a structured error
    print(f"SOAP Fault: {e.message}")
    print(f"Fault code: {e.code}")
    if e.detail is not None:
        print(f"Detail: {e.detail}")

except TransportError as e:
    # HTTP-level error (4xx, 5xx)
    print(f"Transport error: {e.status_code} - {e.message}")

except ValidationError as e:
    # Input validation failed against WSDL schema
    print(f"Validation error: {e}")

except ConnectionError:
    print("Could not connect to the SOAP service")

except Timeout:
    print("Request timed out")

Transport Configuration

from zeep import Client
from zeep.transports import Transport
from requests import Session

session = Session()
session.timeout = 30  # seconds
session.verify = "/path/to/ca-bundle.crt"  # custom CA
session.headers.update({"User-Agent": "MyApp/1.0"})

# HTTP proxy
session.proxies = {
    "https": "http://proxy.example.com:8080"
}

transport = Transport(session=session)
client = Client(
    "https://weather.example.com/soap?wsdl",
    transport=transport
)

Approach 2: requests with Manual XML

When you need to call a single SOAP operation without installing a SOAP-specific library, or when the WSDL is unavailable or broken, you can construct the XML envelope manually using Python's requests library.

Basic Usage

import requests
import xml.etree.ElementTree as ET

url = "https://weather.example.com/soap"

soap_envelope = """<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:tns="http://example.com/weatherservice">
  <soap:Body>
    <tns:GetCurrentWeather>
      <tns:city>Tokyo</tns:city>
      <tns:countryCode>JP</tns:countryCode>
    </tns:GetCurrentWeather>
  </soap:Body>
</soap:Envelope>"""

headers = {
    "Content-Type": "text/xml; charset=utf-8",
    "SOAPAction": "http://example.com/weatherservice/GetCurrentWeather",
}

response = requests.post(url, data=soap_envelope, headers=headers)
response.raise_for_status()

# Parse the XML response
root = ET.fromstring(response.content)

# Define namespaces for XPath queries
ns = {
    "soap": "http://schemas.xmlsoap.org/soap/envelope/",
    "tns": "http://example.com/weatherservice",
}

body = root.find(".//soap:Body", ns)
temp = body.find(".//tns:temperature", ns)
condition = body.find(".//tns:condition", ns)

print(f"Temperature: {temp.text}")
print(f"Condition: {condition.text}")

Parameterized Requests

To avoid XML injection vulnerabilities, never use f-strings or format() to insert user input into XML. Use xml.etree.ElementTree to build the envelope programmatically:

import requests
import xml.etree.ElementTree as ET

def build_get_weather_envelope(city: str, country_code: str = None) -> str:
    SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"
    TNS = "http://example.com/weatherservice"

    ET.register_namespace("soap", SOAP_NS)
    ET.register_namespace("tns", TNS)

    envelope = ET.Element(f"{{{SOAP_NS}}}Envelope")
    body = ET.SubElement(envelope, f"{{{SOAP_NS}}}Body")
    operation = ET.SubElement(body, f"{{{TNS}}}GetCurrentWeather")

    city_el = ET.SubElement(operation, f"{{{TNS}}}city")
    city_el.text = city

    if country_code:
        cc_el = ET.SubElement(operation, f"{{{TNS}}}countryCode")
        cc_el.text = country_code

    return ET.tostring(envelope, encoding="unicode", xml_declaration=True)


envelope = build_get_weather_envelope("Tokyo", "JP")

response = requests.post(
    "https://weather.example.com/soap",
    data=envelope.encode("utf-8"),
    headers={
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": "http://example.com/weatherservice/GetCurrentWeather",
    },
    timeout=30
)

SOAP Fault Detection

def check_soap_fault(response_content: bytes) -> None:
    """Raise an exception if the response contains a SOAP Fault."""
    root = ET.fromstring(response_content)
    ns = {"soap": "http://schemas.xmlsoap.org/soap/envelope/"}

    fault = root.find(".//soap:Fault", ns)
    if fault is not None:
        code = fault.findtext("faultcode", default="Unknown")
        message = fault.findtext("faultstring", default="Unknown error")
        detail = fault.find("detail")
        detail_text = ET.tostring(detail, encoding="unicode") if detail is not None else None
        raise Exception(
            f"SOAP Fault [{code}]: {message}"
            + (f"\nDetail: {detail_text}" if detail_text else "")
        )

# Usage
response = requests.post(url, data=envelope, headers=headers, timeout=30)
check_soap_fault(response.content)

Approach 3: suds-community

suds-community is a community-maintained fork of the original suds library. It provides a similar experience to zeep but with less active maintenance.

Installation

pip install suds-community

Basic Usage

from suds.client import Client

client = Client("https://weather.example.com/soap?wsdl")

# List available operations
for service in client.wsdl.services:
    for port in service.ports:
        for method in port.methods.values():
            print(f"  {method.name}")

# Call an operation
result = client.service.GetCurrentWeather(city="Tokyo", countryCode="JP")

print(f"Temperature: {result.temperature}")
print(f"Condition: {result.condition}")

When to Use suds

suds-community is primarily useful when:

  • You are maintaining a codebase that already uses suds
  • You need compatibility with Python 2 code being migrated
  • The WSDL has quirks that zeep does not handle (rare, but it happens)

For new projects, zeep is the better choice due to its more active maintenance, better Python 3 support, and stronger WS-Security capabilities.

Performance Comparison

For a typical SOAP call to a weather service, here are approximate timings:

Phasezeeprequests + XMLsuds
WSDL parsing (first call)~200-500msN/A~300-800ms
Request construction~1-5ms~0.1-0.5ms~2-8ms
Network round-tripSameSameSame
Response parsing~2-10ms~1-5ms~3-15ms

The raw requests approach is fastest because it skips WSDL parsing entirely. However, zeep and suds cache the parsed WSDL, so the initial parsing cost is amortized over subsequent calls.

For zeep, you can cache the WSDL parsing across application restarts:

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

transport = Transport(cache=SqliteCache(path="/tmp/zeep_cache.db", timeout=86400))
client = Client("https://weather.example.com/soap?wsdl", transport=transport)

Best Practices

1. Cache the WSDL

WSDL files rarely change. Parse them once and reuse the client object:

# Good — create client once
client = Client("https://service.example.com/soap?wsdl")

def get_weather(city: str):
    return client.service.GetCurrentWeather(city=city)

# Bad — parsing WSDL on every call
def get_weather(city: str):
    client = Client("https://service.example.com/soap?wsdl")
    return client.service.GetCurrentWeather(city=city)

2. Set Timeouts

SOAP services can be slow. Always set explicit timeouts:

from zeep.transports import Transport
from requests import Session

session = Session()
session.timeout = 30

transport = Transport(session=session)
client = Client(wsdl_url, transport=transport)

3. Log Raw XML for Debugging

When something goes wrong, seeing the actual SOAP XML is invaluable:

import logging

logging.getLogger("zeep.transports").setLevel(logging.DEBUG)

4. Handle SOAP Faults Specifically

Do not catch generic exceptions. SOAP Faults carry structured error information:

from zeep.exceptions import Fault

try:
    result = client.service.ProcessPayment(amount=100)
except Fault as e:
    if "InsufficientFunds" in str(e.detail):
        handle_insufficient_funds()
    elif e.code == "soap:Server":
        retry_later()
    else:
        raise

Decision Guide

Use this flowchart to pick the right approach:

Do you need to call multiple SOAP operations?
├── Yes → Is the WSDL available and valid?
│         ├── Yes → Use zeep
│         └── No  → Use requests + XML, read the docs/examples
└── No  → Is this a one-off script?
          ├── Yes → Use requests + XML (fewer dependencies)
          └── No  → Use zeep (better long-term maintainability)

How SOAPless Helps

All three approaches require your Python code to handle SOAP-specific concerns: WSDL parsing, XML envelope construction, namespace management, and SOAP Fault handling. As the number of SOAP services grows, this complexity multiplies.

SOAPless eliminates the SOAP layer from your Python code entirely. Instead of using zeep or building XML by hand, you call a standard REST endpoint:

import requests

response = requests.post(
    "https://api.soapless.com/v1/your-service/GetCurrentWeather",
    headers={
        "X-API-Key": "your_api_key",
        "Content-Type": "application/json",
    },
    json={"city": "Tokyo", "countryCode": "JP"},
    timeout=30
)

data = response.json()
print(f"Temperature: {data['temperature']}°{data['unit']}")

No zeep, no XML, no namespaces, no SOAP Faults to parse. SOAPless handles the WSDL parsing, XML conversion, and SOAP communication on the server side, with AES-256-GCM encrypted credential management and an auto-generated OpenAPI 3.0 spec that works with any HTTP client library — not just Python-specific SOAP tools.