Meet the language.
Typhon looks like Python because it is Python, with a few rules that differ on purpose. Here is what those rules catch, and what the compiler turns your code into.
What you deploy is Python
The compiler writes readable CPython 3.13: dataclasses, PEP 695 generics, match and asyncio. Nothing Typhon-specific is installed in production.
@gatherable
async def fetch_user(uid: UserId) -> User: ...
@gatherable
async def fetch_posts(uid: UserId) -> list[Post]: ...
async def dashboard(uid: UserId) -> str:
gather:
user = fetch_user(uid)
posts = fetch_posts(uid)
return f"{user.name}: {len(posts)} posts"async def dashboard(uid: UserId) -> str:
async with asyncio.TaskGroup() as __typhon_tg_0__:
__typhon_gather_1__ = __typhon_tg_0__.create_task(fetch_user(uid))
__typhon_gather_2__ = __typhon_tg_0__.create_task(fetch_posts(uid))
user = __typhon_gather_1__.result()
posts = __typhon_gather_2__.result()
return f"{user.name}: {len(posts)} posts"Real output from tyc 1.0.0-beta.2. gather: runs independent awaits on a TaskGroup, so if one fails the rest are cancelled.
let port: int = parse_port(raw_port)?__typhon_q_0__ = parse_port(raw_port)
if isinstance(__typhon_q_0__, __typhon_Err__):
return __typhon_q_0__
port: int = __typhon_q_0__.value? becomes an ordinary isinstance check, so stack traces stay clean. Source maps take any traceback back to the line of .ty that caused it.
What tyc refuses to compile
Each of these runs in Python until the day it doesn't. There are 94 diagnostics in all, each with its own page and an offline tyc explain entry.
nullable_useA value that might be None is used where it can't be.
greet(find_user(2))non_exhaustive_matchA match over a sealed union skips one of its variants.
case Circle(r): … # no Squareimmutable_assignSomething declared with let is assigned again.
let n: int = 1 n = 2missing_awaitSynchronous code calls an async function and drops the coroutine.
let u = fetch_user(1)result_error_mismatchA ? passes on an error type the function never declared.
parse(raw)?missing_annotationA parameter or return type has no annotation.
def add(a, b):
The eight rules
If you know Python, these are the places Typhon will tell you no.
- Every parameter and return type is annotated.There is no inference fallback and no implicit Any.
- Local names are declared with let or mut.let cannot be reassigned. Use mut when you mean to.
- A plain type cannot hold None.Write str? when it can, and narrow it before use.
- Methods live in impl blocks.Classes hold data, and their constructors are generated.
- Any only comes in through unsafe: or .dty stubs.An as! cast checks the shape at runtime instead.
- A match over a sealed union is exhaustive.Add a variant and every match that misses it fails.
- Errors are returned as Result[T, E].Propagate them with ?, and turn exceptions into values at the edges.
- A let declared without a value is assigned before use.Reading it on a path that never assigned it is an error.
Learn the rest
The documentation has a ten-lesson tour, the full language reference and a cheat sheet. If you have typed Python already, tyc migrate will convert it for you to start from.