Skip to content

Language

Limitations

What the compiler refuses, or traps on, rather than answering differently from CPython.

On this page

Each of these is refused at compile time, or traps, rather than answering differently from CPython, apart from the few listed first.

Where the answer differs

These answer differently from CPython without an error. Each is known, and anything else is a bug:

  • int is 32 bits and wraps instead of growing (see Numbers)
  • A NaN inside a collection does not equal itself: (x,) == (x,) is False for x = float("nan"), where CPython answers True (see Numbers)
  • A temporary lives to the end of its statement: in print(Res().name) the Res is finalized after the line is printed, where CPython finalizes it as soon as .name is read. What a variable, a field, or a collection holds, a function's own locals, and the arguments of a call to a function, a method, or a constructor are let go of exactly where CPython lets go of them
  • An object in a reference cycle is finalized when the collector next runs (after enough has been allocated, or as the program ends), which is not the allocation count CPython's collector runs at. Both finalize what is left at exit; a module with no __main__ block has no exit, so nothing is finalized when its host drops it
  • A number that happens to equal an object's address keeps the object alive while it is held, since the collector reads memory without knowing what each word is (the text of a str or bytes excepted, which it does not read), so that object's __del__ can run later than CPython's
  • An exception __del__ raises is discarded, without the Exception ignored in report CPython writes to standard error
  • A traceback is not kept: a __main__ block's uncaught exception writes only the last line of CPython's report, logger.exception() and exc_info= log that same line (ValueError: bad) in place of the traceback, and a logging error on standard error leaves its traceback out
  • A generator expression given to a builtin or a standard-library call (sum(), sorted(), str.join(), Counter()) is built as a list first, so every element is computed before the call reads any, where CPython computes each as it is read. The answer is the same; what differs is the order of anything the elements print or change. One a for statement walks (with one for clause of its own), or that is the argument of any() or all(), is pulled only as far as CPython pulls it. Anywhere it could be told from a list, a generator expression is refused (see Generators)
  • A task's exception that nothing retrieves is reported on standard error with the first and last lines of CPython's report (Task exception was never retrieved, ValueError: bad), since its future: line names the script's path and the traceback is not kept; the report is written as the task is reclaimed, as CPython's is: as soon as it fails when nothing holds it, when the coroutine holding it finishes, or at exit when a module variable holds it. An exception a cancelled task raises at asyncio.run()'s shutdown is reported the same way, as asyncio.run() finishes

Types

  • A place with no type holds a single word read back as an int: an unannotated parameter or return value, and the elements of a bare list, set, or dict parameter or return. Only ints, bools, None, and functions pass through one; a string, float, collection, or instance is refused with a hint to annotate, since it would lose its length, width, or type on the way. A bare annotation on a variable or field is fine: its elements take the type of the value assigned. An empty list, set, or dict with no annotation (seen = []) takes the types of the first value stored in it (an append(), add(), or d[k] = v), and a value whose type the compiler cannot tell there is refused with a hint to annotate the collection. len() of an untyped value is refused for the same reason
  • None is the word 0, which an untyped value can also be, so x is None on an untyped value is refused. An Optional[int], Optional[bool], or Optional[float] is boxed, so None is told apart from 0: is None, ==, truthiness, str(), and dict.get(key) on number values give CPython's answer, and arithmetic or an ordering on None raises its TypeError. An optional number passed or returned where a plain one is declared (past an is None test, typically) is its number, and None there traps, since CPython would pass the None on and the place cannot hold it. Arithmetic between two optional numbers is refused
  • dict.get(key) without a default is refused unless the values are numbers or instances, since a missing key answers None, which a string, bytes, or collection value has no distinct word for; pass a default
  • Reading an attribute through a value whose type is not known, most often an unannotated parameter, is a compile error with a hint to annotate it. The rich comparison methods are the exception: an unannotated other in __eq__/__lt__/... is typed as the class. A field the class does not have is refused too
  • A variable, field, or collection that holds instances of more than one class is typed as their nearest common base for the whole function, so a method call on it dispatches on each instance's own class. Types are not tracked per program point, so reading a field that only one of the subclasses has through it is refused, and so is a method call on instances with no common base. Storing an instance where an annotation names an unrelated class (a Rect passed to a Circle parameter, or returned from -> Circle) is refused rather than dispatched as the annotated class
  • Annotations are taken as the type a place holds: an int, bool, float, str, or bytes stored where a different one of these is declared (an int passed to a str or Optional[str] parameter, a str assigned to a field typed int, or a local bound to an int and later to a str) is refused, since the place would read it as the declared kind. CPython keeps the value whatever the annotation says. That includes an int or a bool where a float is declared, and a bool where an int is: the place would hold it as the declared kind and print 5.0 or 1 where CPython prints 5 or True, so x: float = 0 is refused with a hint to write 0.0, and f(n) for an int n with one to write float(n). Where only the number is used (compared with a float, computed with, or passed to a math function or a datetime class), an int converts with CPython's answer
  • a or b and a and b whose answer is used as a value (stored, returned, printed) answer one of the operands, so the two need a type in common, as a conditional expression's arms do: n or "none" (an int or a str) and n > 1 and n (a bool or an int) are refused, where CPython keeps whichever it answers. An Optional left of or counts as what it holds, since or answers it only when it is not None, and c and x or y as x against y. In a condition, not, or bool(), only the operands' truth is used, so any operands work
  • A lambda's parameters, and a nested function's unannotated ones, are typed from the calls in their scope when those agree, a sorted() key's from the elements it is given, and a lambda passed where a parameter is declared Callable[[...], R] takes those parameter types and returns an R (a body returning something else is refused). 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 a word like any untyped place, so indexing it, measuring it, or calling a method on it is refused; annotate a named def instead
  • A @singledispatch call needs a first argument whose type the compiler knows (a literal, an annotated parameter, an instance); dispatching on an untyped value is refused rather than sent to the base implementation

Collections

  • A collection holds one element type, so a literal mixing types no one type holds ([1, "a"], {"n": "bob", "age": 30}, [1, True]) is refused; use a class or a tuple for a record. A tuple is typed per position, so (1, 2.5) and (1, "a") are fine, but walking one whose members differ (a loop, sorted, sum) is refused
  • A collection reads every slot at one width, so a value written into one has to be its element type. Otherwise the write is refused rather than stored wrong: an int into a List[float] (CPython keeps the int), a float into a collection of ints (it would truncate), a float into a collection with no element type, a value whose type is unknown into a collection of floats, and list.extend() between lists of different widths. list.extend() takes a list, or a tuple whose members share a type; any other iterable (a range, a set, a string) is refused
  • Comparing two collections needs both to have the same, known element types: (1, 2) == (1.0, 2) (True in CPython) and a comparison with an untyped list are refused rather than read at the wrong width. Comparing two empty literals ({} == {}, [] == []), whose element types nothing gives, comparing collections whose elements are sets, and a list, dict, or set as a set member or dict key (unhashable in CPython) are refused; two sets compare directly (==, and <=/< as subset tests). A class that defines __eq__ cannot be a set member or dict key, since calling a user __hash__ is not supported; a named tuple can, as it hashes as its tuple
  • A method called with the wrong number of arguments is a compile error: xs.append(3, 4) and xs.clear(9) are refused. The optional forms Python accepts compile, so pop(), pop(i), sort(), and sort(reverse=True) all work
  • for k, v in x.items() (and .keys() / .values()) calls the method when x is known to be an instance of a class defining it (a local, a parameter, a field, or a call's declared result). Where the instance comes from something whose class is not known at the loop, such as another method's result, the loop is refused with a hint to assign it to an annotated local first
  • A loop over a list, dict, or set sees what is done to it while it runs, as CPython's does: a list is walked to wherever its end is by then, and a dict or set that changes size raises RuntimeError (dictionary changed size during iteration, Set changed size during iteration; OrderedDict mutated during iteration while an OrderedDict has a key left, and deque mutated during iteration for a deque). Three cases differ. A dict whose keys change without its size changing (a key renamed with d[k.upper()] = d.pop(k)) raises RuntimeError: dictionary keys changed during iteration at once, where CPython's answer depends on how its table is laid out: it may walk on and raise later, or finish. An OrderedDict or deque changed without its size changing (a key moved with move_to_end(), a deque rotated) is walked on, where CPython raises. And a dict or set changed by a function the loop calls, rather than by the loop's own code, traps where CPython raises, unless the function the loop is in can raise for some other reason
  • A comprehension over a dict literal with string keys written in place ([k for k in {"a": 1}]) is refused; assign the dict to a variable first
  • A bytearray holds its bytes as a list of ints. It has the methods listed on the Python subset, and not bytes' text methods (decode, hex, split, upper, and the rest, which bytes do not have here either); in, find, count, index, startswith, and endswith take a byte (an int) for the first four and bytes for the last two; slice assignment (b[1:3] = ...), bytearray(s, encoding), and bytearray() of another iterable (a range, a tuple) are refused. It is unhashable, as in CPython, so it cannot be a set member or a dict key
  • A chained comparison (0 <= x <= 100) needs its middle operands to be names or constants; one that is a call or an attribute is refused, since it would be read twice. Assign it to a variable first

Functions and classes

  • Closures capture the variable, not a snapshot of it: a captured variable reassigned after the closure is made changes what the closure sees, and closures created in a loop share the loop variable. A captured variable keeps one type (a float or a string included), so rebinding it to a value of another type is refused
  • A nested def has to sit at the top level of the enclosing function's body (not inside an if, loop, try, or with), outside a generator, with constant defaults, and it cannot be called before its def runs. A function value that passes through an untyped place (an unannotated parameter) is called with int arguments for an int result, so one taking or returning a float, string, collection, or instance is refused there; annotate the parameter with Callable[[...], R]
  • Every implementation of an overridden method is reached through one indirect call, which names a single WebAssembly signature, so an override that changes the parameter or return types is a compile error naming both methods
  • Decorators: a wrapper that uses *args other than to pass it on, a user decorator on a method or a class, and any decorator name the program does not define and the compiler does not implement are refused. So are calls to any other name the program does not define and the compiler does not implement (divmod() and hash() of a builtin value, for example)
  • There is no NotImplemented, so the special method the operator's rules pick answers: an __add__ cannot defer to the right operand's __radd__. A reflected method is called only when the left operand has no method of its own, and needs one of the two operands to be a constant or a variable, since it runs the right operand first. A method's default must be a constant to be left out of a call (CPython evaluates it once, when the method is defined)
  • reversed() gives the list it walks, so only where it is walked (a loop, a comprehension, list(), sorted(), str.join()), and only over a variable or a field, which it reads twice; anywhere else, where CPython gives an iterator object, it is refused
  • A class's bases are classes named directly: a class of the program, a standard-library class, a builtin exception, object, or abc.ABC. Subclassing a builtin value type (class Stack(list), dict, str, int) is refused, since an instance would have to be that value too; subclass collections.UserList, UserDict, or UserString, or hold the value in a field. A base computed when the class statement runs (class C(make_base()):) and a subscripted one other than Generic[T], Protocol[T], or a collections user class are refused
  • A with statement needs its context manager's class to be known at compile time (an instantiation, a call with an annotated class return type, or a variable of known class type). __exit__'s exc_type receives the exception object as well as exc, and tb None; either may only be compared with None or tested for truth, and any other use is refused
  • The default repr of an instance whose class defines neither __repr__ nor __str__ is refused, since CPython's includes an address
  • The if __name__ == "__main__": block runs after every module definition is made, so one that reaches a definition placed after it (a NameError in CPython) is refused; move the block to the end of the file. A name the block binds for the first time is its own (an import's included), not a module global another function can read, and rebinding a module function or class there is refused

Exceptions and formatting

  • Types are static, so the name a handler binds has the class the handler names (the common base of several): a field only a subclass has is refused through it. e.args reads back when every argument is a str, and traps otherwise
  • A traceback is not kept: a __main__ block's uncaught exception writes only the last line of CPython's report (ValueError: boom), and UnicodeDecodeError's position counts from the start of the line where CPython counts from its read buffer
  • str.translate() takes a table whose type is known, a dict[int, int] or a dict[int, str] (map a character to "" to delete it, since a table mixing ints and None has no one type), and str.maketrans() takes two strs of equal length; its one-argument and three-argument forms are refused. str.encode() takes its encoding and errors as constants: utf-8, ascii, or latin-1, and for the last two strict, ignore, or replace. An exception it raises carries CPython's message, but not UnicodeEncodeError's start, end, or reason attributes. bytes.decode() is not supported
  • A format spec CPython rejects (format(1.5, "d"), "{:,s}".format("a")) is a compile error carrying CPython's ValueError message when the spec is a literal, and traps when it is built at run time. str.format and % need a literal format string; a * width or precision in %, and a precision on %d/%x/%o, are refused

Generators

  • A generator method that another class of its hierarchy also defines is refused, since which generator a call makes would depend on the instance, and so is iterating a generator whose class is not known where it is iterated (one returned through an unannotated function, say)
  • A generator expression is accepted where something walks it once: an argument of a builtin that takes an iterable (sum, min, max, sorted, list, tuple, set, dict, any, all, enumerate, zip, map, filter), of a standard-library function or class, or of a collection's or str's method (join, extend, update, and the set methods), what a for statement walks, or what a comprehension walks. Kept in a variable, returned, stored, or passed to the program's own functions and classes, it is refused, since it is built as a list, which can be walked twice, measured, and printed where CPython's generator cannot, and reads the variables it uses when it is made rather than when it is walked; print(), len(), bool(), str(), and type() of one are refused for the same reason. Write a list comprehension for a list, or a generator function for a lazy sequence
  • A comprehension can iterate a generator or iterator only in its first for clause: an inner clause is evaluated twice (once to size the result, once to fill it), so an iterator there is refused rather than drained twice. len() of a generator is refused, as CPython raises TypeError

Coroutines and JavaScript

  • asyncio covers run, create_task, ensure_future, gather, sleep, wait_for, timeout, TaskGroup (its create_task() takes the coroutine alone), a task's methods, Lock, Event, Semaphore, BoundedSemaphore, and the three queues; anything else of the module is refused by name. A queue's item type is named where it is made (q: asyncio.Queue[int] = asyncio.Queue()), and an unannotated asyncio.Queue() is refused. A timeout's when() and reschedule() are refused, since the loop's clock (loop.time()) is not a value here. gather() and wait_for() are awaited directly (await asyncio.gather(...)), gather() with arguments that are all fixed or one *xs, and return_exceptions=True only where every awaitable resolves to None, since a result or an exception in one list needs a type that holds either (its elements are then Optional[BaseException]). sleep() takes the delay alone. A coroutine's result has a type an annotation can name, a coroutine method is not overridden in its class's hierarchy, and neither uses super(). task.cancel() takes no message. Tasks left pending are cancelled at asyncio.run()'s end, and a task group's when one of them fails, in the order they were made, where CPython's order is a set's and can change from run to run. An ExceptionGroup that escapes to the top is reported by its own line (ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)), where CPython draws the tree of the exceptions it holds; ExceptionGroup's split(), subgroup(), and derive() are not supported
  • Time is the host's monotonic clock, and the loop sleeps through WASI's poll_oneoff (in the playground's shim and the browser gate, a busy wait), except in a program using the JavaScript bridge run through the loader's start() where the browser has JSPI, which suspends instead. Equal delays set one after another fall due in the order they were set, as they do in CPython, whose clock moves between the two
  • A JavaScript value is converted where it is used, and a typed place holds one kind, so one stored where another is declared raises TypeError (a JS string where an int is declared, an integer-valued number where a float is), where Pyodide keeps it as it is. int() and float() of a JS string raise TypeError with a hint to convert through str(), where Pyodide parses it. Arithmetic on a JS value and passing a list, dict, or instance to JavaScript (pass to_js(x)) are refused. An augmented assignment to a JS property reads it as the kind of the right-hand side (a str, an int, or a float), so += 1 on a property holding a non-integer number raises TypeError where Pyodide answers a float, and its object is read twice, so it must be a name or a chain of attributes. to_js() and .to_py() go through JSON, so to_js() of None inside a list makes null where Pyodide makes undefined, a set is refused, and .to_py() of a Set, a function, a cycle, or a Map with keys that are not strings raises TypeError. A name imported from js that the page lacks raises AttributeError where it is used, not ImportError at the import. A JS value's item is read and assigned by an int (an array's, a typed array's), not a key
  • pyfetch()'s json() is stored where a type is declared (d: dict[str, int] = await r.json()) or awaited for nothing, and it parses with JavaScript's JSON.parse rather than Python's json, which differ on a number past 2**53 and on NaN. pyfetch()'s signal and fetcher, and a response's buffer(), bytes(), memoryview(), abort(), and unpack_archive(), are refused. A keyword's dict or list value is written out in the call (headers={...}) or passed through to_js()
  • A typed array's buffer methods are to_bytes(), assign(b) of bytes or a bytearray, and assign_to(b) into a bytearray. to_memoryview() answers a memoryview, which is not supported, so it is refused, and .to_py() of a typed array (a memoryview in Pyodide) raises TypeError. Copy the bytes into a bytearray with assign_to(), or out as bytes with to_bytes()
  • create_proxy() and create_once_callable() take a function defined with def at module level, or a lambda, and none of their options (capture_this, roundtrip). Passing a Python function to JavaScript without one is refused, where Pyodide passes a proxy it destroys when the call returns. A proxy called with another number of arguments than its function takes throws PythonError (TypeError: the function takes 1 positional argument but 2 were given), whose wording differs from CPython's. An exception a proxied function raises comes back to the Python that called into JavaScript as JsException (PythonError: ValueError: ...), where Pyodide raises the original exception again. One JavaScript called on its own (an event listener, an animation frame) throws the PythonError for the browser to report, where Pyodide also writes the traceback to standard error
  • A promise awaited inside asyncio.run() needs JavaScript Promise Integration and the loader's start(); without them, or inside a call JavaScript made into the module, it raises RuntimeError, since the loop cannot hand control back to JavaScript there. An exported coroutine awaits promises everywhere

The standard library

  • random.seed() of a str or bytes is refused: CPython seeds those from their SHA-512 digest. Seed with an int or a float
  • math's transcendental functions round as musl's libm does, which CPython on macOS or glibc can differ from in the last bit (see Numbers)
  • os.getcwd() is "/" and os.getpid() is 42, as on CPython for WASI, which has neither; sys.path is empty, since every import is linked at compile time
  • datetime's datetime is not a subclass of date here, so a datetime passed where a date is declared is refused (isinstance(dt, date) still answers True). The only tzinfo is timezone (a fixed offset, all the module defines), the local time zone is UTC, and date.strftime and the others render in the C locale. strptime takes a constant format, without %G, %V, %C, the %O/%E forms, or flags and widths (%-d). replace() takes the fields by name. The classes are not hashable here, so they cannot be set members or dict keys. sorted() orders them, but list.sort(), min(), and max() of instances are refused (min(days, key=lambda d: d.toordinal()) works)
  • re patterns are compiled with the program, so a pattern is written as a literal, a module constant, or a pattern compiled from one; one built at run time is refused, as are the LOCALE flag (for a bytes pattern too, since it matches by the host's locale), and a pattern CPython prints a FutureWarning for ([[, -- in a set). A pattern's findall and split are only on a pattern whose group count is known (not one held where a plain re.Pattern is declared). re.sub and re.split take count, maxsplit, and flags by name, as CPython 3.13 prints a DeprecationWarning for them by position. A group reference inside a lookbehind is refused. re.escape() of bytes needs the bytes written out, a module constant, or a parameter annotated bytes; bytes reaching it any other way are refused as bytes passed for a str
  • logging formats are compiled, so a format string is written as a literal or a module constant, and one naming a field CPython could not fill (%(pathname)s, %(filename)s, %(module)s, %(thread)d, or a name a record does not have) is refused rather than reported as a logging error at every record. A message formatted with arguments is a literal. warn() is refused, since CPython prints a DeprecationWarning for it (call warning()), as is a StreamHandler writing anywhere but sys.stdout or sys.stderr
  • logger.exception() and exc_info=True work inside an except block; in a function called from one, or outside any, they are refused, as exc_info=e is the way to pass the exception along. exc_info is True, False, None, or an exception, not a tuple. %(exc_info)s, %(exc_text)s, and stack_info= are refused
  • extra= is a dict written out in the call, of str, int, float, or bool values, and a key has one type across the program; record.x = ... adds a field where the parameter is annotated logging.LogRecord
  • logging.config.dictConfig() reads its dict at compile time, so it is written out in the call or a module constant nothing else reads; incremental=True and the . key are refused, and a factory () is a class of the program named directly. fileConfig() and listen() are refused
  • logging.handlers: TimedRotatingFileHandler keeps local time as UTC, as datetime does, so utc= changes nothing and no daylight saving time is accounted for, its atTime is refused, and its suffix may use %Y, %m, %d, %H, %M, %S, and %% only. SocketHandler, DatagramHandler, SysLogHandler, HTTPHandler, and SMTPHandler (no sockets), QueueHandler and QueueListener (no queue), and NTEventLogHandler are refused. A rotating handler's namer and rotator are not supported
  • logging.shutdown() at exit runs when a __main__ block ends, not for a program entered through main() alone
  • json.loads() parses into the type its value is declared to have, so it needs one (an annotated variable, parameter, or return); a JSON object becomes a dict[str, T], an array a list or tuple of the declared element types. A document the type cannot hold raises TypeError, where CPython would hand back the value it found: a string where an int is declared, and an integer where a float is (a float place would print 1.0 where CPython keeps the int 1, so write 1.0 in the document). json.dumps() takes its options as constants. A hook (dumps' default=, an encoder's default() method, loads' or a decoder's object_hook=) is a function whose parameter and return are annotated, since what it is called with and what it answers are written and parsed by their types: default is called for the values of its parameter's type (or a subclass), and one that does not take a value it would be called with, or that answers what it is called with again, is refused. A lambda's parameter has no type here, so a hook is a def. Every JSON object goes to an object_hook, the outer ones too, and one whose result the declared type cannot hold raises TypeError, as a value of the wrong type does; the document is checked to be JSON before the hook sees any of it. A JSONEncoder's indent is a constant, its default= argument is refused (override default() instead), as are iterencode() and reading indent back, and encode() on an encoder whose default() a subclass overrides. A JSONDecoder's object_hook is a function of the module, the program's hooks share one signature, and parse_float, parse_int, parse_constant, object_pairs_hook, strict=False, and raw_decode() are refused; so is cls= naming a class that defines __init__
  • itertools.tee() and zip_longest() take sequences, and groupby() a sequence, not a generator; itertools' functions take a function of the module or a lambda that reads nothing of the scope it is written in
  • namedtuple() is supported as a module-level assignment (a class made inside a function would be a new class on every call), with defaults= but not rename= or module=; a field given values of two types in different calls has no one type and holds ints only. A named tuple's fields cannot be assigned, as in CPython; _asdict() needs every field to have one type
  • A Counter, defaultdict, OrderedDict, deque, or ChainMap needs its element types to be known where it is made: an annotation naming it or the builtin it reads as (c: Counter[str] = Counter(), self.by: Dict[str, List[int]] = defaultdict(list)), the constructor's argument, or what the function does with the variable afterwards. An empty one used for nothing that types it holds ints. A bare annotation (c: Counter) is refused with a hint to write the types. A Counter made from a generator whose element is a method call (Counter(w.lower() for w in words)) is refused; build the list first
  • A defaultdict whose factory is a function or a lambda has no repr (CPython's shows the function's address), and a lambda factory cannot read the scope it is written in. A deque's popleft() and appendleft() move its other elements, so they cost its length, where CPython's take constant time
  • UserDict, UserList, and UserString subclasses: overriding __iter__ is refused. A UserList or UserString subclass that overrides __getitem__ cannot be walked (for, list(), sum(), and the like), since CPython walks it through that method, and a comprehension over .items() or .values() of a mapping with an overridden __getitem__ is refused, while a for loop reads each value as it goes. A method that makes an instance (a slice, +, |, copy()) is refused where the subclass's __init__ does not take the value alone, and a UserDict subclass's copy() where it has attributes of its own. Comparing a mapping view with == is refused. UserString answers the string methods it hands to its data as a str does (partition(), rfind(), istitle(), encode(), and the like); of those CPython makes a new UserString from, zfill(), removeprefix(), removesuffix(), expandtabs(), and translate() are refused on it, as are % formatting and hash()
  • collections' classes are not subclasses of dict here, though a Counter, defaultdict, or OrderedDict passes where a dict is declared (as the dict it holds) and answers isinstance(c, dict). A | merge of a defaultdict or an OrderedDict is refused, and so is a store into one reached through another object's field (self.stats.counts[w] += 1), which works on the instance's own field or a local. Two specializations are two classes, so type(Counter("a")) is type(Counter([1])) is False where CPython answers True

Memory, modules, and files

  • Memory is reclaimed by a mark-and-sweep collector that runs between statements once enough has been allocated, so a program that builds and drops objects in a loop runs in the memory its live objects need. It does not move objects or give pages back to the host, and it reads memory conservatively: a number that looks like an address keeps what it points at. Nothing is counted, so there is no cost on a store, and a module that never allocates carries no allocator or collector at all
  • A value an exported function returns to the host (a string, a list, an instance) is kept for the life of the instance, since nothing says when the host is done with it. A host that calls such a function without end holds everything it was ever handed
  • del of an attribute (del obj.x) is refused. del name is supported where the name is not read again before it is assigned: CPython raises NameError there, which a compiled variable cannot, so that program is refused, as is deleting a module variable another function uses or a variable a nested function shares
  • Imported user modules share one flat namespace in the output module, so two modules defining the same function name is a compile error naming both files
  • Every module named by a constant is linked into the program when it is compiled. importlib.import_module() of a name computed at run time loads a separately compiled module through the host (see Modules loaded by name), and __import__() of one is refused, as are their other arguments (package, fromlist) and importlib's other functions. An import in a function binds its name there, so a read before it, a read in another function, and an import inside an if, a loop, or a handler, where it may not run, are refused. A module of your own first imported in a function runs its top level when that import first runs, as in CPython, but a dynamic import of one used in place (importlib.import_module("m").f() in a function) is refused; assign the import to a name. A module whose top level raised is not run again by a later import, where CPython runs it again
  • A file nothing imports, which compile_python_project links all the same, runs its top level when the program starts, after the entry's; CPython would not run it
  • f.read() without a size reads up to 64 KiB per call; open() modes must be string literals