Skip to content

Language

The Python subset

What compiles and runs today, construct by construct, each giving CPython's answer.

On this page

Waspy compiles a typed subset of Python ahead of time. The rule it holds to: a construct either gives CPython's answer or is refused at compile time, never a quietly different one. The few exceptions, integer width first, are listed under Where the answer differs. The modules board tracks each feature, every one backed by a test asserting CPython's answer.

Syntax and control flow

  • Functions, several per module, calling each other; multiple files compile into one module
  • Control flow: if/else, while and for loops with break and continue, and else: clauses on for, while, and try, run exactly when CPython runs them
  • Conditional expressions (a if cond else b), evaluating only the arm the condition picks. Both arms must read as one type: two classes share their nearest common base, a class and None make an Optional, and an empty collection takes the other arm's element type; arms that would read differently (1 if c else 2.5, True if c else 0, "a" if c else None, or an int arm where a float is held, since CPython keeps the int) are refused
  • Variables, assignment, and type annotations on parameters and return values. For a collection passed into or out of a function, the parameterised form (List[str], Dict[str, int], Tuple[int, str]) is what gives the compiler its element type; a bare list or dict there carries only ints, bools, and None (see Limitations)
  • Calls pass arguments by position or by keyword (f(1, c=7), Point(x=1)), with defaults filled in for the parameters skipped. Functions and __init__ take keyword-only parameters (def f(x, *, factor=2)), *args: T (a tuple of T), and **kwargs: T (a dict[str, T]), and f(*xs) passes a list or tuple on as *args. A call to one of the module's own functions is checked against its signature, so a surplus or missing argument is a compile-time TypeError
  • assert test, msg raises AssertionError when the test is false, evaluating the message only then
  • global name in a function or method rebinds the module definition for every reader; a rebinding keeps the definition's type
  • del xs[i], del xs[a:b], and del d[k], raising IndexError / KeyError for what is not there, and del name, which lets go of what the name held
  • Tuple unpacking types each target from the value, and a value of the wrong length raises ValueError. Extended unpacking (a, *b, c = xs) binds the starred target to the middle slice as a real list
  • Augmented assignment through a name, an attribute, or a subscript (counts[w] += 1, self.items[k].qty += n), each target evaluated once, and annotated fields (self.items: Dict[str, Item] = {})
  • Unsupported syntax is rejected up front with a located error and a hint, instead of failing deep in code generation

Numbers and operators

  • Types: integers (32-bit, see Numbers), floats, booleans, strings, bytes, bytearray, and the collections below. bytes(), bytes(n), and bytes(ints) make bytes (a value out of range(0, 256) raises ValueError), and sep.join(parts) joins bytes
  • Arithmetic (+, -, *, /, %, //, **), comparisons (==, !=, <, <=, >, >=, chained as in 0 <= x <= 100), boolean operators (and, or, short-circuiting and answering the operand that decides, as CPython's do: 0 or 7 is 7, name or "anon" a str), and bitwise operators (&, |, ^, <<, >>)

Strings and formatting

  • String operations: slicing (with any step, s[::-1] included), concatenation, repetition (s * n), 20+ methods, and formatting: f-strings, .format(), format(), and %, all through CPython's format specification mini-language (fill, alignment, sign, z, #, 0, width, ,/_ grouping, precision, and every type letter), digit for digit with CPython. .format() takes automatic, positional, and named fields, attribute and index lookups ({0.x}, {1[2]}, {d[key]}), conversions, and specs with nested fields; % takes every conversion with its flags, a tuple operand, and %(key)s over a dict
  • String methods run on a string built at run time, not only on a literal: case and trim transforms (swapcase and casefold included), find/rfind/index/rindex/count/startswith/endswith, split/rsplit (with maxsplit)/splitlines/partition/rpartition/join/replace (with a count), removeprefix/removesuffix, the layout methods (zfill and expandtabs included), and the predicates (istitle, isascii, isprintable, and isidentifier included). translate() takes a dict[int, int] or dict[int, str] table, or the one str.maketrans(x, y) makes, and encode() answers the bytes in UTF-8, ASCII, or Latin-1 (with errors of strict, ignore, or replace), raising CPython's UnicodeEncodeError. A method folded on a literal answers what it answers at run time. A % format string known only at run time is not supported yet. Strings compare by content everywhere, so a key built at run time matches an equal one already in a dict
  • A str is a sequence of code points, as in CPython: len, indexing, slicing, find/index, padding, iteration, and comprehensions count characters, not UTF-8 bytes. Case conversion (upper, lower, capitalize, title, casefold, with CPython's final-sigma and titlecase rules and ß to SS) and the is* predicates (isalpha, isalnum, isdigit, isdecimal, isnumeric, isspace, isupper, islower, isprintable, isidentifier) cover the whole of Unicode, from tables generated from CPython 3.13 (Unicode 15.1), and so do strip() and split()'s idea of whitespace
  • F-strings interpolate every placeholder, rendering each value with str(). {x!r}, {x!a}, and {x:spec} (with nested fields, {x:{w}.{p}f}) format as CPython does. Constant placeholders fold into the literal at compile time
  • str() and repr() of every value: a float by its shortest round-trip digits (CPython's repr, exponent notation from 1e16), bools, None, quoted and escaped strings and bytes, ranges, nested lists, tuples, dicts, and sets, and instances through their __str__ and __repr__. float() parses a string correctly rounded, as CPython does, and raises a catchable ValueError on bad input

Built-in functions

  • Built-in functions: int() (including parsing a string, with a catchable ValueError on bad input), float(), str(), repr(), ascii(), format(), bool(), len() (of strings, collections, and ranges), abs(), round(x[, ndigits]) (rounding the exact binary value half-to-even, as CPython does), min()/max() (several arguments, all ints, all floats, or all strings, or one list, tuple, range, set, dict, or string, with key= and default=), sum() over lists and tuples (with an optional start value), isinstance() against a class, a builtin type, or a tuple of them, sorted(iterable[, key][, reverse]), dict(mapping) (a copy), dict(pairs) and dict(**kwargs), and reversed(seq) where it is walked (a loop, a comprehension, list(), sorted(), str.join()). print() writes to standard output (with sep, end, file=sys.stderr, and flush), and input() reads a line of standard input, both through WASI (see Running compiled modules)

Collections

  • Collections: lists, dicts, sets, tuples, and ranges: literals, indexing, slicing, methods, and membership (in/not in), with full-precision f64 elements and hash-table sets; lists, dicts, and sets grow at run time. A collection's elements live in a block its header points at, so growing one never moves the collection: a list grown inside a function it was passed to is grown for every other name for it too. An empty collection is falsy as in Python
  • bytearray, a mutable sequence of bytes: bytearray(n), bytearray(b) of bytes or a bytearray, and bytearray(ints); indexing and item assignment, slicing (a new bytearray), len(), iteration, in, == and ordering against bytes or a bytearray, +, *, +=, *=, del, bytes(b), and append, extend, insert, pop, remove, clear, reverse, copy, count, find, index, startswith, and endswith, with CPython's ValueError for a value outside range(0, 256) and IndexError for an index out of range
  • List and tuple methods: sort (in place and stable, of numbers, strs, bytes, and tuples of them), reverse, pop(i), index, count, append, extend, insert, remove. Lists and tuples concatenate (xs + ys) and repeat ([0] * n, (a, b) * 3)
  • Sets take |, &, ^, and - (and their in-place forms) and the full method set (union, intersection, difference, symmetric_difference, the _update forms, issubset, issuperset, isdisjoint, taking a set, list, or tuple; copy, pop, clear; set(iterable)). A set iterates in CPython's order: its hashes for ints, bools, floats, and tuples of them, and its table algorithm, are CPython's, so a loop over a set, pop(), and the order a set prints in agree with CPython 3.13 for those members. A set of strings has no reproducible order in CPython, whose string hash is keyed per process; here the order is fixed but unspecified
  • Dicts merge with d1 | d2 and |= in CPython's order and have get(key, default), keys, values, items, update, pop, popitem, setdefault, copy, clear, and dict.fromkeys. dict.get(key) without a default needs number or instance values (see Limitations)
  • A walk over a collection visits what CPython's does: a str its characters, a dict its keys, a set its members in CPython's order, in a comprehension, sum, sorted, min, and max alike
  • Values compare by what they hold: ==, !=, and the orderings on tuples, lists, and strings follow CPython, and so do in, list.index/count, set membership, and dict keys, so a tuple can be a set member or a dict key. Dicts compare by their entries, whatever their order. Sets compare as sets (==, and <=/< as subset tests). is compares identity: two instances, an instance and None, or a str against None

Classes and objects

  • Module-level variables and class definitions. A module-level definition that is not a plain constant (a collection, an instance, a call) is evaluated once at instantiation, in source order, and shared by every function, so mutating it is seen everywhere
  • Classes with heap-allocated instances, several live per class, usable as arguments and return values. Class-level variables are read through an instance or the class (a subclass's own when it defines one) and rebound through the class that defines them (Counter.count += 1)
  • Object-oriented Python: inheritance from one base or several, with CPython's C3 method resolution order and cooperative super(), isinstance/issubclass over the class hierarchy, @staticmethod/@classmethod/@property (with setters), @dataclass (generated __init__/__eq__/__repr__), abstract base classes via abc.ABC, and rich comparisons: == calls __eq__, and </<=/>/>= between instances call __lt__/__le__/__gt__/__ge__ (the left operand's own, then the right operand's reflection, as CPython does)
  • Special methods: binary operators call __add__, __sub__, __mul__, and the rest (the left operand's, or the right one's reflected __radd__, a subclass's reflection first, as CPython orders them), -x, +x, and ~x call __neg__, __pos__, and __invert__, x in c calls __contains__, c[k] and c[k] = v call __getitem__ and __setitem__, a += b calls __iadd__ (or __add__), len() calls __len__ (a negative answer is CPython's ValueError), if x: calls __bool__ (or __len__), abs(), int(), float(), and round(x) call theirs, and calling an instance calls __call__. A method's defaulted parameters can be left out
  • Virtual dispatch: a method call reaches the override the receiver's run-time class defines, so a base method calling self.method() gets the subclass's version (the template-method pattern), including through a variable, a parameter, or a list element declared as the base. @property getters and __eq__ dispatch the same way, and super().method() stays non-virtual as in Python
  • Finalizers: __del__ runs when the last reference to an instance goes, as it does under CPython's reference counting: when the function holding it returns (its arguments first, then its locals, in order), when a name is rebound or deleted, when a collection lets go of it (a list and a tuple last to first, a dict and an instance's attributes first to last), and for whatever is left as the program ends. A subclass inherits it, it runs once, and objects that only refer to each other are finalized when the collector finds them. See Limitations for where the moment differs
  • Classes are values: type(x) and x.__class__ give an instance's class, a class name is its class object, compared by identity (type(s) is Square), named by __name__, shown as <class '__main__.Square'>, read for its class variables, and annotated type[A]. Calling one held in a variable, and type() of a builtin value, are refused
  • Metaclasses: class M(type) with metaclass=M, inherited by subclasses. M's __new__ and __init__ run once as each class is created, specialized for it at compile time, so a registry or a counter works and the attributes they give the class (cls.plugin_id = ...) are its class variables; M.__call__ replaces instantiation (a singleton works); M's other methods and class variables are reached through the class (Plugin.describe()). The class namespace argument, __prepare__, and creating a class by calling the metaclass are refused
  • Decorators: the standard ones (@staticmethod, @classmethod, @property/@x.setter, @abstractmethod, @dataclass), functools's @singledispatch with @f.register (dispatch is static, on the first argument's type) and @total_ordering (the missing ordering methods are derived from the one defined), @lru_cache/@cache (accepted, and change nothing observable), and decorators the program writes, on module functions. A user-written decorator is applied at compile time: its wrapper is specialized for each function it decorates, so a wrapper forwarding *args and **kwargs gets the function's own parameters, state it keeps through nonlocal is one per decorated function as in CPython, and a factory (@repeat(3)) and a stack of decorators work

Exceptions and context managers

  • Exceptions are objects, as in CPython: raise transfers control, try/except/finally catch and clean up, and an exception propagates out of a call into the caller's handler. raise E(args) raises an instance, except E as e binds it, and str(e), repr(e), e.args, a user exception's own fields and __str__, and raise X from Y (e.__cause__) work. except (A, B): catches either type, and except Base catches its subclasses, along user classes and CPython's builtin hierarchy (LookupError over IndexError and KeyError, ArithmeticError over ZeroDivisionError, and the rest). A bare raise (or raise e) in a handler re-raises. The run-time's own faults carry CPython's messages (list index out of range, invalid literal for int() with base 10: 'x', KeyError('k'))
  • Faults raise where CPython raises, catchably: division or modulo by zero (ZeroDivisionError, integer and float), an index outside a sequence (IndexError; a negative index counts from the end, and slicing clamps), a missing dict key (KeyError), list.index(v) and list.remove(v) of a missing value (ValueError), set.remove(v) of a missing member (KeyError), and list.pop() from an empty list (IndexError). Item assignment into a tuple or a string is a compile error
  • Context managers: with obj as name: over a user class implementing __enter__/__exit__, including nested blocks and inherited protocols. __exit__ runs on every way out, receives an exception leaving the block, and suppresses it by answering true, as in CPython. The block holds its manager, which is let go of when the block is done

Iteration, generators, and closures

  • Comprehensions: list, set, and dict comprehensions with filters, multiple generators, nesting, and {k: v for k, v in pairs} unpacking. A generator expression goes where something walks it once: an argument of a builtin or standard-library call (sum(x * x for x in xs), ", ".join(str(x) for x in xs)), a for statement, or another comprehension (see Limitations). A comprehension over a generator or iterator pulls one value at a time, as CPython's does, so their side effects interleave the same way; list(), tuple(), sum(), sorted(), min(), max(), any(), all(), and set() take one too
  • Generators with real state preservation: yield suspends and resumes (inside try/except/finally and with too), yield from delegates, and next()/send()/close() work, close() raising GeneratorExit at the suspended yield so finally blocks and handlers run; generator methods; user classes implementing __iter__/__next__ iterate in for loops with StopIteration ending the loop, next() of an exhausted iterator raises a catchable StopIteration (passed on by a __next__ that calls next() of another, and made RuntimeError in a generator, as CPython 3.7+ does), a method declared to return an iterator class iterates like a call of the class, and zip() pulls a generator one value per round in any argument position. A generator left part way through a try (a loop that breaks out, a next() or two and no more) is closed when the last reference to it goes, so its finally blocks run then, as CPython's do
  • Tuple targets in for loops (for a, b in pairs, star targets included) and the iterator-shaped builtins: enumerate(xs[, start]) (start= too), zip(...), and .items()/.keys()/.values() of a dict, of a collections mapping, or of an instance whose class defines them
  • Closures: lambdas compile to real functions dispatched through a call_indirect table, capture enclosing variables by reference, and work as first-class values: returned, passed as arguments, and stored in collections. A module function, len, or abs named as a value works too (sorted(words, key=len))
  • Nested functions: a def inside a function or method keeps its annotations, closes over the enclosing variables by reference (floats and strings included), recurses, calls its siblings, and takes keywords and defaults. Called by name it is a direct, fully typed call; returned or passed on it is a closure value called at its signature where that is known (Callable[[float], float] annotations are understood). nonlocal name rebinds the enclosing variable for every reader

Coroutines and JavaScript

  • async def and await, coroutine methods, async with and async for over classes of the program, and asyncio's run, create_task, ensure_future, gather, sleep, wait_for, and timeout, TaskGroup and ExceptionGroup, its locks, events, semaphores, and queues, on CPython 3.13's event loop, so tasks interleave in CPython's order (see JavaScript and async)
  • The page through Pyodide's surface: js.document and its elements, listeners through pyodide.ffi.create_proxy, and HTTP requests through pyodide.http.pyfetch (see JavaScript and async)
  • JavaScript values through Pyodide's js and pyodide.ffi: attributes, calls, new, items, iteration, conversions as Pyodide makes them, to_js(), .to_py(), JsException, an await of a promise, and exports a page calls with JavaScript values (an async def answering a Promise), through the loader waspy::js_loader() (see JavaScript and async)

Modules, files, and the standard library

  • Imports: import mod, import mod as m, and from mod import f [as g], of your own modules and the standard library's, at module level, in a function (which binds the name in that function), or in the if __name__ == "__main__": block. Your own modules resolve sibling .py files (and pkg/mod.py packages) and are statically linked into the single output module, each compiled once however many import paths reach it
  • importlib.import_module("name") and __import__("name") with a constant name (a string literal or a module-level constant) are the import they name, so the module is linked when the program is compiled: json = importlib.import_module("json"), in a function or at module level, or used in place (importlib.import_module("math").sqrt(2.0)). __import__("os.path") answers the package os and importlib.import_module("os.path") the submodule, as in CPython. importlib.import_module(name) of a name computed at run time loads a separately compiled module the host holds (see Modules loaded by name)
  • File I/O through WASI: open(), read([n]), write(s), close(), and with open(...) as f: (see File I/O)
  • The standard library, module by module (the modules board tracks each function; anything not listed is refused at compile time, naming the call):
    • math: every function on the board, on runtime values, with CPython's ValueError and OverflowError; math.pi, e, tau, inf, nan. The transcendental functions give musl libm's results, bit for bit (see Numbers)
    • random: CPython's generator, so a seed gives CPython's sequence: seed, random, getrandbits, randrange, randint, uniform, choice, shuffle, sample, gauss, normalvariate, expovariate. Unseeded, it seeds from the host's entropy
    • sys: argv, stdin/stdout/stderr, exit(), maxsize, platform ("wasi"), version, and path (empty: imports are linked at compile time)
    • os: environ and getenv() from the host, urandom(), remove(), unlink(), rename(), replace(), listdir(), os.path.getsize(), and os.path.getmtime() through the host's file system with CPython's OSError subclasses, getcwd() ("/") and getpid() (42), which is what CPython on WASI answers, and the constants (os.name is "posix", os.sep and friends)
    • os.path: join, basename, dirname, split, splitext, normpath, abspath, isabs, as posixpath computes them, and exists, isfile, isdir through the host
    • functools: reduce, partial, partialmethod, cmp_to_key, @cached_property, @singledispatchmethod, and the decorators above
    • itertools: every function, in CPython's order and with its argument checks: count, cycle, repeat, accumulate, batched, chain (and chain.from_iterable), compress, dropwhile, takewhile, filterfalse, starmap, islice, pairwise, tee, zip_longest, groupby (whose groups end when it moves on, as CPython's do), product, permutations, combinations, and combinations_with_replacement
    • collections: namedtuple() and typing.NamedTuple (a module-level class whose instances are their fields as a tuple, with _replace, _asdict, _make, and _fields; namedtuple()'s fields take their types from the calls of the class), and Counter, defaultdict, OrderedDict, deque, and ChainMap. Each of those is a class specialized by its element types, written Counter[str] or defaultdict[str, list[int]] in an annotation, and found otherwise from the constructor's arguments or from what the function does with the variable (c[w] += 1 keys a Counter by w's type, d[k].append(x) makes a defaultdict(list) hold lists of x's type). They read as the dict or list they hold wherever CPython reads them as one, a parameter declared dict included, and add their own behaviour: Counter's missing counts of 0, most_common, elements, total, update, subtract, and multiset operators; defaultdict's factory; OrderedDict's move_to_end and popitem(last=False); deque's ends, maxlen, rotate, and indexing; ChainMap's maps, new_child, and parents; and their reprs and errors. UserDict, UserList, and UserString are classes to subclass, as class Lower(UserDict[str, int]) or bare, class Stack(UserList), whose types come from what its methods declare and how the module uses it. As in CPython they go through their special methods, so update() and the constructor store through an overridden __setitem__, get(), pop(), items(), and values() read through an overridden __getitem__, __missing__ answers a missing key, and an override's unannotated parameters take the base's types. A slice, +, *, |, copy(), and UserString's string methods answer the subclass
    • json: dumps and dump of ints, floats (CPython's repr, and NaN/Infinity), bools, None, strings (with ensure_ascii's escapes), lists, tuples, dicts (keys of str, int, float, bool, or None), and the collections classes that are dicts, with indent, separators, sort_keys, ensure_ascii, and allow_nan as constants; loads and load into the type the value is declared to have (data: dict[str, list[int]] = json.loads(text)), raising CPython's JSONDecodeError (a ValueError) with its message and position for a document that is not JSON. dumps(default=f) writes what f answers for a value JSON has no form for (an instance of a class), and loads(object_hook=f) gives f each object and keeps what it answers (p: Point = json.loads(text, object_hook=as_point)). JSONEncoder and JSONDecoder are classes whose options are fields, so one made with indent=2 and held in a variable writes as CPython's does: encode(), decode(), a subclass's default() (with super().default(o)'s TypeError), and dumps(cls=...) / loads(cls=...)
    • datetime: timedelta, date, time, datetime, and timezone, CPython's own algorithms with its messages and reprs. Construction checks every field; timedelta normalizes days, seconds, and microseconds from any of its arguments, ints or floats, rounding halves to even; the classes add, subtract, negate, multiply, divide, compare, and print as CPython's; date and datetime give toordinal, weekday, isoweekday, isocalendar, isoformat, ctime, strftime (the C locale's directives, %f, %z, %:z, and %Z), replace, and f-string specs; fromisoformat reads every ISO 8601 form CPython 3.13 reads; datetime.strptime matches its format as CPython's regular expression does, backtracking included; timestamp, fromtimestamp, utcfromtimestamp, now, today, and utcnow read the host's clock (the local time zone is UTC, as on every WASI host); and an aware value compares, subtracts, and converts (astimezone) by its offset. isinstance(dt, date) is True for a datetime, as in CPython
    • logging: getLogger() and the logger hierarchy (a logger made after its descendants becomes their parent, as in CPython), debug/info/warning/error/critical/log on a logger and as the module's functions (which configure the root logger first, as CPython's do), basicConfig() with level, format, datefmt, style, stream, filename, filemode, handlers, and force, setLevel (by number or name), disable(), propagate, disabled, getChild, hasHandlers, getLevelName, and lastResort for a record no handler takes. Handler, StreamHandler (to sys.stdout or sys.stderr), FileHandler (modes a and w, delay), NullHandler, Formatter in the %, {, and $ styles with CPython's validation errors, Filter, and LogRecord, each subclassable. A call's arguments are always evaluated and its message formatted only when the level is enabled; a record carries the call's line (%(lineno)d) and function (%(funcName)s), its time (%(asctime)s, from the host's clock, local time being UTC), and process 42, as CPython on WASI reports. addLevelName() names a level for %(levelname)s, getLevelName(), and setLevel(). exception() and exc_info=True log the exception an except block caught (exc_info=e the one given), extra={...} written out in the call adds fields to the record that a format or record.ip reads, as does a filter's record.ip = .... logging.config.dictConfig() takes a dict written out in the call or a module constant, and follows CPython's order: formatters, filters, handlers, disable_existing_loggers, loggers, and the root. logging.handlers has RotatingFileHandler, TimedRotatingFileHandler (every when and interval, with backupCount), WatchedFileHandler, BufferingHandler, and MemoryHandler. Handlers are flushed and closed when the __main__ block ends, as atexit does through logging.shutdown()
    • re: compile, search, match, fullmatch, findall, finditer (a lazy iterator, as CPython's callable_iterator is: each next() finds the next match), split, sub and subn (with a template or a function), escape, and purge, the flags (I, M, S, X, A, U) as arguments and inline ((?i), (?i:...)), and Pattern and Match objects (group by number or name, groups, groupdict, start, end, span, expand, lastindex, lastgroup, pos, endpos, m[k], and their reprs, and Pattern.groupindex, a read-only mappingproxy of the group names' numbers). \N{name} escapes take a character's name or alias as CPython 3.13 looks it up (Unicode 15.1, case-insensitively). Bytes patterns (re.compile(rb"\d+")) match bytes as CPython matches them, by ASCII's classes and case, and answer bytes for their groups, findall, split, sub, and escape; re.Pattern[bytes] and re.Match[bytes] annotate them. Patterns are parsed as CPython 3.13 parses them, so a bad one is reported when compiling with re.error's message and position, and matched as CPython's sre matches: backtracking in its order, greedy, lazy, and possessive repeats with its zero-width rule, atomic groups, lookahead and fixed-width lookbehind, back references, conditional groups, \b, the Unicode classes (\d is any decimal digit, \w any alphanumeric) or ASCII ones under re.A, and case folding with CPython's special cases (the Kelvin sign, long s). Empty matches in findall, split, and sub follow CPython 3.7+. A group that did not take part is None, an Optional[str] that compares, concatenates, and converts as CPython's None would
    • re.sub and re.escape over constant arguments are folded at compile time where the result is provably CPython's, and so are the modules' constants: the re flags, the logging levels, and datetime.MINYEAR/MAXYEAR