Running
What a program looks like
One complete working program, and a plain list of the shapes that do not compile. Read it in a minute and you will know whether your code is in scope.
Compiles and runs
Classes holding collections of instances, inheritance with virtual dispatch, methods calling methods, string and dict work, comprehensions, f-strings, exceptions as objects, generators, closures, coroutines, printing and file I/O through WASI, and imports across your own files.
Refused, loudly
Everything not implemented is a compile error naming the construct, its line, the enclosing function, and a workaround. A refusal is the design, not a bug.
Answers differently
A short documented list, led by int, which is 32 bits and wraps instead of growing, and a NaN inside a collection, which does not equal itself: see Where the answer differs. Everywhere else the rule is Python's answer or a loud failure.
A whole program
This is the code of examples/shopping_cart.py from the repository, as it is apart from its module docstring. Nothing in it was chosen to show off a compiler feature, and its results are asserted against what CPython answers for the same source, under three WebAssembly runtimes.
class Item:
def __init__(self, name: str, price: float, qty: int):
self.name = name
self.price = price
self.qty = qty
def subtotal(self) -> float:
return self.price * self.qty
class Cart:
def __init__(self):
self.items = []
self.count = 0
def add(self, name: str, price: float, qty: int):
item = Item(name, price, qty)
self.items.append(item)
self.count = self.count + 1
def total(self) -> float:
out: float = 0.0
for it in self.items:
out = out + it.subtotal()
return out
def units(self) -> int:
n: int = 0
for it in self.items:
n = n + it.qty
return n
def discount_rate(total: float) -> float:
if total >= 100.0:
return 0.10
if total >= 50.0:
return 0.05
return 0.0
def checkout() -> float:
cart = Cart()
cart.add("widget", 9.99, 3)
cart.add("gadget", 24.50, 2)
cart.add("doohickey", 5.00, 4)
subtotal = cart.total()
rate = discount_rate(subtotal)
return subtotal - subtotal * rate
def item_count() -> int:
cart = Cart()
cart.add("a", 1.0, 2)
cart.add("b", 2.0, 3)
return cart.count * 100 + cart.units()
def empty_cart_total() -> float:
cart = Cart()
return cart.total()
Compiled, every function above is an export a host can call: checkout() answers 94.0215, item_count() answers 205, and empty_cart_total() answers 0.0, as CPython does. What you need to know to write the same:
- Annotations
- Parameter and return annotations pick the WebAssembly value types. A collection passed into or out of a function needs the parameterised form,
List[str]rather than a barelist: the parameterised form is what gives the compiler an element type. Calls may pass arguments by keyword, and functions and__init__collect extras with*args: Tand**kwargs: T. - Entry points
- Every module-level
defis exported. Only definitions run at module level; put the code that runs in a function, or underif __name__ == "__main__":, which makes the module a WASI command thatwasmtime runruns likepython3. - Imports
import modandfrom mod import fresolve sibling.pyfiles (andpkg/mod.pypackages) from disk and link them into the one output module, each compiled once. No PyPI packages: beyond your own modules, the supported subset is standard-library only.- Side effects
print(),input(),sys.argv, andopen()go through WASI, and a module imports only the WASI functions it uses, so the program above imports nothing at all. See Running compiled modules.
What it costs
Median of nine release-build compilations on an Apple M1 Pro, via just benchmark. Wall-clock numbers from one machine, so read them for orders of magnitude, not percentages.
A whole program compiles in a few milliseconds into a module of one to a few tens of kilobytes, with no interpreter bundled in: the module is the program, plus the runtime helpers it calls (float formatting, string case mapping, and the like), each built only when a program uses it. Optimization is where the time goes. Binaryen costs 4 to 14 times the rest of the pipeline and takes 6 to 54% off the module, so it is worth running for something you ship and worth skipping in a fast edit loop, which is what CompilerOptions { optimize: false, ..CompilerOptions::default() } is for. The first three rows are end-to-end programs from the repository; nested_collections is the feature example with the largest module, as a second data point at a different shape.
The rule the compiler holds itself to: a construct either produces Python's answer or fails loudly. It never quietly produces a different one.
There are a few documented exceptions. Integer width is the first: int is a 32-bit two's-complement value, so 1000000 * 1000000 wraps to -727379968 where CPython answers 1000000000000. If your arithmetic can exceed 32 bits, your program is not in the supported subset. Another is a NaN inside a collection, which does not equal itself. Both are explained under Numbers, and Limitations has the full list.
Shapes that do not compile
Tagged by what happens when you try. refused is a compile error you will see immediately; traps stops the module at run time instead of answering; differs is a case where the answer is not Python's; workaround means there is a supported way to write the same thing.
-
Module-level statements other than definitions
refusedworkaroundA loop,
if,try, bare call, augmented assignment, tuple unpacking, or write through a subscript or attribute at module level. Only definitions run when a module starts, so the statement would be compiled away.X = 1,X = helper(),X = ClassName(),def,class, and imports (atry/except ImportErrorguarded import included) do work, and a definition that is not a plain constant is evaluated once, at instantiation, in source order, so every function shares the same list or instance. Move the rest into a function, or underif __name__ == "__main__":. -
Asynchronous generators and comprehensions
refusedasync def,await,async with, andasync forover a class with__aiter__and__anext__work, onasyncio's event loop compiled with the program (see Coroutines and asyncio). Anasync defthat yields, and so anasync forover one, an asynchronous comprehension, and anasync definside a function are refused. -
Starred parameters on methods,
refused**unpacking in a call, positional-only parametersFunctions and
__init__take*args: T(a tuple ofT),**kwargs: T(adict[str, T]), and keyword-only parameters,f(*xs)passes a list or tuple on as*args, and keyword arguments work in every call. Other methods take neither starred nor keyword-only parameters, a call cannot unpack a dict with**, and/is refused. Every call to one of the module's own functions is checked against its signature, so a surplus or missing argument is a compile-timeTypeError. -
refusedmatch,except*,typealiases,from x import *Not implemented. Each is reported by name with its location, and so is
delof an attribute or a stepped slice.global,nonlocal, nesteddef,assert, conditional expressions, anddelof a name, a list index, a list slice, or a dict key do work. -
Subclassing a builtin, or a base that is not a class name
refusedworkaroundclass Stack(list),class Config(dict), and a base computed by a call are refused: an instance would have to be the builtin's value too, and leaving the base out is a wrong answer. Subclasscollections.UserList,UserDict, orUserString, which are ordinary classes here, or hold the value in a field. -
Metaclass shapes beyond
refused__new__,__init__, and__call__Multiple inheritance works, along CPython's C3 method resolution order with cooperative
super(), and so do metaclasses: a metaclass's__new__and__init__run once for each class it makes, specialized at compile time, and its__call__replaces instantiation. The classnamespaceargument,__prepare__, creating a class by calling the metaclass, and class keywords other thanmetaclass=are refused. -
An override that changes the signature
refusedworkaroundEvery implementation of an overridden method is reached through one indirect call, which names a single WebAssembly signature, so
def m(self)in the base anddef m(self, k: int)in the subclass is refused. Give the override the same parameters and return type. -
Indexing, measuring, or calling a method on a parameter nothing types
refusedworkaroundA lambda's parameters, and a nested function's unannotated ones, take their types from the calls in their scope when those agree, and a
sorted()key's from the elements it is given, sosorted(pairs, key=lambda kv: kv[1])works. A parameter nothing types (a lambda only passed on to an unannotated parameter, or called with an int in one place and a str in another) holds an untyped word, sokv[1]orlen(w)on it is refused rather than answering from that word. Use a nameddef, whose parameters can be annotated. -
A decorator the compiler cannot apply
refusedworkaroundThe method kinds,
@dataclass,@abstractmethod,@functools.total_ordering,@functools.singledispatchwith@f.register, the caching decorators (accepted, no effect), and decorators the program writes itself all work on module functions: a user decorator is applied at compile time and specialized for each function it wraps, factories and stacks included.functools's@cached_propertyand@singledispatchmethodwork on methods. A user decorator on a method or a class, a wrapper that uses*argsother than to pass it on, and a decorator name the program never defines are refused. Write the wrapper's work inline, or use@property. -
Comparing collections whose element types differ or are unknown
refusedworkaroundTuples, lists, and dicts compare by value, and a tuple works as a set member or a dict key. Comparing two with different element types (
(1, 2) == (1.0, 2), True in CPython) or with element types the compiler cannot see is refused rather than read at the wrong width, and so is a list, dict, or set used as a set member or dict key (unhashable in CPython). Annotate the collections (List[Tuple[int, int]]) so both sides are known. -
Mixing types in one collection
refusedworkaroundEvery slot in a collection is read at one width, so a literal mixing types no one type holds (
[1, "a"],[1, True]) is refused; use a class or a tuple for a record. An int written into a float collection is refused too, since CPython keeps the int (xs.append(3)on aList[float]). So is the other direction, because storing2.5where the slot is read as an integer would truncate it. Make the elements one type. -
Reading a field through an unannotated parameter
refusedworkarounddef total(cart): return cart.counthas no type forcart, so the read is refused with a hint rather than answering 0. Annotate the parameter (cart: Cart). The rich comparison methods are the exception: an unannotatedotherin__eq__,__lt__, and the rest is typed as the class. -
A string, float, collection, or instance through an untyped place
refusedworkaroundAn unannotated parameter or return, and the elements of a bare
list,set, ordictparameter or return, hold one word read back as an int. Ints, bools,None, and functions pass through; anything else is refused, since a string would lose its length and a float its width. Annotate the parameter, or writeList[str]andDict[str, int]. A bare annotation on a variable is fine: its elements take the type of the value assigned. -
refusedworkaroundx is Noneon an untyped value, anddict.get(key)without a defaultNoneis the word 0, which an untyped value can also be, so the test cannot be answered there. AnOptional[int],Optional[bool], orOptional[float]is boxed, sois None,==, and truthiness on one give CPython's answer; arithmetic between two optional numbers is refused.dict.get(key)with no default is refused unless the values are numbers or instances, since a string or collection has no distinct word for theNonea missing key answers; pass a default. -
refusedstr.formator%on a format string built at run timeThe whole format specification mini-language works in f-strings,
format(),.format(), and%, digit for digit with CPython..format()and%need a literal format string, and a*width or precision in%, or a precision on%d,%x, or%o, is refused. A spec CPython rejects is a compile error carrying CPython's message when it is a literal, and traps when it is built at run time. -
traps**with a negative int exponent, or a negative float to a fractional powerFloat powers are CPython's, so
2.0 ** 0.5works. A negative integer exponent traps because Python's answer is a float an int cannot hold, and a negative float to a fractional power traps because its answer is a complex number.float()andint()of a string with non-ASCII digits or spaces trap too, where CPython would accept them. -
Two modules defining the same function name
refusedworkaroundMerged modules share one namespace, so
utils.formatnext toreport.formatis a compile error naming both files. Rename one. -
Object lifetime
differsMemory is reclaimed, and
__del__runs when the last reference goes: as a function returns, a name is rebound or deleted, or a collection lets go of what it held, in CPython's order. A generator abandoned mid-iteration runs itsfinallyblocks then too. What differs is a temporary, which lives to the end of its statement (soprint(Res().name)prints before the finalizer runs), and an object in a reference cycle, which is finalized whenever the collector next runs rather than at CPython's count. -
A NaN inside a collection
differsWith
x = float("nan"),(x,) == (x,)and[x] == [x]are False here, where CPython answers True: floats are unboxed values, so there is no object identity for CPython's same-object shortcut to find.x == xis False in both. -
Tracebacks
differsNo traceback is kept. An exception a
__main__block does not catch writes only the last line of CPython's report (ValueError: boom) to standard error and exits with status 1. -
A generator expression kept in a variable or passed to your own code
refusedworkaroundA generator expression goes where something walks it once: an argument of a builtin or standard-library call (
sum(x * x for x in xs)), aforstatement, or a comprehension.g = (f(y) for y in ys), returning one, and passing one to a function of your own are refused, because it is built as a list, which can be walked twice and measured where CPython's generator cannot. Write a list comprehension for a list, or a generator function for a lazy sequence. -
A generator expression's side effects, inside a builtin call
differssum(f(y) for y in ys)callsffor every element beforesum()reads the first, where CPython interleaves them. The answer is the same; only the order of whatfprints or changes can differ. Aforstatement over one, andany()andall(), pull it as CPython does. -
Third-party packages, and what the standard library does not implement
refusedNo PyPI.
sys,os,os.path,math,random,json,re,datetime,logging,collections,itertools,functools, andasynciorun on runtime values, checked against CPython 3.13. What is not implemented is refused at compile time, by name: the modules board tracks each feature, and Limitations has the rules these modules follow.
Where to look next
The repository's examples/ directory holds the programs this page is drawn from, each asserted against CPython in the test suite.
defaultdict field, Counter, exceptions, and JSON output.
Limitations
The authoritative, exhaustive statement of what the supported subset leaves out, and why.