Dragon

Callable, TypedDict, and intc

Three more annotations finish the type system's vocabulary: Callable types a function value, TypedDict types a dict with a fixed schema, and intc bridges the gap to C's 32-bit int at the FFI boundary. Each fills a specific need a plain int/str/list annotation can't.

Callable[[ArgTs], R] - a function value

Functions are first-class, and the type of "a function taking an int and returning an int" is Callable[[int], int] - the argument types in a bracketed list, then the return type:

def apply(f: Callable[[int], int], x: int) -> int {
    return f(x)
}

def square(n: int) -> int {
    return n * n
}

print(apply(square, 6))   # 36

It accepts a lambda just as readily, which is the idiom for passing behavior into a higher-order function:

def transform(xs: list[int], fn: Callable[[int], int]) -> list[int] {
    out: list[int] = []
    for x in xs {
        out.append(fn(x))
    }
    return out
}

print(transform([1, 2, 3], lambda (n: int) -> int { return n * 10 }))
# [10, 20, 30]

The syntax mirrors Python's typing.Callable. A no-argument function is Callable[[], R]; a two-argument one is Callable[[int, str], bool]. See Functions for the closures and higher-order patterns this type unlocks, and Type Annotations for aliasing a long callable type with type Handler = Callable[[Request], Response].

TypedDict - a dict with a fixed schema

When a dict has a known set of keys with known per-key types - a config object, an API response, a database row - describe it with a TypedDict: a class with a TypedDict base and one field declaration per key.

class User(TypedDict) {
    name:   str
    age:    int
    active: bool
}

Construct it by calling with the fields, and read them back with dot-access (the typed shorthand for string-keyed dicts) or the subscript form:

u: User = User(name="Ada", age=36, active=true)
print(u.name)        # Ada    - typed as str
print(u.age)         # 36     - typed as int
print(u.active)      # True   - typed as bool

Under the hood a TypedDict is a dict - deliberately. That means **spread, .items(), runtime-key access, and passing it where a dict is expected all work. A TypedDict is the right tool when the schema is fixed and you want field access checked at compile time; a plain [dict[K, V]](/docs/0502-dictionaries) is the tool when keys are dynamic. TypedDict slots also compose - a list[User] is a list of typed dicts, and users[0].name reads through to the typed field.

intc - the C-FFI bridge

A Dragon int is 64-bit; C's int is usually 32-bit. When you declare a foreign function with extern "C", the types must match the C ABI exactly or you'll pass 64 bits where the callee reads 32. intc is the type for that boundary - it lowers to i32:

extern "C" def usleep(usec: intc) -> intc

intc is an FFI tool, not a general-purpose number. Use it only in the extern "C" signature; the value widens to a normal int the moment it's back in Dragon code:

import random
random.srand(42)
n: int = random.rand()      # rand() returns intc; widens to int
print(n > 0)                # True

The FFI chapter covers the full set of boundary types.

At a glance

You want to express...Annotation
A function valueCallable[[int], int]
A no-arg functionCallable[[], R]
A fixed-schema dicta class with a TypedDict base
Read a typed keyu.name (dot) or u["name"] (subscript)
A C int at the boundaryintc (only in extern "C")

That rounds out the concrete annotations. The last piece of the type system writes one definition that specializes to native code for every type you use it with: Generics.