Dragon

Any - the Dynamic Escape Hatch

A union says "one of these types." Sometimes you genuinely don't know the type until runtime - a parsed JSON leaf, a config value, a heterogeneous record off the wire. That's what Any is for. It's Dragon's interface{}: the same 16-byte tagged box a union uses, but able to hold anything. It exists so the rest of your program can stay strictly typed and fast

  • you opt into dynamism in exactly the one corner that needs it, and nowhere else.

Holding anything

An Any slot can be reassigned across types:

x: Any = 42
print(x)        # 42
x = "hello"
print(x)        # hello
x = 3.14
print(x)        # 3.14

The headline use: dict[str, Any]

The reason Any earns its place is dict[str, Any] - the typed map of heterogeneous values that every serialization story needs. The hot, monomorphic dicts (dict[str, int], dict[str, str]) stay native and free; Any is a second tier you reach for only where the data is actually mixed:

cfg: dict[str, Any] = {}
cfg["name"] = "my-app"
cfg["port"] = 8080
cfg["debug"] = true

port: Any = cfg["port"]
if isinstance(port, int) {
    print(port + 1)        # 8081 - narrowed to a native int
}

To do anything with an Any value you must narrow it with isinstance first - the compiler rejects using a raw Any as a concrete type, exactly as it does for a union. Reading a value by key and narrowing it is the correct, checked path.

Any vs list[Any]

A bare Any is layout-agnostic: it can hold any value - including a concrete list[str] - and every dynamic operation on it dispatches on the value's real runtime type. list[Any] is a stronger claim: it pins a concrete runtime layout (one 16-byte box per element), which is different from a concrete list's flat 8-byte slots. So a value moves between list[T] and list[Any] only when it is a fresh literal (built in the right layout from birth) or copied element-wise - never by aliasing an existing list under a new annotation. The compiler enforces this at assignments and call sites, and the runtime enforces it with a TypeError when unboxing an Any into a typed list view. Lists walks through the rule with examples.

What it costs

The cost is real but bounded. An Any slot is 16 bytes instead of 8; every read pays one extra load and a tag check; and you must narrow before use. That tag-check-then-unbox is the price of dynamism - cheap and predictable, but not free the way dict[str, int] is. Use Any where the data is genuinely heterogeneous; don't reach for it to dodge writing a real type. It exists precisely so the typed path stays fast: you never widen dict[str, int] to carry boxes just because one corner of the program needs dict[str, Any]. This is also why JSON decoding lands in Any - see Data Formats.

Any is not type introspection

Any defers a type to runtime by design, and isinstance is the bounded, checked way to recover it. That's the only runtime type question Dragon answers - there is no type(x) returning a type object, no getattr/hasattr, no attribute discovery. The static type system already knows the shape of everything that isn't Any; Any is the one place you explicitly hand a type to runtime, and isinstance is how you take it back.

At a glance

You want to...Write
A slot holding anythingx: Any = ...
A heterogeneous mapcfg: dict[str, Any] = {}
Read a value outv: Any = cfg["k"] then narrow
Use it as a concrete typeif isinstance(v, int) { v + 1 }

Any is the dynamic tier. The next chapter covers three more annotations you'll reach for constantly - function values, fixed-schema dicts, and the C-FFI integer: Callable, TypedDict, and intc.