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.
Symbolic links
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.
Recommended tests
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.







