How to Convert XML to PDF with Python

Learn how to convert XML into a tagged PDF with Python and present the data as a hierarchy or formatted source.
Share this page

This Python tutorial converts XML into a styled, tagged PDF using the pdfRest Convert to PDF API Tool. It demonstrates a multipart upload, structured conversion options, and hierarchy presentation for nested XML content.

Why Convert XML to PDF with Python?

Python automation produces and consumes XML in testing, scientific processing, data exchange, and infrastructure tooling. The file may contain exactly the information a team needs, yet raw XML is a poor deliverable for release managers, researchers, or clients who expect a document they can open and annotate.

A continuous-integration pipeline provides a practical example: test tools may emit XML containing suites, cases, durations, and failure details. A Python step can convert that artifact into a hierarchy-formatted PDF for a release evidence package. Reviewers can follow the nested results in a familiar document, while engineers still retain the XML source for machine analysis.

For workflows centered on code review, Python can request source presentation and show the markup directly. For broader audiences, hierarchy mode suppresses syntax and emphasizes names, values, and nesting. The API handles both outcomes while also applying page setup, typography, language metadata, and optional tagging.

Python Code Example for Converting XML to PDF

import json
import os
import requests

# By default, we use the US-based API service. This is the primary endpoint for global use.
api_url = "https://api.pdfrest.com"

# For GDPR compliance and enhanced performance for European users, you can switch to the EU-based service by uncommenting the URL below.
# For more information visit https://pdfrest.com/pricing#how-do-eu-gdpr-api-calls-work
#api_url = "https://eu-api.pdfrest.com"

# This sample converts XML input to a tagged PDF through multipart /pdf.
# It demonstrates structured_text_options and the format-specific conversion options.
input_path = "/path/to/sample.xml"
options = json.loads(r'''{
  "title": "Structured Content Sample",
  "language": "en-US",
  "enable_tagging": true,
  "page_setup": {
    "size": "Letter",
    "orientation": "portrait",
    "margin": {
      "top": 36,
      "right": 42,
      "bottom": 36,
      "left": 42
    }
  },
  "style": {
    "font": "Arial",
    "heading_font": "Arial",
    "code_font": "Courier",
    "text_size": 11,
    "text_color_rgb": [
      34,
      34,
      34
    ],
    "heading_scale": 1.35,
    "table": {
      "column_width_weights": [
        2,
        3,
        2
      ],
      "keep_header_with_first_row": true,
      "repeat_headers_on_overflow": true,
      "show_borders": true,
      "border_width": 0.75,
      "border_color_rgb": [
        180,
        188,
        200
      ],
      "header_fill_color_rgb": [
        33,
        64,
        98
      ],
      "header_text_color_rgb": [
        255,
        255,
        255
      ],
      "row_fill_color_rgb": [
        250,
        250,
        252
      ],
      "alternate_row_fill_color_rgb": [
        235,
        240,
        246
      ],
      "cell_padding": {
        "top": 6,
        "right": 8,
        "bottom": 6,
        "left": 8
      }
    }
  },
  "data_presentation": "hierarchy"
}''')

from requests_toolbelt import MultipartEncoder
with open(input_path, "rb") as input_file:
    fields = {
        "file": (os.path.basename(input_path), input_file, "text/plain"),
        "structured_text_options": json.dumps(options),
    }
    form = MultipartEncoder(fields=fields)
    response = requests.post(api_url + "/pdf", data=form, headers={
        "Accept": "application/json",
        "Content-Type": form.content_type,
        "Api-Key": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    })

print("Response status code: " + str(response.status_code))
if response.ok:
    print(json.dumps(response.json(), indent=2))
else:
    print(response.text)
    raise SystemExit(1)

Source: View the sample on GitHub

Breaking Down the Code

The Python XML sample uses requests and MultipartEncoder from requests-toolbelt. Install both with python -m pip install requests requests-toolbelt when they are not already present. The caller does not need a separate XML parsing package.

import json
import os
import requests

This Python endpoint excerpt identifies the regional service used for XML conversion:

# By default, we use the US-based API service. This is the primary endpoint for global use.
api_url = "https://api.pdfrest.com"

# For GDPR compliance and enhanced performance for European users, you can switch to the EU-based service by uncommenting the URL below.
# For more information visit https://pdfrest.com/pricing#how-do-eu-gdpr-api-calls-work
#api_url = "https://eu-api.pdfrest.com"

Python opens the .xml input in binary mode and sends its base filename with the stream. /pdf uses that filename extension to infer the XML input type.

# It demonstrates structured_text_options and the format-specific conversion options.
input_path = "/path/to/sample.xml"
options = json.loads(r'''{

The options dictionary becomes pdfRest’s structured_text_options field. It supplies document metadata, explicitly requests tags, establishes Letter portrait pages and 36-point margins, and selects the fonts, text size, color, and XML-specific presentation values.

{
  "data_presentation": "hierarchy"
}

source is the default. It first validates the XML and then displays the user’s original source text, including its markup, whitespace, and declared encoding text. hierarchy instead creates a nested list: local element names become bold labels, attributes appear with an @ prefix, and leaf values follow their element names. Namespace prefixes are not displayed in hierarchy labels. Invalid XML and documents without a root element are rejected in either mode.

The shared style.table member does not affect XML hierarchy or source output. It can be removed from an XML-only profile without changing the resulting document.

For the XML call, MultipartEncoder combines the file tuple with serialized structured_text_options. Its content_type, including the boundary, becomes the request’s Content-Type header.

with open(input_path, "rb") as input_file:
    fields = {
        "file": (os.path.basename(input_path), input_file, "text/plain"),
        "structured_text_options": json.dumps(options),
    }
    form = MultipartEncoder(fields=fields)
    response = requests.post(api_url + "/pdf", data=form, headers={
        "Accept": "application/json",
        "Content-Type": form.content_type,
        "Api-Key": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    })

requests.post sends the encoded XML form with the API key. The script pretty-prints resource JSON after success and otherwise displays the API message before exiting with a nonzero status.

        "file": (os.path.basename(input_path), input_file, "text/plain"),
        "structured_text_options": json.dumps(options),
    }
    form = MultipartEncoder(fields=fields)
    response = requests.post(api_url + "/pdf", data=form, headers={
        "Accept": "application/json",
        "Content-Type": form.content_type,
        "Api-Key": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    })

print("Response status code: " + str(response.status_code))
if response.ok:
    print(json.dumps(response.json(), indent=2))

Beyond the Tutorial

This pattern can automate readable reports from test fixtures, metadata repositories, or batch integration output. The choice between hierarchy and source views lets the workflow serve either general reviewers or developers investigating the raw structure.

Tags can make the PDF’s logical organization available to assistive and downstream software, but they do not independently prove accessibility conformance. Explore different profiles in API Lab and reference the Convert to PDF documentation for the complete contract.

Generate a self-service API Key now!
Create your FREE API Key to start processing PDFs in seconds, only possible with pdfRest.