Taipan for Python developers
Taipan is written in Python’s syntax, so most Python code reads the same. The differences come from one decision: types are checked when you compile, not when a line runs. This page lists what that changes.
What stays the same
Section titled “What stays the same”Indentation, def, class, @dataclass, for and while, if/elif/else, try/except/finally, with, lambda, comprehensions, f-strings with format specs, slicing, *args and **kwargs, generators with yield, async/await, and the built-ins you know: print, len, range, enumerate, zip, sorted, min, max, sum, any, all, map, filter, isinstance, open.
@dataclassclass Item: name: str price: float count: int
def total(items: list[Item]) -> float: return sum(i.price * i.count for i in items)
let cart = [Item("tea", 4.5, 2), Item("cake", 3.0, 1)]for i, item in enumerate(cart, start=1): print(f"{i}. {item.name:<6}{item.price * item.count:>6.2f}")print(f"total {total(cart):.2f}")1. tea 9.002. cake 3.00total 12.00@dataclass, @property, @override and the other decorators are built in, so they need no import.
Variables are declared
Section titled “Variables are declared”A name is declared with let (it can’t be reassigned) or var (it can). Plain assignment only changes an existing variable, so a typo can’t silently create a new one:
var count = 0for word in ["a", "b"]: cuont = count + 1main.tpn:3:5: error TP0201: undefined name 'cuont'const declares a top-level constant. Blocks have their own scope, as in C#, and a name in an inner block can’t shadow one in an outer block.
Annotate functions; the rest is inferred
Section titled “Annotate functions; the rest is inferred”Parameters and results are annotated, as in typed Python. Locals take their type from their initializer. An empty [] or {} needs a type from somewhere, such as let names: list[str] = [].
No None surprises
Section titled “No None surprises”A value that might be None has an optional type, written T? (Python’s T | None). It can’t be used as a T until you’ve checked it:
let ages = {"ada": 36}let age = ages.get("bob")print(age + 1)main.tpn:3:7: error TP0203: int? must be narrowed before use help: narrow it with 'is some', or give a default with '??'Check it with is not None, bind it with is some, or give a default with ??:
let ages = {"ada": 36}if ages.get("ada") is some age: print(age + 1)print(ages.get("bob") ?? 0)370No truthiness
Section titled “No truthiness”Conditions must be bool. Write what you mean: if xs: is if len(xs) > 0:, and if name: is if name != "": or if name is not None:.
let xs: list[int] = []if xs: print("not empty")main.tpn:2:4: error TP0205: condition must be bool, found list[int]Enums with data, and exhaustive match
Section titled “Enums with data, and exhaustive match”enum declares a sum type, whose variants may carry fields. A match must handle every variant, so adding one shows you every place to update. There’s no case keyword: each arm is a pattern and a colon.
enum Shape: Circle(radius: float) Rect(width: float, height: float)
def area(s: Shape) -> float: match s: Circle(r): return 3.14159 * r * r Rect(w, h): return w * h
print(area(Rect(2.0, 3.0)))6.0Numbers
Section titled “Numbers”intis 64-bit. Overflow raisesOverflowErrorinstead of growing./gives afloatand//anint, as in Python. Anintwidens to afloatwhere one is expected, and nothing else converts implicitly: useint(x),float(x)andstr(x).int("42")andfloat("2.5")give an optional,Nonewhen the text isn’t a number, rather than raising.
Smaller differences
Section titled “Smaller differences”- Defaults are evaluated on every call, so
def f(xs: list[int] = [])gets a new list each time. Python’s shared-default trap doesn’t exist. - Each loop iteration has its own variable, so
[lambda: i for i in range(3)]gives functions returning 0, 1 and 2. - No conditional expression:
a if c else bis written as anifstatement or amatchexpression. - Strings in containers print with double quotes:
print(["a"])is["a"]. - Lists are invariant: a
list[Dog]isn’t alist[Animal], since code holding it as one could add aCat. - Imports are of Taipan modules, files beside the main file. Python modules are imported with
from python import jsonand .NET namespaces withfrom dotnet.System.IO import File. - No dynamic features: no
eval,setattr, monkey-patching or multiple inheritance. Protocols cover what multiple inheritance usually does.
Using Python libraries
Section titled “Using Python libraries”Python packages are still there, through from python import …. Their values have the type py, which Python checks at run time, and you convert them back with as:
from python import json
let data = json.loads("{\"ok\": true, \"items\": [1, 2, 3]}")let items = data["items"] as list[int]print(sum(items))6See Calling Python.
© 2026 Discover Edge, LLC · Taipan is released under the MIT License.