What a Program Looks Like
One complete working program, and a plain list of the shapes that do not compile yet. Read it in a minute and you will know whether your code is in scope.
Compiles and runs
Classes holding collections of instances, methods calling methods, string and dict work, comprehensions, f-strings, exceptions, generators, 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
One documented case: int is 32 bits and wraps instead of growing. Everywhere else the rule is Python's answer or a loud failure.
A whole program
This is examples/shopping_cart.py from the repository, unedited. 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 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()
return subtotal - subtotal * discount_rate(subtotal)
- Annotations
- Parameter and return annotations pick the WebAssembly value types. A collection whose elements you compare, sort, or use as dict keys needs the parameterised form:
List[str], not a barelist. - Entry points
- Every module-level
defis exported. There is no top-level run step, so module level holds definitions only; put the code that runs in a function, or underif __name__ == "__main__":. - Imports
import modandfrom mod import fresolve sibling.pyfiles from disk and link into the one output module. No PyPI packages: the supported subset is standard-library only.
What it costs
Median of nine release-build compilations on an Apple M1, via just benchmark. Wall-clock numbers from one machine, so read them for orders of magnitude, not percentages.
| Program | Source | Compile | Module | Compile (opt) | Module (opt) |
|---|---|---|---|---|---|
| shopping_cart | 2.0 KiB | 0.2 ms | 1.4 KiB | 1.2 ms | 1014 B |
| text_report | 3.4 KiB | 0.4 ms | 6.5 KiB | 5.8 ms | 5.7 KiB |
| library_project | 3.3 KiB | 0.5 ms | 3.6 KiB | 2.7 ms | 3.1 KiB |
| nested_collections | 4.7 KiB | 0.5 ms | 13.6 KiB | 4.1 ms | 12.7 KiB |
A whole program compiles in well under a millisecond and produces a few kilobytes of WebAssembly, with no interpreter or runtime bundled in: the module is the program. Optimization is where the time goes. Binaryen costs 5 to 15 times the rest of the pipeline and takes 7 to 27% 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 } is for. The first three rows are the end-to-end programs above; 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.
Integer width is the single documented exception. 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.
Shapes that do not compile
Tagged by what happens when you try. refused is a compile error you will see immediately; 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 refused workaround
A loop,
if,try, bare call, augmented assignment, or write through a subscript at module level. A WebAssembly module has no top-level run step, so the statement would be compiled away.X = 1,X = helper(),X = ClassName(),def,class, and imports do work. Move the rest into a function. -
async def,await,async for,async withrefusedThere is no event loop in the output. Planned after 1.0.
-
*args,**kwargs, keyword-only parameters refusedA WebAssembly function has a fixed signature. Pass the arguments positionally.
-
match,global,nonlocal,del,assert,except*,typealiases,from x import *refusedNot implemented. Each is reported by name with its location.
-
Multiple inheritance and metaclasses refused
Single inheritance works, with
super().__init__()andsuper().method(), plusisinstance,@dataclass, andabc.ABC. -
A subclass override reached through an inherited method differs workaround
Dispatch is static, with no vtable. If
Media.describe()callsself.kind()andVideooverrideskind(), aVideostill reachesMedia.kind(). Calling the override directly is correct, so call it directly or overridedescribe()too. -
Indexing, measuring, or calling a method on a lambda's parameter refused workaround
A lambda's parameters carry no annotation, so
lambda kv: kv[1]andlambda w: len(w)are refused rather than answering from an untyped word. Arithmetic and comparison are fine, sosorted(xs, key=lambda v: 0 - v)works. For anything more, use a nameddef, whose parameters can be annotated. -
A bare
listordictannotation workaroundThe parameterised form is what carries an element type. Write
List[str]andDict[str, int]for any collection whose elements are compared, sorted, or used as dict keys. -
Exception objects and their messages differs
raise,try/except/finally, and propagation across calls all work. An exception carries its type, not an object:raise ValueError("msg")drops the message andexcept ValueError as ebinds the type's code. Matching is by exact type name, plusException, which catches anything. -
f-string format specifiers other than
.Nfrefusedf"{x:.2f}"is how a float gets printed. Widths, alignment, separators, and the!r/!aconversions are refused rather than dropped. A bare float, bool, or collection in a placeholder is also a compile error, sincestr()cannot render them yet. -
**with a fractional or negative exponent differsPowers are computed by repeated multiplication.
2.0 ** 0.5traps, because it needs exp/log this runtime does not carry, and a negative integer exponent traps because Python's answer is a float an int cannot hold. -
Two modules defining the same function name refused workaround
Merged modules share one namespace, so
utils.formatnext toreport.formatis a compile error naming both files. Rename one. Module-qualified names are the real fix and are not done yet. -
Object lifetime differs
The allocator has no
free: every instance lives until the module is torn down, and__del__is never invoked. A long-running program that allocates in a loop will grow its memory. -
Set methods beyond
add,remove, anddiscardrefusedunion,intersection, and friends are not implemented. Membership, de-duplication, and the three mutators work. -
Third-party packages refused
No PyPI. The bundled standard-library support is
sys,os(includingos.path),math,random,json,re,datetime,logging,collections,itertools, andfunctools.
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.
shopping_cart.py
A domain model: classes holding instances, methods calling methods, float money arithmetic.
text_report.py
A word-frequency tool: string methods, dict accumulation, sorting, comprehensions, fixed-point output.
library_project/
Four modules with the domain class shared between them, compiled from the entry file.
README limitations
The authoritative, exhaustive statement of the supported subset and what it leaves out.