Typed objects: DOBJ¶
This page shows how to make a Python object that can leave your program. You will declare a class with a d/OS schema, turn a whole graph of those objects into bytes, turn the bytes back into fresh objects with every link intact, and see why that is the foundation of copy, paste and drag and drop on d/OS. It runs in the playground and on macOS.
Why objects need a schema¶
On most systems, copy and paste means text, and drag and drop means a file path. d/OS does something stronger: programs hand each other typed objects. A spreadsheet range, a styled paragraph, a card on a board, a game piece: each one crosses to another program as a value with a known shape, not as a pointer into someone's memory and not as a screenshot of text. d/OS calls that shape a schema and the travelling value a DOBJ, short for d/OS object. Both d/Python and d/BASIC use the same schemas and the same byte format, so an object written by one language reads correctly in the other.
Declare one¶
import dos
@dos.dobj("NODE", owner="442f4f532d4150503030303930303031")
class Node:
count: dos.i32 = 0
title: str = ""
raw: bytes = b""
next: "Node" = None
node = Node()
node.count = 42
node.title = "Zoë 🐍"
print(node.title, node.count, node.next is None)
The decorator takes two things. The name, "NODE", is the schema's name. The owner is the identity of the application that defines the schema: sixteen bytes, written as 32 hexadecimal digits. The example owner spells out D/OS-APP00090001 in ASCII, which is d/OS's standard form: the text D/OS-APP followed by the application's ID in hexadecimal. Both must be literals in the source, because they are part of the object's identity, not something computed at run time.
Every field needs a type annotation and a default, and the types are the ones a schema can carry:
| Annotation | What is stored | What Python sees |
|---|---|---|
dos.i16, dos.i32 |
a signed 16-bit or 32-bit integer | a Python int; assigning a value outside the range raises OverflowError before anything changes |
dos.f32, dos.f64 |
a single- or double-precision float | a Python float; f32 stores round to the nearest single |
dos.decimal |
d/OS's fixed-point decimal with four places | write the default as dos.decimal("1.2345") |
bytes |
raw bytes, any values | bytes |
str |
text, checked as valid UTF-8 when stored | str |
| another class with the same owner | a reference to another object, or None |
the object; the default must be None |
A field's type is fixed by its annotation. You cannot add fields the class did not declare, and you cannot change the class-level defaults. Ordinary Python classes without the decorator are untouched by any of this.
Encode a graph, decode a copy¶
import dos
@dos.dobj("NODE", owner="442f4f532d4150503030303930303031")
class Node:
count: dos.i32 = 0
title: str = ""
raw: bytes = b""
next: "Node" = None
original = Node()
original.count = 42
original.title = "Hello, D/OS"
original.raw = b"\x00\xff"
original.next = original
payload = dos.dobj_encode(original)
copy = dos.dobj_decode(Node, payload)
copy.title = "Independent copy"
print("Count:", copy.count)
print("Original:", original.title)
print("Copy:", copy.title)
print("Cycle preserved:", copy.next is copy)
print("Raw bytes:", repr(copy.raw))
dos.dobj_encode freezes the object and everything it points to into one immutable byte string, the payload. The example object points at itself, and the payload records that relationship rather than looping forever. dos.dobj_decode builds brand-new objects from the payload: the copy is independent (changing its title leaves the original alone), its self-reference now points at the copy, and the raw bytes come back exactly, including the zero byte. Constructors do not run during decode; the fields are filled in directly from the bytes.
That is the whole trick behind interchange. Once an object is a payload, it can be published to the pasteboard, offered in a drag, or stored, and any program that knows the schema can rebuild it.
Rules that keep it honest¶
- Decode the exact class you encoded. If
Childinherits fromBase(both decorated, same owner), decode aChildpayload asChild. Asking for it asBaseraisesNotImplementedErrortoday rather than guessing. - A reference field may be
None. Test it withis Noneor in anifbefore using it; reading a field throughNoneraisesAttributeError, as in Python. - Text fields must be valid text. A
strfield checks its UTF-8 when you assign it; if you need arbitrary bytes, declarebytes. - A payload is at most 65,535 bytes in the current runtime. A graph larger than that is declined, never truncated.
- Encoding is local. Encoding and decoding need no permission from d/OS. Publishing an object so that another program can read it is a separate step with its own checks, on the copy and paste page.
What is not there yet¶
Lists and dictionaries as fields, records and arrays, weak references, deciding a schema automatically from an ordinary class, and decoding a subclass through its base type are not yet supported; each is a named entry in the compatibility table. d/OS's own object format can express more than d/Python exposes today.
Packaging¶
When you package an application, dpython package writes the schemas of every decorated class into the package, so a d/OS installation knows exactly which object shapes the application defines and owns. An installed schema is what lets another program trust a DOBJ it receives from yours. See Packaging.
Next: Copy & Paste, where a payload leaves your program.