When XML Attributes Are the Better Choice Over Elements
Attributes suit short identifying values, elements suit everything with structure or long text. A test of the whitespace and ordering rules shows why.

XML gives you two places to put a piece of data on an element: as an attribute on the start tag, or as a child element. The specification does not prefer either one. The two forms have different properties that decide which fits, and a wrong choice is expensive to undo once other people read your documents. A short rule of thumb handles most cases, and a couple of experiments show where it comes from.
What differs
An attribute is a name and a value on a start tag: <price currency="EUR">. A child element is its own node: <price><currency>EUR</currency></price>. Beyond the syntax:
An element can repeat; an attribute cannot. The same attribute name twice on one tag is a well-formedness error. A list of values needs an element for each or a separator inside one value.
An element can have structure. It can hold children, attributes of its own, and mixed text. An attribute value is a flat string.
An attribute value is normalized. A parser replaces tabs and line breaks in an attribute value with spaces, and for typed attributes in a DTD, collapses runs of spaces. Element content keeps its whitespace as written.
Attribute order has no meaning. A producer may write them in any order, and parsers do not preserve it as data (canonical XML and why diffs need it sorts them for comparison). Child element order is part of the document.
Attributes are not extensible in place. Add a unit to
weight="12"and you have to turn it into an element or add a second attribute with an agreed name.
The whitespace difference, shown
Store the same text, with a line break and a tab, in both forms and read it back:
from lxml import etree
el = etree.fromstring('<a note="line1\nline2\ttab"><note>line1\nline2\ttab</note></a>')
print(repr(el.get("note")))
print(repr(el.findtext("note")))'line1 line2 tab'
'line1\nline2\ttab'The attribute lost both characters; each became a space. The element kept them. If you write a multi-line address, a log excerpt or a code sample into an attribute, it cannot come back as it went in, unless you escape the characters as and 	, which most generators do not do on their own. This is one of the few places where an XML design decision silently changes data, and a reason to keep free text in elements.
The rule of thumb
Use an attribute when the value is:
short, usually a word or a code;
single, with no sensible reason to repeat;
atomic, with no structure of its own;
about the element rather than part of its content, such as an
id, atype, alang, acurrency, aunit.
Use a child element when the value is:
long, or may contain line breaks, markup or special whitespace;
repeatable, now or plausibly later;
structured, or might gain parts (a name that could split into first and last, an amount that could gain a currency);
a real piece of content that a reader would call "the data" rather than "a property of the data".
So <order id="7" status="paid"> with <customer> and <total> as children is a typical good split: identity and state as attributes, content as elements. The lang attribute on a title is another standard case, since the language describes the text rather than being the text.
Growth is the deciding question
The most useful question is "what happens when this needs a second thing?". A weight written as <item weight="12"/> is fine until someone needs pounds. Then it is weight="12" weightUnit="kg", with the unit detached from the value and the attribute names growing a naming convention. Written as <weight unit="kg">12</weight>, the same change was a unit attribute on an element that already existed. A date as an attribute stays an attribute; a price that will one day carry a currency, a tax rate and a validity range should start as an element.
Adding an attribute to an element is a backward-compatible change for most readers, since readers ignore attributes they do not know. Turning an attribute into an element is not: every reader that looked at @weight breaks. If a field is likely to need structure, start with the element, even at the cost of a few more bytes (versioning an XML format without breaking readers).
What it looks like after conversion
Attributes and elements both survive a conversion to JSON, but they land differently, and that affects how pleasant the result is. xmltodict prefixes attributes with @ and puts element text under #text when both are present:
import json, xmltodict
x = '<order id="7"><item sku="A" qty="2">Pen</item><item sku="B">Ink</item><note/></order>'
print(json.dumps(xmltodict.parse(x), indent=1)){
"order": {
"@id": "7",
"item": [
{
"@sku": "A",
"@qty": "2",
"#text": "Pen"
},
{
"@sku": "B",
"#text": "Ink"
}
],
"note": null
}
}The item text is under #text only because it has attributes; an item without attributes would be a plain string. A design that puts everything in attributes converts to objects with @ keys everywhere, and one that puts everything in elements converts to plainer JSON. Neither is wrong. If the document will usually be converted, think about which you would prefer to read (moving from XML to JSON gracefully).
Querying and validating
Both forms are equally queryable: //item[@sku='A'] finds by attribute, //item[sku='A'] by child (filtering XML nodes by attribute with XPath). Both are validated by a schema, with attributes declared as xs:attribute and elements as xs:element. The practical difference is that attributes are unordered in a schema and elements are ordered unless you say otherwise, so an attribute-heavy design is more tolerant of writers that reorder.
Related reading
The cluster hub is designing XML documents that age well. The syntax side is in XML attributes versus child elements, and the name side in choosing XML element names and casing. The specification of attribute-value normalization is in the XML 1.0 recommendation. The @ and #text convention comes from xmltodict. Convert a snippet with the XML to JSON tool.
When the attribute rule of thumb does not apply
The format already decided. Maven, Android and SVG all have a style. Follow it, or readers will find your additions strange.
The consumer is a tool with a fixed mapping. Some serializers map every property to an attribute or every property to an element. If the tool offers no choice, the choice is not yours.
You need to preserve whitespace exactly. Use elements. Or, if you must use an attribute, escape the line breaks as character references, and test the round trip.
The data is a list. A space-separated list in an attribute (class="a b c", as HTML does) is legal and typed in some schemas. It is also unqueryable with simple XPath, so use it only when the format follows that convention.


