xml.etree.ElementTree ofrece una API sencilla y eficiente para leer, consultar, modificar y generar XML. Representa el documento como un árbol: ElementTree envuelve el documento y cada Element representa una etiqueta con atributos, texto, tail y elementos hijos.
ElementTree resulta útil para archivos de configuración, feeds, integraciones heredadas, formatos de proyecto y otros documentos XML. La documentación oficial advierte que XML no autenticado puede ser peligroso. Impone límites de tamaño, profundidad, tiempo y acceso a recursos externos.
Leer un archivo XML
import xml.etree.ElementTree as ET
arbol = ET.parse("catalogo.xml")
raiz = arbol.getroot()
print(raiz.tag)
print(raiz.attrib)
parse() devuelve un ElementTree. Para una cadena pequeña ya cargada, usa fromstring():
xml = "<producto id='7'><nombre>Teclado</nombre></producto>"
raiz = ET.fromstring(xml)
No leas un archivo arbitrariamente grande antes de aplicar un límite de bytes. Valida tamaño en almacenamiento, upload o HTTP antes de entregar datos al parser.
Tags, atributos, text y tail
Un elemento expone tag, attrib, text y tail. text contiene contenido antes del primer hijo, mientras tail contiene texto después del cierre y antes del siguiente hermano.
for hijo in raiz:
print(hijo.tag, hijo.get("id"), hijo.text, hijo.tail)
Para juntar todo el texto interno de una subárbol, usa "".join(elemento.itertext()). No supongas que todo el texto visible está únicamente en .text.
Buscar elementos
for producto in raiz.findall("producto"):
nombre = producto.findtext("nombre", default="")
precio = producto.findtext("precio", default="0")
print(nombre, precio)
find() devuelve el primer elemento o None. Comprueba con elemento is None. El test booleano de elementos vacíos está deprecado y puede confundir un elemento vacío con uno ausente.
Iteración recursiva
for enlace in raiz.iter("enlace"):
print(enlace.get("href"))
iter() recorre toda la subárbol en orden del documento. Para contenido grande o no confiable, cuenta elementos y detén el procesamiento al superar el límite.
XPath limitado
ElementTree soporta un subconjunto útil de XPath para rutas, descendientes, atributos, texto y posiciones.
activos = raiz.findall(".//producto[@activo='si']")
segundos = raiz.findall(".//item[2]")
No es un motor XPath completo. Evita construir expresiones con texto arbitrario del usuario. Usa consultas conocidas y valida valores antes de interpolarlos.
Namespaces
Durante el parsing, las etiquetas con namespace se expanden al formato {URI}nombre.
namespaces = {
"atom": "http://www.w3.org/2005/Atom",
}
for entrada in raiz.findall("atom:entry", namespaces):
titulo = entrada.findtext("atom:title", namespaces=namespaces)
print(titulo)
Define tu propio mapa de prefijos. El prefijo original no es la identidad del namespace; la URI sí lo es.
Crear XML
raiz = ET.Element("catalogo", {"version": "1"})
producto = ET.SubElement(raiz, "producto", {"id": "7"})
ET.SubElement(producto, "nombre").text = "Teclado"
ET.SubElement(producto, "precio").text = "199.90"
ET.indent(raiz, space=" ")
xml = ET.tostring(raiz, encoding="unicode")
print(xml)
El serializer escapa texto y atributos. Aun así, valida nombres de tags y estructura cuando se elijan dinámicamente. No permitas nombres arbitrarios sin allowlist.
Escribir en archivo
arbol = ET.ElementTree(raiz)
arbol.write(
"catalogo.xml",
encoding="utf-8",
xml_declaration=True,
)
Escribe primero en un archivo temporal del mismo directorio y reemplaza el destino después del éxito. Consulta tempfile en Python.
Modificar elementos
for producto in raiz.findall("producto"):
producto.set("revisado", "si")
precio = producto.find("precio")
if precio is not None:
precio.text = str(round(float(precio.text or "0"), 2))
Para eliminar hijos, recógelos primero y modifica después. Cambiar la colección durante la iteración puede saltar elementos.
eliminar = [
producto
for producto in raiz.findall("producto")
if producto.get("inactivo") == "si"
]
for producto in eliminar:
raiz.remove(producto)
Parsing incremental con iterparse
iterparse() procesa documentos grandes sin mantener todos los nodos útiles en memoria, aunque realiza lecturas bloqueantes.
for evento, elemento in ET.iterparse(
"datos.xml",
events=("end",),
):
if elemento.tag == "registro":
procesar(elemento)
elemento.clear()
Usa eventos end cuando necesitas hijos y texto completos. En un evento start, el contenido puede no estar listo. clear() libera referencias y reduce memoria.
XMLPullParser para entrada por bloques
parser = ET.XMLPullParser(events=("end",))
for bloque in fuente_de_bloques():
parser.feed(bloque)
for evento, elemento in parser.read_events():
if elemento.tag == "registro":
procesar(elemento)
elemento.clear()
parser.close()
XMLPullParser encaja cuando otra capa controla lecturas no bloqueantes. Todavía necesitas límites de bytes, elementos, texto y profundidad.
Límites de recursos
Antes del parsing, limita bytes. Durante el procesamiento, cuenta elementos, atributos, profundidad, namespaces y tamaño de texto. Interrumpe al superar cualquier umbral contractual.
MAX_ELEMENTOS = 100_000
contador = 0
for evento, elemento in ET.iterparse("datos.xml", events=("end",)):
contador += 1
if contador > MAX_ELEMENTOS:
raise ValueError("el XML supera el límite de elementos")
XML no confiable
XML puede explotar expansión de entidades, referencias externas, profundidad excesiva y payloads gigantes, según el parser y entorno. Para datos de terceros, considera una biblioteca endurecida, aislamiento de proceso y límites de infraestructura.
Un parsing exitoso no es validación. ElementTree no valida automáticamente un XSD ni garantiza que el documento siga el esquema de negocio.
XInclude
xml.etree.ElementInclude puede sustituir nodos XInclude por archivos o recursos. El loader por defecto lee del disco. No proceses XInclude de XML no confiable sin loader restrictivo, raíz permitida y profundidad limitada.
from xml.etree import ElementInclude
ElementInclude.include(
raiz,
loader=loader_controlado,
max_depth=3,
)
No uses max_depth=None con datos externos. Rechaza rutas absolutas, URLs y traversal.
Canonicalización
canonicalize() aplica C14N 2.0 y genera una representación determinista útil para comparaciones y flujos de firma.
normalizado = ET.canonicalize(
xml_data=xml_original,
with_comments=False,
strip_text=False,
)
Canonicalizar no verifica una firma ni vuelve confiable el documento. Solo normaliza decisiones de serialización como namespaces, atributos y espacios.
Orden de atributos
El modelo de información XML no asigna significado al orden de atributos. Las versiones actuales preservan la inserción, pero la lógica de negocio no debe depender de ella. Usa canonicalización para salida criptográfica o comparación byte a byte.
Encoding
tostring(..., encoding="unicode") devuelve str; otros encodings devuelven bytes. Combina el tipo con el stream correcto. No escribas string en stream binario ni bytes en stream de texto.
Para problemas de encoding, consulta codecs en Python.
Errores de parsing
XML malformado lanza ET.ParseError, normalmente con línea y columna.
try:
raiz = ET.fromstring(xml_recibido)
except ET.ParseError as error:
linea, columna = error.position
raise ValueError(
f"XML inválido en línea {linea}, columna {columna}"
) from error
No expongas fragmentos sensibles del XML en mensajes públicos.
Validación de dominio
Después del parsing, valida tags obligatorias, tipos, rangos, cardinalidad y relaciones. Usa Decimal para dinero, reglas explícitas de timezone y allowlists para enums y URLs.
Pruebas recomendadas
Prueba documento vacío, XML malformado, namespaces, atributos ausentes, texto tail, XPath soportado y no soportado, archivos grandes, profundidad excesiva, demasiados elementos, encoding divergente, escritura atómica, XInclude bloqueado y canonicalización.
Errores comunes
Los fallos frecuentes son cargar XML ilimitado, ignorar namespaces, asumir que .text contiene todo, probar con if not elemento, modificar hijos durante iteración, esperar XPath completo, habilitar XInclude libremente, depender del orden de atributos y tratar parsing como validación.
Conclusión
ElementTree es una solución práctica para XML en la biblioteca estándar. Usa parse() y fromstring() para documentos pequeños, iterparse() para archivos grandes y XMLPullParser para entrada incremental. Modela namespaces, valida el dominio e impone límites antes y durante el parsing.
Consulta la documentación oficial de ElementTree y las orientaciones de seguridad XML de Python. Para XML externo, combina parser actualizado, límites, validación y aislamiento.







