Symbolic object-oriented programming rests on a single change to what an object is: it keeps the arguments it was constructed from. A program built out of such objects is still a program you run — but it is also a structure your code can read, rewrite, compare, and search over. Six ideas, each one interactive.
An ordinary Python object forgets how it was made: Foo(1) runs its __init__, and the arguments that produced it are gone. A symbolic object keeps that call. It has an executable view — data members and methods, like any object — and a symbolic view — the stored arguments, addressable and rewritable. Change the symbolic side and the executable side recomputes to match. The two never drift apart.
Objects holding objects form a tree, and every node in it has an address: the sequence of keys leading down to it, called a key path. A key path lets you read or rewrite any node, at any depth, without walking there yourself. Hover a node to see its topo_path; click one to rebind its value by path.
When a node changes, its containing object is considered changed too — the notification travels bottom-up: the updated object first, then each ancestor, up to the root, each receiving it exactly once. That is what lets a holder recompute state it derived from a child it doesn't directly own. Click a leaf to watch the cascade.
on_sym_readyfires only when the object is concrete — every field present. Derived state belongs here. The one to reach for first.on_sym_boundsame timing, but fires even while the object is still partial. For logic that must run on an incomplete program.on_sym_changereceives the exact field_updates, so an expensive derivation can refresh only what the change touched.on_topo_parent_change / on_topo_path_changefire when a node is adopted, moved, or detached — for caches keyed on where a node sits. Needs topo=True.Always call super() in these hooks — overriding on_sym_change without it swallows the cascade.
An abstract object's __init__ may not have run yet — so validation can't live there. Instead, rules declared alongside the fields are enforced at every construct, assignment, and rebind, catching invalid arguments the moment they arrive rather than when the object is finally evaluated. Try feeding values to a field declared pg.typing.Int(min_value=0):
A symbolic object can exist before it is fully specified. A partial object has holes — pg.MISSING_VALUE, a hole with a name, not None. A pure symbolic object holds a stand-in like pg.oneof that represents a choice before anything decides it. Either way the object is abstract: __init__ is delayed until it becomes concrete — which is what lets a program be assembled in stages and still be checkable at every stage.
The Symbolic Object Model answers what a program is made of. Symbolic Detour answers how it is interpreted: under a context manager, constructing one class yields another — even inside library code you cannot edit, with no symbolization required. Nested scopes compose, outer mappings win, and redirects are transitive. Toggle the context manager:
The full conceptual treatment — types, operations, and the fine print on each of these — lives in the documentation.