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¶
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, cached for the lifetime of the node, and invalidated automatically when the model changes.
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:
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¶
Every call above is awaitable. 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.
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.
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 |
For the underlying service calls (when you need full control over the request payload), see the Client and Service API reference.