List Editing#
You have already used list editing without being told what it was.
Every time a lesson authored prepend references = ..., that prepend was a list-editing operation. There are four more.
What Is List Editing?#
Most metadata in OpenUSD resolves by strength: the strongest opinion wins and the rest are ignored. Ordered lists do not work that way. If they did, a shot layer adding one reference would wipe out every reference the asset layer had contributed.
Instead, list-valued fields compose. Each layer contributes operations — add this to the front, remove that one — and OpenUSD combines them from weakest to strongest into a final list.
Many fields are list-edited, and they all share the same machinery:
Composition arcs:
references,payload,inherits,specializes,variantSetsapiSchemas, the list of applied API schemasRelationship targets and attribute connections
Important
subLayers is not list-edited, despite looking like it should be. See subLayers Is the Exception below — this trips people up regularly.
The examples in this lesson use relationship targets, because GetTargets() returns the composed list in one call and makes the mechanism easy to see. Everything shown applies identically to references, inherits, and the rest.
The Operations#
Operation |
|
Effect |
|---|---|---|
Prepend |
|
Add to the front of the composed list |
Append |
|
Add to the back |
Delete |
|
Remove a matching item contributed by a weaker layer |
Reset to explicit |
|
Discard everything weaker and use exactly this list |
Reorder (deprecated) |
|
Constrain relative order without adding or removing |
Two of these are legacy. add still parses but should not be used in new content, and reorder is deprecated — the official OpenUSD glossary describes list editing as prepend, append, remove, and reset, and does not include reorder among the operations at all.
Prepend and Append Make a Sandwich#
A stronger layer’s prepend lands in front of everything the weaker layers contributed, and its append lands behind everything they contributed. The weaker layer’s items end up in the middle.
Caution
This has a consequence worth pausing on: for composition arcs, list order is strength order — the first arc is the strongest. So append from a strong layer produces the weakest arc on that prim. A strong layer can author a weak opinion, and that is a very common source of “why isn’t my reference winning?”
Delete Only Reaches Downward#
delete removes an item contributed by a weaker layer. It never reaches upward: if a stronger layer prepends the same item back, the item is present.
Matching is on the whole value, not on a string. For a reference, that means the asset path and prim path and layer offset all have to match. The asset path is anchored to the layer that authored it before comparison, so @./B.usda@ in one layer and @../B.usda@ in a layer one directory down do match if they resolve to the same file. A delete whose layer offset differs will silently fail to match.
Reset to Explicit Is a Reset, Not a Lock#
Authoring the bare form with no keyword discards every weaker opinion. It is the “stop listening to the layers below me” escape hatch.
It only blocks weaker layers, though. An explicit list is still freely edited by prepend, append, and delete in stronger layers. Explicit is not a lock.
Note
An explicit empty list serializes as references = None, not references = []. Both spellings parse identically, but OpenUSD always writes the None form, so a hand-typed [] round-trips into None.
Reorder Is Deprecated, and Surprising#
Caution
reorder is deprecated and is not part of the official list-editing operation set. Do not author it in new content.
It is covered here because you will meet it in existing scene description, and because its behavior is almost always misread when you do.
reorder never adds and never removes — it only permutes items already in the composed list. Names that are not in the list are silently ignored, with no warning.
The part that catches people out is what “reorder” actually guarantees. It is not “move these to the front.” It is a relative-order constraint: OpenUSD rearranges only as much as needed to make the named items appear in the order given, and unnamed neighbors get dragged along.
On a base list of [A, B, C, D]:
Reorder |
Result |
Why |
|---|---|---|
|
|
|
|
|
|
|
|
No-op — |
Reorders from several layers accumulate weakest to strongest rather than replacing one another, and a reorder cannot resurrect a deleted item.
Working With Python#
Position is controlled by UsdListPosition, which has exactly four values, reachable only as module-level names:
1Usd.ListPositionFrontOfPrependList
2Usd.ListPositionBackOfPrependList # the default
3Usd.ListPositionFrontOfAppendList
4Usd.ListPositionBackOfAppendList
Caution
AddReference defaults to BackOfPrependList, so it authors prepend, not append, despite being named “Add”.
For references and payloads the argument after the paths is layerOffset, not position — AddReference(path, primPath, Usd.ListPositionBackOfAppendList) raises an error. Always pass position as a keyword: position=Usd.ListPositionBackOfAppendList.
There are three different ways to “remove” something, and they are not interchangeable:
Call |
What it authors |
Effect on weaker layers |
|---|---|---|
|
nothing (erases only this layer’s opinion) |
weaker references come back |
|
|
weaker reference is blocked |
|
|
everything weaker is blocked |
Reaching for Clear when you meant to block is a real bug, and it fails quietly.
Two more API facts worth knowing. SetReferences() with a non-empty list authors an explicit list op, so it is not a convenience wrapper around repeated AddReference calls — it blocks weaker layers. And Usd.References has no getter at all; to read back what a layer authored you go through prim.GetMetadata("references"), which returns an Sdf.ReferenceListOp.
Note
There is no convenience API for reorder. To author one you hand a raw list op with orderedItems set to either Usd.Prim.SetMetadata() or an Sdf spec’s list editor, such as spec.targetPathList.orderedItems = [...].
Examples#
Tip
You can run these examples locally as Jupyter notebooks. See How to Run Notebooks Locally for setup instructions.
Example 1: Prepend and Append Across Layers#
The weak layer contributes A, B, C. The strong layer prepends D and appends E. Watch where they land.
1from pxr import Usd
2
3stage, weak, strong = build_stack()
4
5with Usd.EditContext(stage, weak):
6 for path in ["/A", "/B", "/C"]:
7 members(stage).AddTarget(path)
8
9with Usd.EditContext(stage, strong):
10 # BackOfPrependList is the default, shown here for clarity
11 members(stage).AddTarget("/D", Usd.ListPositionBackOfPrependList)
12 members(stage).AddTarget("/E", Usd.ListPositionBackOfAppendList)
13
14print("what the strong layer authored:")
15print(strong.ExportToString())
16print("composed:", composed(stage))
what the strong layer authored:
#usda 1.0
over "World"
{
over "Set"
{
custom rel members
prepend rel members = </D>
append rel members = </E>
}
}
composed: ['/D', '/A', '/B', '/C', '/E']
The strong layer’s prepend went in front of all three weaker items and its append went behind all three, producing ['/D', '/A', '/B', '/C', '/E']. If these were references, /E would be the weakest arc on the prim despite being authored in the strongest layer.
Example 2: The Three Ways to Remove#
Here the same weak layer is combined with three different strong-layer edits, to show that delete, reset-to-explicit, and clear are three genuinely different operations.
1from pxr import Usd
2
3def weak_abc(stage, weak):
4 with Usd.EditContext(stage, weak):
5 for path in ["/A", "/B", "/C"]:
6 members(stage).AddTarget(path)
7
8# 1. delete: removes one item contributed by the weaker layer
9stage, weak, strong = build_stack()
10weak_abc(stage, weak)
11with Usd.EditContext(stage, strong):
12 members(stage).RemoveTarget("/B")
13print("RemoveTarget('/B') ->", composed(stage))
14removal_delete = composed(stage)
15
16# 2. reset to explicit: discards everything weaker
17stage, weak, strong = build_stack()
18weak_abc(stage, weak)
19with Usd.EditContext(stage, strong):
20 members(stage).SetTargets(["/X", "/Y"])
21print("SetTargets(['/X','/Y']) ->", composed(stage))
22removal_explicit = composed(stage)
23
24# 3. clear: erases only this layer's own opinion
25stage, weak, strong = build_stack()
26weak_abc(stage, weak)
27with Usd.EditContext(stage, strong):
28 members(stage).ClearTargets(removeSpec=False)
29print("ClearTargets() ->", composed(stage))
30removal_clear = composed(stage)
RemoveTarget('/B') -> ['/A', '/C']
SetTargets(['/X','/Y']) -> ['/X', '/Y']
ClearTargets() -> ['/A', '/B', '/C']
RemoveTarget authored a delete and took /B out. SetTargets reset the list to exactly what it was given, discarding the weak layer entirely. ClearTargets removed the strong layer’s own opinion, so the weak layer shows through unchanged — which is the behavior people expect from the other two.
Example 3: Reorder Is Not Move-to-Front#
Three reorders against the same base list of [A, B, C, D].
1from pxr import Usd, Sdf
2
3reorder_results = {}
4
5for order in (["/C", "/A"], ["/C", "/B"], ["/B", "/D"]):
6 stage, weak, strong = build_stack()
7 with Usd.EditContext(stage, weak):
8 for path in ["/A", "/B", "/C", "/D"]:
9 members(stage).AddTarget(path)
10
11 # There is no Usd-level API for reorder, so author it through the Sdf spec
12 with Usd.EditContext(stage, strong):
13 members(stage)
14 strong.GetRelationshipAtPath("/World/Set.members").targetPathList.orderedItems = order
15
16 reorder_results[tuple(order)] = composed(stage)
17 print(f"reorder {order} -> {composed(stage)}")
reorder ['/C', '/A'] -> ['/C', '/D', '/A', '/B']
reorder ['/C', '/B'] -> ['/A', '/C', '/D', '/B']
reorder ['/B', '/D'] -> ['/A', '/B', '/C', '/D']
Only the relative order of the named items is guaranteed. reorder ['/C', '/A'] dragged /D forward along with /C, and reorder ['/B', '/D'] did nothing at all because /B already preceded /D.
subLayers Is the Exception#
subLayers looks like it should be list-edited and is not. Its field type is a plain vector of strings, not a list op, and the list-op keywords are hard parse errors on it — a file containing prepend subLayers = [...] fails to open rather than being quietly ignored.
The practical consequence is the important part:
A stronger layer can delete a reference contributed by a weaker layer. It cannot delete a sublayer contributed by a weaker layer, because
delete subLayersis not valid syntax.
Sublayer composition is governed by each layer’s own flat list, resolved recursively. If a weaker layer brings in a sublayer you do not want, your options are to edit the layer that actually owns that entry, or to mute it — see Edit Targets and Layer Muting. Muting is stage state and cannot be shipped in a file the way a delete references can.
Note
layer.subLayerPaths behaves like a Python list but is not one: remove() on a missing item is a silent no-op rather than a ValueError, and index() returns -1 instead of raising. A typo in a sublayer path therefore fails quietly.
Key Takeaways#
List editing is why a shot layer can add one reference without destroying the ones an asset layer contributed. Lists compose from weakest to strongest out of four current operations: prepend, append, delete, and reset-to-explicit. A fifth, reorder, is deprecated but still appears in existing content.
Three things are worth remembering past this lesson. Because list order is strength order for composition arcs, append from a strong layer produces a weak arc — if an arc is not winning, check which end of the list it landed on. reorder is a relative-order constraint, not move-to-front, and it is a no-op when the requested order already holds — worth recognizing when you read it, though it is deprecated for authoring. And Clear, Remove, and Set([]) are three different removals: only the last two block a weaker layer, and choosing the wrong one fails silently.
Finally, subLayers is not list-edited at all. When you need to suppress a sublayer, reach for muting or edit the owning layer.