SOAP with Python objects

Dot access, attributes, lists and namespaces - SOAP messages as plain Python objects with no schemas or WSDL.

Every SOAP request you send and every response you receive is a SOAPMessage - a dynamic Python object where attributes are XML elements. You never declare its shape upfront, you never touch a schema, and no code is ever generated from a WSDL.

from zato.common.soap.message import SOAPMessage

request = SOAPMessage()
request.namespace = 'urn:cdc:iisb:2014'
request.hl7Message = 'MSH|^~\\&|...'

That is a complete request body. The rules below are the whole contract.

The rules

1. Dot access creates elements

Assigning to an attribute creates an element. Intermediate elements spring into existence as you go deeper - there is nothing to declare first:

request = SOAPMessage()
request.order.customer.name = 'Jane Smith'
request.order.customer.city = 'Geneva'
<order>
    <customer>
        <name>Jane Smith</name>
        <city>Geneva</city>
    </customer>
</order>

2. Assignment order is wire order

Elements appear on the wire in exactly the order you assigned them. Nothing is reordered, which matters because many SOAP endpoints validate element order strictly - if the documentation of a system says username comes before password, assign them in that order and that is the order they will have.

3. Brackets are attributes

Brackets set and read XML attributes:

request.slot['name'] = 'creationTime'
request.slot.value = '2026-01-15T10:30:00'
<slot name="creationTime">
    <value>2026-01-15T10:30:00</value>
</slot>

One bracket key is reserved: namespace. Setting message.namespace picks the default namespace for the whole message, while setting it on a node overrides the namespace from that node down:

request = SOAPMessage()
request.namespace = 'urn:example:orders'

# This one subtree uses a different namespace
request.metadata['namespace'] = 'urn:example:common'
request.metadata.created_by = 'api'

4. Lists are repeated elements

Assigning a list produces one element per item - both for scalars and for nested SOAPMessage nodes:

line1 = SOAPMessage()
line1.product = 'AA-100'
line1.quantity = 3

line2 = SOAPMessage()
line2.product = 'BB-200'
line2.quantity = 1

request.order.lines.line = [line1, line2]
<order>
    <lines>
        <line><product>AA-100</product><quantity>3</quantity></line>
        <line><product>BB-200</product><quantity>1</quantity></line>
    </lines>
</order>

5. Python types choose the XML form

The type of the value dictates its lexical form on the wire - you assign natural Python values and the platform writes what the receiving system expects:

You assignThe wire receives
strThe text as-is
int, float, DecimalThe number's canonical form
booltrue or false
datetimeAn xs:dateTime timestamp
dateAn xs:date value
NoneAn xsi:nil element
bytesAn MTOM part when the connection has MTOM enabled, inline base64 otherwise

6. Reading mirrors writing

Responses come back as the same kind of object, so reading is dot access again - repeated elements are lists, xsi:nil reads as None, and you never need to know or spell out the namespaces of the reply:

response = conn.invoke('getOrder', request)

order = response.getOrderResponse.order
status = order.status

for line in order.lines.line:
    self.logger.info('%s x %s', line.product, line.quantity)

An element that has attributes is always a node - its attributes read through brackets and its text through str():

# <status code="42">Accepted</status>
code = response.status['code']   # '42'
text = str(response.status)      # 'Accepted'

7. Responses include their protocol context

Two reserved attributes ride along on responses when the connection uses the matching blocks:

  • response.addressing - the reply's WS-Addressing headers: action, message_id, relates_to
  • response.attachments - the reply's MTOM parts, each with content_id, content_type and data bytes

Faults are exceptions

A SOAP fault of either version surfaces as a single exception type, SOAPFault, raised before your code sees any part of the response:

from zato.common.soap.common import SOAPFault

try:
    response = conn.invoke('submitSingleMessage', request)
except SOAPFault as fault:
    self.logger.warning('Rejected: %s %s', fault.code, fault.reason)

fault.code and fault.reason are strings and fault.detail is a dot-accessed SOAPMessage holding whatever the endpoint put in the fault's detail section.

Learn more