Skip to content

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 Child inherits from Base (both decorated, same owner), decode a Child payload as Child. Asking for it as Base raises NotImplementedError today rather than guessing.
  • A reference field may be None. Test it with is None or in an if before using it; reading a field through None raises AttributeError, as in Python.
  • Text fields must be valid text. A str field checks its UTF-8 when you assign it; if you need arbitrary bytes, declare bytes.
  • 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.