Control flow, None & errors¶
Branching, looping, and iteration in TurboPython are ordinary Python. This page
starts with the everyday constructs, comprehensions, and pattern matching, then
turns to the parts that carry TurboPython-specific rules: narrowing away
None, exceptions and panics, and writing iterators and generators.
Loops and conditionals¶
if/elif/else, for, while, break, continue, and range behave as
in Python:
def main():
total = 0
for i in range(10):
if i == 5:
break # stop at 5
if i % 2 == 1:
continue # skip odds
total += i
print(total) # 0 + 2 + 4 = 6
n = 20
while n > 1:
n //= 2
print(n) # 1
main()
A conditional expression such as "odd" if n % 2 == 1 else "even" works as in
Python. Context managers work as well: with drives both built-in resources
such as open files and any class that defines __enter__ and __exit__.
Comprehensions¶
List, set, and dict comprehensions and generator expressions all work as in Python:
def main():
squares = [x * x for x in range(5)]
evens = {x for x in range(10) if x % 2 == 0}
index = {x: x * x for x in range(4)}
total = sum(x * x for x in range(5)) # generator expression
print(squares) # [0, 1, 4, 9, 16]
print(len(evens)) # 5
print(index[3]) # 9
print(total) # 30
main()
Pattern matching¶
match branches on the shape of a value, an alternative to a chain of
if/elif. It handles primitive values with literal and OR patterns and an
optional guard, and a final case _ catches the rest:
def classify(n: int) -> str:
match n:
case 0:
return "zero"
case 1 | 2 | 3: # OR pattern
return "small"
case _ if n < 0: # guard
return "negative"
case _: # wildcard
return "anything else"
def main():
print(classify(0)) # zero
print(classify(2)) # small
print(classify(-5)) # negative
print(classify(99)) # anything else
main()
match also destructures classes, binding the fields a pattern names
(case Circle(radius=r)), and branches on the members of a union, where the
compiler checks that every member is covered. Data modeling
shows both forms. A pattern can bind the whole value with as, as in
case Circle() as c.
Sequence, tuple, and mapping patterns (case [a, b], case {"k": v}) are not
available yet.
Narrowing away None¶
A value of type T | None cannot be used as a plain T; the compiler warns
and inserts a runtime check. A check on the value narrows it, so inside the
checked branch the type is T and the warning is gone. is None and
is not None comparisons, truth-value checks (if r:), early returns, and
assert all narrow:
from tpy import Own
class Reading:
sensor: str
values: list[float]
def __init__(self, sensor: str, values: Own[list[float]]):
self.sensor = sensor
self.values = values
def label(r: Reading | None) -> str:
if r is None:
return "no reading"
return r.sensor # r is Reading here
def sensor_of(r: Reading | None) -> str:
assert r is not None # narrows too
return r.sensor
def main():
print(label(None)) # no reading
print(label(Reading("boiler-3", [20.5]))) # boiler-3
print(sensor_of(Reading("boiler-7", [19.0]))) # boiler-7
main()
The rule is to narrow once, close to where the None can appear, and to pass
plain T onward. Functions that take T | None make every caller
repeat the check.
A failed assert raises an AssertionError, catchable like any exception;
uncaught, it stops the program. Optimized builds keep asserts — there is no
CPython -O stripping.
Exceptions¶
raise, try/except/finally, exception classes, and except ... as e
all behave as in Python. An exception class is an ordinary class deriving from
Exception:
class ParseError(Exception):
pass
def parse(text: str) -> int:
if len(text) == 0:
raise ParseError("empty input")
return int(text)
def main():
try:
print(parse("42")) # 42
finally:
print("always runs")
try:
parse("")
except ParseError as e:
print("bad:", str(e)) # bad: empty input
main()
except on a base class catches derived exceptions, and a bare raise
re-raises the active one.
A try block costs nothing while nothing is raised; actually raising is
comparatively expensive. Exceptions are the right shape for failures, not for
hot-path control flow — which is why __next__ gets special treatment below.
Silent difference from CPython
For a user-defined exception class, print(e) prints the exception
object rather than its message, and no diagnostic flags this today.
Built-in exceptions print the message, matching CPython. str(e) and
f-string formatting (f"failed: {e}") return the message on both
runtimes, as in the example above.
Exceptions are for failures the program can handle. They are distinct from panics — integer overflow, a failed narrowing conversion — which stop the program and cannot be caught (the types page covers them).
Iterators¶
A custom iterator is written exactly as in Python: __iter__ returns the
iterator, __next__ returns the next value or raises StopIteration. The
same class runs unchanged under CPython:
from tpy import Int32
class Countdown:
n: Int32
def __init__(self, n: Int32):
self.n = n
def __iter__(self) -> "Countdown":
return self
def __next__(self) -> Int32:
if self.n == 0:
raise StopIteration
self.n -= 1
return self.n + 1
def main():
for i in Countdown(3):
print(i) # 3 2 1
main()
The raise StopIteration costs nothing at runtime; ending iteration is
signaled without actually throwing. Writing efficient TPy
covers the @error_return mechanism behind this and its explicit form.
Generators¶
A generator function uses yield and is annotated -> Iterator[T]. It is the
concise form of the iterator above, with no explicit __next__:
from typing import Iterator
def squares(n: int) -> Iterator[int]:
for i in range(n):
yield i * i
def main():
for s in squares(4):
print(s) # 0 1 4 9
main()
The next page consolidates all of this into performance guidance: Writing efficient TPy.