Python ElementInclude: Safe XInclude

Published on: August 23, 2026
Reading time: 4 minutes
Close-up view of a computer screen displaying code in a software development environment.

The xml.etree.ElementInclude module adds limited XInclude support to trees created with xml.etree.ElementTree. XInclude lets an XML document declare that another XML file or text resource should be inserted at a specific point.

This can simplify configuration files, documentation, catalogs, and large documents split into reusable sections. It also creates security risks: reading files outside an approved directory, remote requests, cycles, content expansion, and hidden dependencies. Custom loaders, allowlisted paths, and a strict depth limit are essential.

What XInclude does

XInclude uses the namespace http://www.w3.org/2001/XInclude. An xi:include element provides an href and may use parse="xml" or parse="text".

<document xmlns:xi="http://www.w3.org/2001/XInclude">
  <title>Report</title>
  <xi:include href="sections/summary.xml" parse="xml"/>
</document>

During expansion, the include element is replaced by the root of the referenced XML. For text mode, the referenced text is inserted as character data.

A first example

from xml.etree import ElementTree, ElementInclude

tree = ElementTree.parse("document.xml")
root = tree.getroot()

ElementInclude.include(root)

tree.write(
    "result.xml",
    encoding="utf-8",
    xml_declaration=True,
)

The default loader treats href as a filename. That is convenient for trusted local projects, but unsafe when XML or paths can be influenced by users.

ElementTree integration

ElementInclude works with Element or ElementTree objects. Before using XInclude, understand parsing, namespaces, queries, and serialization. The guide to Python ElementTree covers those operations.

Expansion happens in place, modifying the original tree. If the pre-expansion document must be retained, parse it again or create a controlled copy.

XML mode and text mode

With parse="xml", a loader must return an Element. With parse="text", it returns a string. The default text encoding is UTF-8 unless another encoding is specified.

<document xmlns:xi="http://www.w3.org/2001/XInclude">
  <footer>
    <xi:include href="year.txt" parse="text" encoding="utf-8"/>
  </footer>
</document>

Included text does not automatically become XML markup. It remains character data and is escaped during serialization.

Resolve relative references with base_url

The base_url argument helps resolve references relative to the main document.

from pathlib import Path
from xml.etree import ElementTree, ElementInclude

file = Path("configs/main.xml").resolve()
tree = ElementTree.parse(file)

ElementInclude.include(
    tree.getroot(),
    base_url=file.as_uri(),
    max_depth=4,
)

base_url is not a security boundary. It resolves references but does not block ../, absolute paths, or remote URLs when the loader accepts them.

Maximum depth

include() uses max_depth=6 by default. The limit reduces recursive inclusion and content-explosion risk. Passing None disables the limit and is rarely appropriate.

Choose a smaller number when the project structure is known. A configuration format that supports only one or two nested levels should use a matching policy.

Inclusion cycles

A cycle occurs when a.xml includes b.xml and b.xml includes a.xml. Maximum depth eventually stops expansion, but a custom loader should detect repeated paths and report a clear error.

A local allowlist loader

from pathlib import Path
from xml.etree import ElementTree

ROOT = Path("content").resolve()

class LocalLoader:
    def __init__(self):
        self.visited = set()

    def __call__(self, href, parse, encoding=None):
        path = (ROOT / href).resolve()

        if ROOT not in path.parents and path != ROOT:
            raise ValueError("Include is outside the allowed directory")

        if path in self.visited:
            raise ValueError("XInclude cycle detected")
        self.visited.add(path)

        if path.stat().st_size > 2 * 1024 * 1024:
            raise ValueError("Included file is too large")

        if parse == "xml":
            return ElementTree.parse(path).getroot()
        if parse == "text":
            return path.read_text(encoding=encoding or "utf-8")

        raise ValueError(f"Unsupported parse mode: {parse}")

Pass the loader explicitly:

loader = LocalLoader()
ElementInclude.include(
    tree.getroot(),
    loader=loader,
    max_depth=4,
)

The example blocks path traversal, simple cycles, and oversized files. Production code should also limit total included bytes, file count, nesting, and processing time.

Path.resolve() exposes symbolic links and .. components before the directory check. A time-of-check to time-of-use race can still exist. In hostile environments, use controlled directories, restrictive permissions, and operating-system APIs designed for safer relative file access.

Avoid remote includes

A loader can fetch URLs, but that introduces SSRF, redirects, DNS rebinding, huge responses, and availability problems. For third-party XML, prefer banning HTTP and HTTPS includes.

When network access is unavoidable, allowlist hosts and protocols, validate resolved IP addresses, limit redirects, enforce timeouts, and cap response bytes. The guide to Python urllib.request explains bounded downloads.

Temporary files and caching

When remote resources are downloaded before expansion, use safe temporary storage and integrity-aware caching. See Python tempfile and Python hashlib.

Never use an untrusted href directly as a local filename. Generate an internal name and store the mapping separately.

Feature limitations

Python provides limited XInclude support and does not implement full XPointer syntax. Do not assume compatibility with every advanced XInclude document or XML toolchain. Test against real documents used by the integration.

For schemas, full XPath, advanced validation, or complex resolution policies, a specialized XML library may be more appropriate.

Validate after expansion

An included fragment can introduce unexpected elements, namespaces, or values that violate business rules. Validate the final expanded tree, not only individual source files.

items = tree.getroot().findall(".//item")
if len(items) > 10_000:
    raise ValueError("Too many items")

for item in items:
    if not item.get("id"):
        raise ValueError("Item is missing an ID")

Atomic output

When saving the expanded document, write to a temporary file in the target directory and replace the final file only after successful serialization. This prevents partial XML after an error.

Logging and diagnostics

Record the main document, normalized include path, size, depth, and outcome. Do not log sensitive contents. Errors should identify the failing include without exposing unnecessary internal paths.

Test XML and text includes, invalid encoding, missing files, absolute paths, ../, symbolic links, direct and indirect cycles, exceeded depth, oversized files, invalid parse modes, namespaces, and validation of the expanded tree.

Common mistakes

Common failures include using the default loader with external XML, disabling max_depth, allowing arbitrary URLs, missing cycles, checking a path before resolving it, trusting href as a local filename, and failing to validate the expanded tree.

Conclusion

xml.etree.ElementInclude lets applications split XML into reusable files and expand XInclude inside ElementTree structures. The feature is simple, but resource resolution must be treated as a sensitive operation.

Use a custom loader, an allowed root directory, strict depth and size limits, and final-tree validation. Consult the official ElementTree XInclude documentation and the W3C XInclude recommendation.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Close-up of a saxophone and sheet music stand, perfect for jazz lovers and musicians.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python xmlreader: Control SAX Parsers

    Learn Python xmlreader to configure SAX parsers, InputSource, incremental parsing, attributes, locators, and safer XML handling.

    Ler mais

    Tempo de leitura: 4 minutos
    23/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python saxutils: XML Utility Functions

    Learn Python saxutils to escape XML, quote attributes, generate documents, build SAX filters, and avoid context mistakes.

    Ler mais

    Tempo de leitura: 5 minutos
    23/08/2026
    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pulldom: Partial DOM for XML

    Learn Python pulldom to process XML events, expand only selected subtrees, and reduce memory use with safer limits.

    Ler mais

    Tempo de leitura: 5 minutos
    23/08/2026
    A laptop screen showing a code editor with visible programming code in a dimly lit environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python xml.sax: Process XML Events

    Learn Python xml.sax to process XML as events with low memory use, namespaces, handlers, limits, and secure entity handling.

    Ler mais

    Tempo de leitura: 4 minutos
    22/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python minidom: Manipulate XML with DOM

    Learn Python xml.dom.minidom to read, navigate, create, and serialize XML with DOM nodes, namespaces, memory control, and security.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    A solitary tree with sunlight filtering through the leaves on a grassy hill under a blue sky.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ElementTree: Read and Modify XML in Python

    Learn Python ElementTree to read, search, modify, and generate XML with namespaces, incremental parsing, limits, and security.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026