Node API¶
o6\ gives you direct, pythonic access to OPC UA nodes and data types through the object model exposed by a connected client.
The Node API is more than a service-based wrapper. You can navigate an OPC UA server's address space — including both nodes and type definitions — using Python's regular attribute, subscript, and call operators. Those operators are translated into service requests like Browse, Read, Write, Call, letting you write expressions like the one above without ever constructing a request object.
A taste¶
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
print(client.objects.MyInteger()) # read
client.objects.MyInteger(42) # write
print(client.objects.TestMethods.Hello("o6")) # call
Motivation¶
A service-based API is centered around OPC UA requests and response objects: building ReadRequests, sending them, and parsing the result. This is powerful and explicit, but it is also low-level and often very verbose.
The Node API is a higher-level, object-oriented layer on top of the OPC UA services.
| Service API | Node API |
|---|---|
client.serviceRead(...) |
client.root.objects.MyVariable.read() |
client.serviceWrite(...) |
client.root.objects.MyVariable(123) |
client.serviceCall(...) |
client.objects.MyMethod.call(...) |
The . syntax in the Node API gives you canonical Python access to the server's actual UA objects, wrapped as Python node objects. This blends OPC UA's object-oriented model with Python's own object orientation so your code feels natural and easy to follow.
- It reduces boilerplate compared to manually constructing service requests.
- It makes address-space traversal readable and discoverable.
- It preserves OPC UA semantics while fitting naturally into Python code.
- It gives a smoother onboarding path for application developers who want the flexibility of OPC UA without leaving Python's object model.
The Node API is the bridge between OPC UA's object-oriented server model and Python's own object orientation.
Easy access to OPC UA nodes from a client¶
Servers expose server.root, server.objects, server.types, server.views,
and typed server[nodeId] lookup as local entry points into the same Node API.
See Finding existing nodes
for canonical server identity, lifecycle, and synchronous/asynchronous lookup.
When a client has connected, it exposes the server entry points as Python objects:
client.rootclient.objectsclient.typesclient.views
These objects are live views into the server. The dotted attribute syntax represents the actual OPC UA nodes in the server address space, wrapped in Python node objects. Each path component is resolved on demand through a Browse request. Client handles
cache child names and retry a cached miss with one fresh browse. Client node/reference
mutations, BrowseName writes, reconnects, and updateRemoteNamespaces() invalidate
navigation caches; a complete client.browse(node) invalidates that node's cache,
including other handles for the same NodeId.
Changes made by another client or by the server do not automatically invalidate
cached hits. Call client.invalidateNodeCache(node) to invalidate one parent's
children, or client.invalidateNodeCache() for all existing handles. The next
navigation browses again; async callers must await invalidation. Attribute reads
and writes always go to the server.
from o6 import Client
client = Client("opc.tcp://localhost:4840")
client.connect()
node = client.root.objects.MyVariable
# The node object supports call-style to access attributes
print(node()) # read the node value
print(node(attr="BrowseName")) # read a specific attribute by name
# or use the OPC UA AttributeId enum
# print(node(attr=o6.AttributeId.BROWSE_NAME))
node(123) # write a new value to the node
Expected output:
In the example above, client.root.objects.MyVariable is a direct path into the server's object hierarchy. The node returned behaves like a Python object, while still representing the underlying UA node.
Important: a node object can be safely cached for later use, only when we are sure the server model remains unchanged and the node is not being removed or renamed on the server side.
Usage¶
This page is a hands-on tour of the Node API once a client is connected. It assumes you have an OPC UA server reachable at opc.tcp://localhost:4840.
Setting the stage¶
A running example is the fastest way to learn. The snippets below assume a server that exposes an object with a few variables and a method, similar to the example server used by the tutorials:
Objects/
├── MyVariables (Object)
│ ├── MyInteger (Variable, Int32, read/write)
│ └── MyString (Variable, String, read/write)
└── TestMethods (Object)
└── Hello (Method, input: String, output: String)
The exact path is irrelevant — replace it with the structure of your own server.
What matters is the three navigation patterns you'll see below: . to browse, [] to look up by path, () to act on a node.
Connecting¶
A Node-API session is a regular Client session. The Node API is layered on top of an active client connection.
Once client.connect() returns, the client has an active session and the entry-point nodes (client.root, client.objects, client.types, client.views) are live and ready to be navigated.
Browsing¶
Browsing is how you navigate from a node to its children. Two syntaxes are available: dotted attribute access for the common case, and bracket indexing for paths that are not safe as identifiers.
Entry points¶
A connected Client exposes four top-level nodes that correspond to the four standard folders of the OPC UA address space:
| Attribute | NodeId | What's inside |
|---|---|---|
client.root |
i=84 |
The address-space root. The three folders below are also reachable from here. |
client.objects |
i=85 |
The instance address space — your application's objects, variables, methods. |
client.types |
i=86 |
The type hierarchy — ObjectTypes, VariableTypes, ReferenceTypes, DataTypes. |
client.views |
i=87 |
Server-defined views over the address space, if any. |
Dot syntax¶
Every entry point is an ObjectNode that supports the same dot, bracket, and call syntaxes as any other node. Most user code starts from client.objects; the others are useful when you need to walk the type hierarchy or query a server-defined view.
. triggers a server-side Browse for the named child. Children are resolved lazily and cached on the parent node, so repeated access to the same child is free.
# A single step — resolve one child of Objects
parent = client.objects.MyVariables
# Chained steps — each `.` resolves one level
var = client.objects.MyVariables.MyInteger
# We can continue from the cached parent
var = parent.MyInteger
Names are matched case-insensitively against BrowseName. The result is a Node subclass (VariableNode, ObjectNode, MethodNode, …) depending on the child's NodeClass.
In interactive shells, dir(node) and <TAB> completion both run a browse and list the children — see Interactive completion below.
Bracket syntax []¶
The dot syntax fails when a BrowseName is ambiguous: if a node has more than one child with the same name (case-insensitively), node.child raises because the path does not pin it down to a single target. In these cases you have to use [], which returns a list of matching targets that you can disambiguate yourself.
# Two children both called "Status" → dot syntax can't pick one
nodes = client.objects["MyDevice/Status"] # list of targets
status = nodes[0] # pick the right one
[] is translated server-side through TranslateBrowsePathsToNodeIds and handles the full range of the browse path syntax specified in the spcefication here.
Node access — the () operator¶
Once you have a node, the python call operator () handles every interaction: read, write, call.
The distinction is by argument shape, not by a different method.
What () does¶
Every Node subclass overloads __call__. Calling a node with no value is a read, calling it with a value is a write, and calling a MethodNode is a call. An optional attr= keyword selects which attribute to act on; None (the default) means the Value attribute for variables and the method invocation for methods.
# Read
node()
# Read a specific attribute
node(attr="BrowseName")
node(attr=o6.AttributeId.DISPLAY_NAME)
# Write
node(42)
node("hello")
# Call (method nodes only)
parent.MyMethod("arg1", "arg2")
The attr= keyword accepts either an o6.AttributeId member or a case-insensitive o6.AttributeId-like string such as "BrowseName", "NodeClass", "DisplayName", "Description", "DataType", "ValueRank", "AccessLevel", …
Reading values¶
Call with no argument to read the current value of a variable:
Reading a different attribute:
name = client.objects.MyVariables.MyInteger(attr="BrowseName")
print(name) # QualifiedName('1:MyInteger')
class_ = client.objects.MyVariables.MyInteger(attr="NodeClass")
Writing values¶
Pass the new value as the first positional argument:
Writing a non-Value attribute: pass both value= and attr=:
This is equivalent to client.write(nodeid, value) on the high-level client API — but the node is reached through the address-space path, so no NodeId string is required.
Calling methods¶
Method nodes are called by passing the input arguments positionally. When the method is reached through its parent object, the parent is picked up automatically:
result = client.objects.TestMethods.Hello("World")
print(result) # (StatusCode.GOOD, "Hello, World!")
Typed Method calls return (StatusCode, *output_arguments) on success and
raise o6.StatusCodeError for a failed method or service. This applies to local
and remote handles. Remote handles now check method statuses explicitly. Use
client.call() or server.call() to receive bad method statuses in the tuple;
those APIs still raise for service failures.
The Object is part of an OPC UA Call; it is not intrinsic to Method identity.
Dot lookup returns a lightweight bound Method containing both the Object used
for that lookup and the Method node. A Method obtained directly by
NodeId has no lookup context, so pass the Object explicitly with object=. It
can be an Object node or a NodeId-like value:
addReference only adds an address-space edge. It does not change which
callback a Method uses or mutate the Method node. If the same Method is
referenced from several Objects, every dot lookup receives its own binding and
therefore calls it with the correct Object.
Methods that take no inputs are invoked with an empty call:
Async — one rule covers everything¶
In async tasks, calls on both client and server nodes are always awaitable,
including local reads and writes that complete immediately. Sync and async share the same syntax — just add or drop the await:
import asyncio
from o6 import Client
async def main():
client = Client("opc.tcp://localhost:4840")
await client.connect()
# Read
value = await client.objects.MyVariables.MyInteger()
# Write
await client.objects.MyVariables.MyInteger(42)
# Call
greeting = await client.objects.TestMethods.Hello("World")
# Browse (also awaitable)
node = await client.objects.MyVariables.MyInteger
await client.disconnect()
asyncio.run(main())
Under the hood, a sync call on a Client schedules a coroutine on the client's background loop and blocks the caller; an await on the same expression suspends until the result is available. The dot syntax, bracket syntax, and call syntax are identical in both modes — only the await keyword changes.
Firing events — the << operator (server-side)¶
The three operators above work from a Client or a Server. A fourth, <<, is server-only: on an ObjectNode or ViewNode handle, it fires an event sourced at that node without going back to the owning Server object:
pump << o6.Event(server, message="Cavitation detected")
pump << {"message": "Cavitation detected", "severity": 400} # a plain mapping works too
<< is only defined for Object and View nodes — the two NodeClasses OPC UA defines an EventNotifier attribute for — so using it on any other node class raises TypeError immediately, naming the reason. The same is true for a node obtained from a Client, since local event creation is a server-side operation.
This section is not the full picture — see Events for eventType inference, reusable event drafts, and the parentheses required when awaiting << on an externally-driven event loop.
Interactive completion¶
The Node API is designed to be driven from a Python REPL (the standard >>> prompt, IPython, or any IDE / notebook that uses Jedi for completion):
client.objects.<TAB>shows the children of theObjectsfolder.- After a child is resolved,
<TAB>on it shows its children, and so on. dir(node)returns the same names explicitly.- Type hints on
Node(via the bundled.pyistubs) let your editor autocomplete theattr=keyword, thevalue=keyword for writes, and the availableo6.AttributeIdvalues.
>>> from o6 import Client
>>> client = Client("opc.tcp://localhost:4840")
>>> client.connect()
>>> client.objects.<TAB>
# Lists the children of the Objects folder:
# MyVariables TestMethods Server ...
>>> client.objects.MyVariables.<TAB>
# Lists the children of the MyVariables object:
# MyInteger MyString ...
>>> client.objects.MyVariables.MyInteger.<TAB>
# Lists the Node API members on the resolved node:
# nodeid value ...
>>> client.objects.MyVariables.MyInteger(attr=<TAB>
# Lists the AttributeId enum members:
# NODEID NODECLASS BROWSENAME DISPLAYNAME VALUE ...
The dotted navigation fetches and caches child nodes on demand; the first <TAB> after connecting pays a single Browse round-trip, subsequent <TAB> presses on the same node are served from the cache.
dir(node) exposes Python aliases for child BrowseNames. Instance aliases use
the same spelling rules as generated model members: My Variable becomes
mySpaceVariable, My-Variable becomes myMinusVariable, 3DPosition becomes
threeDPosition, and Class becomes class_. Type nodes keep class-style names,
replacing punctuation with underscores, for example My Type becomes My_Type.
Aliases that would start with _ receive a node prefix (Node for types), so
they remain accessible without looking up private Python attributes.
Unicode aliases are normalized to the identifier spelling Python uses in source code.
The original BrowseName is unchanged and remains usable through getattr or a
namespace-qualified browse path (namespace index 1 in this example):
client.objects.mySpaceVariable() # Read using the Python alias.
getattr(client.objects, "My Variable")() # Read using the original BrowseName.
client.objects["/1:My Variable"][0]() # Resolve by browse path and read.
Exact original names take precedence; otherwise lookup matches original names
and aliases case-insensitively. Collisions, such as My Variable and
MySpaceVariable both appearing as mySpaceVariable, raise AttributeError
instead of selecting an arbitrary child. Use an exact original name or a browse
path to disambiguate. Existing Python attributes retain normal precedence over
child lookup; browse paths remain available when those attributes hide a child.
On the client's owning event loop, dir(node) lists cached children and Python
attributes without making a remote request. Use await client.browse(...) for
fresh discovery in that context.
Tip:
dir(node)returns a list synchronously, even though the children are resolved from the server. It is implemented withasyncio.run_coroutine_threadsafeagainst the client's background loop, so it works in a regular interactive shell withoutawait.
Putting it together¶
from o6 import Client
with Client("opc.tcp://localhost:4840") as client:
# browse (dot and bracket)
var = client.objects.MyVariables.MyInteger
method = client.objects["TestMethods/Hello"]
# read
print("before:", var())
# write
var(7)
# read again
print("after: ", var())
# call
greeting = method("o6")
print("call: ", greeting)
Expected output (values will differ):
Quick reference¶
| Operation | Node API | Equivalent service call |
|---|---|---|
| Browse by name | node.child |
Browse |
| Translate path | node["a/b/c"] |
TranslateBrowsePathsToNodeIds |
| Read value | node() |
Read of Value |
| Read attribute | node(attr="BrowseName") |
Read of the given attribute |
| Write value | node(42) |
Write of Value |
| Write attribute | node(value=..., attr=...) |
Write of the given attribute |
| Read via property | var.value |
Read of Value |
| Call method | parent.MyMethod(arg1, arg2) |
Call |
| Async variant | await … (same syntax) |
— |
| List children | dir(node) / <TAB> |
Browse |
| Fire an event (server-side) | node << o6.Event(...) |
— |
For the underlying service calls (when you need full control over the request payload), see the Client and Service API reference.