2025, Oct 16 08:00

How to fix ElementTree TypeError 'cannot serialize int' when building JUnit-style XML in Python

Learn why xml.etree.ElementTree raises TypeError: cannot serialize int for JUnit-style XML and how to fix it by casting numeric attributes to strings. Fast fix.

When building JUnit-style XML in Python with xml.etree, a deceptively simple detail can break serialization: attribute values must be strings. If any attribute is an int, the write step fails with a TypeError. Below is a walkthrough of the exact failure mode and the minimal fix that restores XML generation.

Problem setup

The goal is to produce an XML structure like this:

<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="65" failures="0" disabled="0" errors="0" name="AllTests">
  <testsuite name="Tests" tests="65" failures="0" disabled="0" skipped="0" errors="0">
    <testcase name="TestInit" file="" status="run" result="completed" />
    <testcase name="TestAlways" file="" status="run" result="completed" />
  </testsuite>
  ...
</testsuites>

Here is a Python snippet that constructs the XML from in-memory objects and then writes it to disk. It demonstrates the failure-producing pattern.

import xml.etree.cElementTree as ET

class CaseNode:
    def __init__(self, label, outcome):
        self.name = label
        self.result = outcome
    
    def __str__(self):
        return f"Test Case: {self.name}, Status: {self.result}"

class SuiteNode:
    def __init__(self, title):
        self.name = title
        self.test_case_list = []

    def __str__(self):
        return f"Test Suite: {self.name}"

def run():
    print("Hello from html-test-report-generator!")
    suites = build_html_report()
    for s in suites:
        print(s)
        for c in s.test_case_list:
            print(c)
        print()
    write_xml_report(suites)

def build_html_report():
    # generates a list of SuiteNode objects and returns the list back to the caller
    pass

def write_xml_report(suites):
    root_suites = ET.Element(
        "testsuites",
        tests=len(suites),          # <-- problematic: int attribute
        failures="0",
        disabled="0",
        errors="0",
        name="AllTests",
    )
    for s in suites:
        suite_node = ET.SubElement(
            root_suites,
            "testsuite",
            name=s.name,
            tests=len(s.test_case_list),  # <-- problematic: int attribute
            failures="0",
            disabled="0",
            skipped="0",
            errors="0",
        )
        for c in s.test_case_list:
            ET.SubElement(
                suite_node,
                "testcase",
                name=c.name,
                file="",
                line="",
                status="run",
                result=c.result,
            )
    tree = ET.ElementTree(root_suites)
    tree.write("filename.xml")

Running this code leads to an exception during serialization:

TypeError: cannot serialize 23 (type int)

It can also be accompanied by a message like:

AttributeError: 'str' object has no attribute 'write'

The latter appears because the write operation is attempting to serialize an invalid tree and bubbles up through internal handling. The actionable problem is the TypeError: ElementTree refuses to serialize an integer attribute.

What actually goes wrong

In xml.etree, element attributes are strings. When you pass a Python int as an attribute value (for example, tests=len(...)), the serializer can’t convert it implicitly and raises TypeError: cannot serialize 23 (type int). The failure occurs at write time, when ElementTree flattens the in-memory structure into bytes. Because the tree isn’t valid according to its own rules, serialization stops.

You can confirm this with a minimal reproducer that mirrors the same failure:

import xml.etree.cElementTree as ET

root = ET.Element("testsuites", tests=23)  # int attribute
ET.ElementTree(root).write("filename.xml")  # raises TypeError

And you can see it succeed after forcing a string:

import xml.etree.cElementTree as ET

root = ET.Element("testsuites", tests=str(23))  # string attribute
ET.ElementTree(root).write("filename.xml")      # OK

Fixing the XML generation

The correction is straightforward: convert numeric attributes to strings before handing them to Element or SubElement. In the original flow, that means fixing the tests attribute in two places.

import xml.etree.cElementTree as ET

class CaseNode:
    def __init__(self, label, outcome):
        self.name = label
        self.result = outcome
    
    def __str__(self):
        return f"Test Case: {self.name}, Status: {self.result}"

class SuiteNode:
    def __init__(self, title):
        self.name = title
        self.test_case_list = []

    def __str__(self):
        return f"Test Suite: {self.name}"

def run():
    print("Hello from html-test-report-generator!")
    suites = build_html_report()
    for s in suites:
        print(s)
        for c in s.test_case_list:
            print(c)
        print()
    write_xml_report(suites)

def build_html_report():
    # generates a list of SuiteNode objects and returns the list back to the caller
    pass

def write_xml_report(suites):
    root_suites = ET.Element(
        "testsuites",
        tests=str(len(suites)),     # to string
        failures="0",
        disabled="0",
        errors="0",
        name="AllTests",
    )
    for s in suites:
        suite_node = ET.SubElement(
            root_suites,
            "testsuite",
            name=s.name,
            tests=str(len(s.test_case_list)),  # to string
            failures="0",
            disabled="0",
            skipped="0",
            errors="0",
        )
        for c in s.test_case_list:
            ET.SubElement(
                suite_node,
                "testcase",
                name=c.name,
                file="",
                line="",
                status="run",
                result=c.result,
            )
    tree = ET.ElementTree(root_suites)
    tree.write("filename.xml")

Verifying the structure while you work

If you want to inspect the shape of the tree as it’s built, print the intermediate structures. Call testsuites.dump() after each Element or SubElement construction to check attributes and nesting. Also use print(), print(type(...)), and print(len(...)) around the values that feed into attributes. This quick “print debugging” can save time by pinpointing where a non-string slips into attributes.

Why this detail matters

XML tooling is strict about data types at serialization boundaries. In ElementTree, attributes are plain strings, not dynamically coerced values. Small mismatches like an int in an attribute can produce confusing stack traces that hint at write failures higher up the call chain. Recognizing that the serializer is rejecting an int attribute lets you fix it at the source and avoid chasing unrelated hypotheses.

Takeaways

When generating XML with xml.etree, explicitly cast numeric values assigned to attributes to strings. Apply this consistently at every construction point, such as ET.Element(...) and ET.SubElement(...). If serialization fails, reduce to a minimal example and confirm attribute types quickly with print debugging or by dumping the element tree to stdout. This keeps the XML pipeline predictable and the output compatible with downstream consumers.

The article is based on a question from StackOverflow by Harry and an answer by furas.