Control Flow
Control flow in Dragon is small, familiar, and Python-shaped - if / elif / else, while, for, match, plus break and continue. The two adjustments for .dr mode are curly-brace blocks and parentheses-free conditions.
if / elif / else
const score: int = 78
if score >= 90 {
print("excellent")
} elif score >= 70 {
print("good")
} elif score >= 50 {
print("passing")
} else {
print("needs work")
}
The condition is any expression that evaluates to bool. Truthiness is not automatic - if 0 { ... } is a type error. Use if value != 0, if not value, or if len(s) > 0 instead.
Conditional expressions
The Python ternary works:
const status: str = "ok" if response_code < 400 else "error"
Use this for short, one-shot value selections. For anything longer or with side effects, write a normal if statement.
while loops
n: int = 10
while n > 0 {
print(n)
n = n - 1
}
print("liftoff")
A while runs its body as long as the condition is true. Use break to exit early, continue to skip to the next iteration:
while true {
const line: str = read_line()
if line == "" {
break
}
if line.startswith("#") {
continue
}
process(line)
}
for loops
for x in iterable walks any iterable - lists, tuples, sets, dicts, strings, generators, ranges:
for name in ["Alice", "Bob", "Carol"] {
print(f"hi, {name}")
}
for i in range(0, 10) {
print(i)
}
Iterating a dict iterates its keys, just like Python. Use .items() to get key-value pairs:
for word in counts {
print(f"{word}: {counts[word]}")
}
# Or:
for word, count in counts.items() {
print(f"{word}: {count}")
}
break and continue work in for loops the same way they do in while loops. The Python else clause on loops works too:
for candidate in primes {
if candidate == target {
print("found")
break
}
} else {
print("not found")
}
The else runs only when the loop completed without hitting a break.
match statements
match does structural pattern matching, like Python 3.10+. In .dr mode each arm uses a brace body (the .py form uses the indented case …: block):
const value: str | int = parse(token)
match value {
case 0 { print("zero") }
case int() { print("a non-zero int") }
case "" { print("empty string") }
case str() { print("a non-empty string") }
}
Supported patterns today: literals (0, "hi", True, None), type tests (case int(), case str(), case MyClass() - a type test matches by runtime tag for a Union/Any subject and by a non-null check for a Class | None, and an instance of a subclass matches its base), OR-patterns (case int() | bool()), sequence patterns (case [a, b] / case (a, b)), capture (case n, binds), and wildcard (case _).
A type test does not bind or narrow the subject - use it to dispatch, then read the value in the body. Class field destructuring (case Point(x, y)) and dict/mapping patterns are not implemented yet and report a clear error.
Range and enumerate
range(stop) and range(start, stop[, step]) produce numeric iterators:
for i in range(5) {
print(i) # 0, 1, 2, 3, 4
}
for i in range(10, 0, -1) {
print(i) # 10, 9, ..., 1
}
enumerate(iterable) pairs items with their index:
for i, name in enumerate(["Alice", "Bob"]) {
print(f"{i}: {name}")
}
Both are the same as Python's builtins and behave identically.
A worked example
A function that finds the first non-empty line in a list of lines and returns its index, or -1 if there isn't one:
def first_nonempty(lines: list[str]) -> int {
for i, line in enumerate(lines) {
if len(line.strip()) > 0 {
return i
}
}
return -1
}
Try a few variations: rewrite it without enumerate. Rewrite it as a while loop. Rewrite it using a list comprehension and next(). Each form has its place; pick the one that reads best for your team.
You now have everything you need to write small, useful Dragon programs. The next chapter turns to functions - defining them, passing arguments and keyword arguments, returning values, and writing higher-order functions.
