Dragon

Bytes and Encoding

A str is text - a sequence of Unicode code points. But the world outside your program speaks in bytes: a file on disk, a socket frame, a hash digest, a binary protocol. Conflating the two is the source of a whole category of bugs, which is why Dragon, like Python 3, keeps them as distinct types. str is for text; bytes is for raw octets; and you cross between them explicitly, with encode and decode, at the edges of your program.

Bytes literals

A bytes literal is a string prefixed with b. Printable ASCII appears as-is; anything else is written with \xNN hex escapes:

const greeting: bytes = b"hello"
const frame: bytes = b"\xde\xad\xbe\xef"
const mixed: bytes = b"GET \x2f HTTP/1.1"

Length, indexing, slicing

len is the byte count. Indexing a single position gives you an int in the range 0-255 - the byte's value, not a one-character string. Slicing gives you back bytes:

const b: bytes = b"hello"
print(len(b))      # 5
print(b[0])        # 104   - an int (ord of 'h'), not "h"
print(b[1:3])      # b'el' - a slice is bytes

This int-on-index behavior is the key difference from str, and it's deliberate: a byte is a number.

Concatenation

+ joins bytes objects, just like strings:

const c: bytes = b"hello" + b" world"
print(c)           # b'hello world'

Hex

.hex() renders bytes as a lowercase hex string; the bytes.fromhex constructor goes the other way:

const digest: bytes = b"\xde\xad\xbe\xef"
print(digest.hex())              # deadbeef
print(bytes.fromhex("deadbeef")) # b'\xde\xad\xbe\xef'

Crossing the boundary: encode and decode

str.encode turns text into bytes; bytes.decode turns bytes back into text. Both default to UTF-8 - the encoding you want almost always - and both accept an explicit encoding name:

const text: str = "café"
const raw: bytes = text.encode("utf-8")
print(len(text))            # 4   - code points
print(len(raw))             # 5   - UTF-8 bytes (é is two bytes)
print(raw.decode("utf-8"))  # café

len(text) is 4 and len(raw) is 5 - the same text/bytes distinction from Strings and Unicode, now made concrete: the é is one code point but two UTF-8 bytes.

Encoding errors are catchable

Strict decoding of invalid bytes raises UnicodeDecodeError; strict ASCII encoding of non-ASCII text raises UnicodeEncodeError. Both are ordinary exceptions you can catch (see Exceptions):

try {
    const bad: bytes = b"\xff\xfe"
    const s: str = bad.decode("utf-8")
    print("decoded")
} except UnicodeDecodeError {
    print("UnicodeDecodeError caught")     # this fires
}

If you'd rather not fail, pass "replace" as the error handler: invalid input becomes the replacement character U+FFFD on decode, or ? on an ASCII encode, instead of raising.

Supported encodings

Dragon's codec set is deliberately small and fast: utf-8 (aliases utf8, u8) and ascii (alias us-ascii). UTF-8 is the universal default and covers essentially every modern text need; ASCII is there for the protocols that require it. Asking for any other codec name raises LookupError - Dragon doesn't ship the long tail of legacy encodings in core.

Where the boundary belongs

The discipline that keeps text code clean: keep data as str while you work with it, and drop to bytes only at the I/O edge - reading or writing a binary file, framing a socket message, computing a digest, building a wire protocol. The crypto and hashing APIs in Cryptography and Hashing operate on bytes for exactly this reason.

At a glance

You want to...Write
A bytes literalb"...", b"\xde\xad"
Byte countlen(b)
One byte (as int)b[i]0-255
A slice (as bytes)b[a:c]
Concatenateb1 + b2
To/from hexb.hex(), bytes.fromhex(s)
Text → bytess.encode() (UTF-8) / s.encode("ascii")
Bytes → textb.decode() / b.decode("utf-8", "replace")
Handle bad inputexcept UnicodeDecodeError or errors="replace"

That completes Part 4. Next, the container types text so often flows into and out of - starting with Lists.