2026-06-01

Day 2: Hand-writing OpenUSD Python from the docs

openusdpythonlearning-lognetwork-engineer-bridge

Second session of the OpenUSD learning track. Day 1 was the scan-import-render pipeline. This one is about authoring USD by hand in Python so the abstractions stop being magic. The friction notes and the Q-and-A learnings are the load-bearing content for now.

The shift

Day 1 was operating on USDZ (scan it, import it, render it). Day 2 is authoring USD by creating a stage from scratch in Python, defining prims one at a time, reading the .usda text format, and watching it change as I mutate the in-memory scenegraph.

This is the part where the OpenUSD bridge to network-engineering mental models starts paying off. The text format reads like a YANG-shaped tree. The Python API reads like NSO’s MAAPI. The composition arcs (next session) will read like NSO services + YANG augments. But you don’t see any of that until you’ve handwritten the API a few times.

What I’m working from

LearnOpenUSD: Setting the Stage module is seven pages, around 90 minutes if you read carefully and run every snippet:

  1. Stage: the top-level scene container
  2. OpenUSD modules: which pxr.* packages to import
  3. Prims: the scene-graph nodes
  4. Prim and Property Paths: /World/ChairA-style XPath equivalents
  5. Attributes: the data on prims
  6. Relationships: typed pointers between prims

The companion exercise stack lives at omniverse-learn/ in my prep repo. The files are hello_usd.py, 01_references.py, 02_variants.py, and 03_material.py. The plan is to redo each from scratch, by hand, after reading the corresponding docs chapter.

What clicked

The moment OpenUSD stopped feeling like a mystery and started feeling familiar was the moment I opened a saved .usda file in a text editor for the first time. I had assumed USD was some opaque binary format that you only ever interacted with through tooling. It is not. The native authoring format is human-readable text, the syntax is a hierarchical tree of typed prims with attributes, and the whole thing reads like a YANG document that happens to describe geometry instead of network configuration. Once I saw the text, the Python API made sense, because the Python API is just a procedural way to author the same tree I could have typed by hand.

The other thing that clicked was that there is no real substitute for typing the API out a few times. I had spent the previous week watching Omniverse YouTube content and reading documentation, which gave me vocabulary but not muscle memory. Sitting down and writing stage = Usd.Stage.CreateNew(path) and then stage.DefinePrim("/World/Chair", "Cube") and watching the resulting .usda file appear on disk produced more understanding in twenty minutes than the previous week of passive consumption had. The reason is that passive consumption lets you skim over the parts you do not quite get, and writing code by hand forces you to confront each part on its own terms. I do not think that observation is unique to OpenUSD. I think it is true of every API anyone has ever learned.

Where the network-engineer mental model genuinely accelerates the first day of OpenUSD work: the idea that the model is the source of truth, the idea that you compose opinions rather than overwriting them, the idea that the text format is a canonical wire format you can diff and version-control, the idea that the API is a procedural way to author the same declarative structure the text format describes. These all map cleanly onto how NSO and YANG work. I was not learning new architecture, I was learning a new vocabulary for an architecture I already had.

Where the mental model stops accelerating: USD’s geometry-specific schemas (Xform, Mesh, Material, UsdShade, UsdLux) have no NSO analog because NSO does not describe geometry. USD’s time-sampled attribute values (animation curves baked directly into attribute storage) have no YANG analog because YANG does not deal with time-varying state. USD’s Hydra rendering layer is a whole separate concern that lives downstream of the composed stage and has nothing to do with the authoring model. These are the parts I have to learn cold, and the cross-domain bridge does not help me get there faster. Knowing where the bridge stops is as useful as knowing where it starts, because it tells me where to budget the learning hours.

Stage lifecycle: three constructors, one save

Usd.Stage.CreateNew(path)    # new file, errors if exists, writes empty .usda immediately
Usd.Stage.CreateInMemory()   # scratch stage, no file at all
Usd.Stage.Open(path)         # load existing into memory
stage.Save()                 # flush in-memory mutations to disk

Mirrors Python’s open(path, 'w') / open(path, 'r') lifecycle exactly. The thing I missed for a few minutes: mutations are in-memory only until you call .Save(). CreateNew writes an empty file at creation time, but anything you DefinePrim afterward sits in memory until saved.

Network analogy: CreateNew ≈ initialize and commit an empty candidate config; Open ≈ pull running-config into a candidate; Savecommit after edits.

DefinePrim(path, type): one thing’s arbitrary, one isn’t

stage.DefinePrim("/World/Chair", "Cube")
#                  ^              ^
#                  path (yours)   type (USD's)

The path is whatever you want. Examples: /World, /Garage, /Scene/Building/Floor1/Lobby/ChairA. The type has to be a schema USD knows (Xform, Cube, Sphere, Mesh, Material, etc.). Pass an unknown type string and the prim survives but has no schema methods attached.

Auto-create behavior: DefinePrim("/A/B/C", "Cube") will silently create A and B as typeless prims if they don’t exist. Convenient, but easy to miss.

Network analogy: path is the XPath in a YANG datastore (/interfaces/interface[name='Eth1/1']), and it is author-chosen. Type is the YANG container or leaf type definition, drawn from a fixed registered model. You can’t def Banana for the same reason you can’t have a YANG leaf of type banana without first defining the typedef.

Python type annotations aren’t enforcement

stage: Usd.Stage = Usd.Stage.CreateNew(file_path)

The : Usd.Stage is decoration. It is documentation for IDEs and mypy, ignored by the Python runtime. You can delete it and the program runs identically. You can also lie (stage: int = ...) and it still runs. This is one of those Python gotchas that hits everyone who came from a typed language and assumed annotations mean something at runtime.

Network analogy: type annotations are like YANG type definitions on a leaf, such as leaf vlan-id { type uint16; }. They don’t change the wire bytes. They just let tooling validate and autocomplete. Runtime ignores them; analyzers use them.

What .usda text actually looks like

After authoring a few prims by hand and saving:

#usda 1.0

def Xform "World"
{
    def Cube "Chair"
    {
    }
}

That’s it. def keyword, type, name, body. The hierarchy is nested braces. Every line maps 1:1 to a Python API call. The text format is the canonical wire-format of USD. It is what gets diffed, version-controlled, and code-reviewed. This is the part that maps cleanest onto YANG mental models: declarative, hierarchical, schema-typed, plain-text-canonicalized.

Friction notes

This week’s batch:

OpenUSD’s Python API breaks PEP 8. Methods are CapitalCase, not snake_case. Standard Python would write stage.create_new(...), prim.get_references(), stage.export_to_string(). OpenUSD writes Stage.CreateNew(...), Prim.GetReferences(), Stage.ExportToString() because the Python bindings preserve the C++ API’s naming. Every Python tutorial outside the Pixar and NVIDIA ecosystem trains the opposite convention. Doc-fix: the “Setting the Stage” intro could call this out in a one-line callout: “Note for Python developers: USD’s Python bindings preserve the C++ API’s CapitalCase naming. This is intentional but breaks PEP 8 expectations.”

Usd.Stage.CreateNew is exclusive-create and silent on rerun. Iterating on the hello-world script fails the second run because the file already exists. The fix is rm between runs, switch to Open, or use CreateInMemory for iteration loops. Doc-fix: teach the three-call cheatsheet upfront in the lesson, not buried in the API reference. Right now CreateNew is taught first as if it’s the normal entry point. For an iteration workflow it’s the wrong one.

The cross-domain connection (still load-bearing)

The day-1 analogies stack continues to hold:

Where the analogy weakens: USD attributes carry full time-sampled value tracks (animation), which YANG explicitly punts on. USD’s composition arcs (next session) are stronger-opinion-wins; YANG augments are additive. The mental-model accelerator runs for the first ~80% of the surface area and then you need to re-tool for the parts that are genuinely USD-specific.

What’s next

Tomorrow: Composition Basics (Layers → References → Strength Ordering), then redo 01_references.py by hand from scratch. That’s the chapter where the “single source of truth” / “non-destructive pipeline” terminology I keep hearing from NVIDIA’s Physical AI talks finally becomes operational.


Find the running notes file (omniverse-learn/openusd-python-notes.md) and the canonical friction log in the prep repo. Reach me on LinkedIn or via Sierra Code Co.