Hello, world
The shortest useful Typhon program, end to end: scaffold a project, write code, type-check, compile to Python, and run the emitted Python. This page assumes you have already installed tyc.
Scaffold a project
-
Create the project:
Terminal window tyc init hellocd hello -
tyc initproduces:hello/├── typhon.toml # project config├── src/│ └── main.ty # entry point└── tests/ -
The default
typhon.toml:[project]name = "hello"version = "0.1.0"src = "src"out = "build"[python]target = "3.13"[emit]class-default = "dataclass"format = true[strictness]no-implicit-any = trueunused-import = "error"exhaustive-match = "error"You can edit these later; the defaults are conservative.
Your first program
Edit src/main.ty:
def main() -> None: print("Hello, world")
if __name__ == "__main__": main()Three things to notice:
- Return types are required.
-> Noneis not optional — Typhon’s[strictness] no-implicit-anydefaults totrue. Omit it andtyc checkwill complain withtyc::missing_annotation. printworks. Built-in Python functions are available without ceremony; Typhon is a (stricter) superset of typed Python.if __name__ == "__main__":is unchanged. Typhon emits idiomatic Python; module entrypoints look identical.
Check, build, run
-
Type-check. This runs everything up to the analyser without emitting any
.py.Terminal window tyc check src/If everything is well-typed, the command exits 0 with no output.
-
Build. Runs the full pipeline: lex, parse, resolve, type-check, desugar, emit, and (if
[emit] format = true) post-process withruff format.Terminal window tyc buildYou get
build/main.py. If you usedResult[T, E],go, orlazy let, you also getbuild/typhon_runtime.py(and friends). -
Run. Standard Python — no Typhon involvement:
Terminal window python build/main.py# Hello, worldOr use the convenience shortcut:
Terminal window tyc run # builds into build/, then execs python build/main.pytyc run --temp # builds into a tempdir; nothing persists
What does the emitted Python look like?
build/main.py:
def main() -> None: print("Hello, world")
if __name__ == "__main__": main()For Hello, world the input and output are byte-identical (formatting aside). That’s the point: every .ty file emits valid, idiomatic .py. There is no Typhon runtime to install on production servers — Typhon is a build-time tool, like TypeScript.
A slightly less trivial example
Take a name from argv and greet it:
import sys
def main() -> None: let name: str = sys.argv[1] if len(sys.argv) > 1 else "world" print(f"Hello, {name}")
if __name__ == "__main__": main()from __future__ import annotationsimport sys
def main() -> None: name: str = sys.argv[1] if len(sys.argv) > 1 else "world" print(f"Hello, {name}")
if __name__ == "__main__": main()New things:
let name: str = ...— an immutable local binding with an explicit type. Usemutinstead if you want to reassign it later. (Values and Bindings goes deep on this.)- Top-level imports —
import sysis unchanged from Python. Typhon addslazy importfor deferred loading (seelazy), but plainimportstill works.
Compile and run:
tyc buildpython build/main.py Alice# Hello, AliceCommon first-time errors
Forgetting the return type
def main(): # ❌ missing return annotation print("Hello")error[tyc::missing_annotation]: `return type` on `main` is missing a type annotation ┌─ src/main.ty:1:5 │1 │ def main(): │ ^^^^ annotation required here = help: Typhon's Rule 1: annotate every parameter and return type. For a function that returns nothing, write `-> None`.Fix: write def main() -> None:.
Using = for a local binding without let or mut
def main() -> None: name = "Alice" # ❌ missing let/mut print(name)error[tyc::missing_binding_kind]: local bindings must be declared with `let` or `mut` ┌─ src/main.ty:2:5 │2 │ name = "Alice" │ ^^^^ add `let` (immutable) or `mut` (mutable) hereFix: let name: str = "Alice" or mut name: str = "Alice".
Inferring Any
import some_library
def main() -> None: let data = some_library.fetch() # infers Any silentlyAny is the top type — it flows freely through assignments, so the checker doesn’t reject the binding above. The recommended convention is to wrap untyped boundaries in unsafe: so reviewers can see where the dynamism lives:
def main() -> None: unsafe: let raw = some_library.fetch() let data: dict[str, int] = dict(raw) # re-assert at the boundaryLong-term fixes: write a .dty stub for the library, or annotate the binding explicitly (let data: dict[str, int] = ...).
What you’ve learned
- How to scaffold a project with
tyc init. - The three commands you’ll use daily:
tyc check,tyc build,tyc fmt. - That Typhon emits clean Python with no runtime dependency.
- The first three diagnostics you’ll see and how to fix each.
Where next
- Project Layout — what
tyc initproduces and why. - Your First Real Program — beyond Hello World: a small typed CLI.
- The Five Rules — the rules every Typhon program follows; internalise these and the checker stops surprising you.