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. Integer width is the one deliberate exception (see Numbers). 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, an empty collection takes the other arm's element type, and an int widens only where a float is expected; arms that would read differently (1 if c else 2.5, True if c else 0, "a" if c else None) 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
  • 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, and the collections below
  • Arithmetic (+, -, *, /, %, //, **), comparisons (==, !=, <, <=, >, >=), boolean operators (and, or, short-circuiting), 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, find/count/startswith/endswith, split/join/replace, the layout and predicate methods. A method folded on a literal answers what it answers at run time. 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, with CPython's final-sigma and titlecase rules and ß to SS) and the is* predicates (isalpha, isalnum, isdigit, isdecimal, isnumeric, isspace, isupper, islower) 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, or range, 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
  • List and tuple methods: sort, 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 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
  • 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

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 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, and zip() pulls a generator one value per round in any argument position
  • 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 dict.items()/.keys()/.values()
  • 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

Modules, files, and the standard library

  • Imports of your own modules: import mod, import mod as m, and from mod import f [as g] resolve sibling .py files (and pkg/mod.py packages) and statically link them into the single output module, each compiled once however many import paths reach it
  • 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(), 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
    • 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
    • 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
    • re: compile, search, match, fullmatch, findall, finditer, 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). 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; the re flags, the logging levels, and datetime.MINYEAR/MAXYEAR. json's JSONEncoder and JSONDecoder, and collections' UserDict, UserList, and UserString, are refused for now