Skip to main content

v0.10.1 - The backlog drop

Released May 7, 2026.

Every long-running port carries a backlog of small things. A helper that got stubbed because the test we needed didn't reach it. A builtin that got a hand-rolled wrapper because the real C function depended on something not yet ported. A getset descriptor that's been a hand-rolled getattr since v0.4. By the time you've crossed half a dozen subsystem ports, the backlog stops being small. The hand-rolled wrappers diverge from CPython in subtle ways. The stubs return the wrong type for an edge case. The diverged-from-CPython entries start tripping each other.

v0.10.1 is the release where we paid the backlog down.

The headline ports are the ones every textbook talks about: compile, eval, exec. The trio everyone knows from CPython's public surface, the trio that lets Python evaluate Python at runtime. Behind that headline is a longer list: the __build_class__ builtin that runs every class statement, the __slots__ machinery that turns instance dicts into fixed-layout arrays, super() with and without arguments, the real __import__ driver, io.open, memoryview over bytes, the weakref collection types (WeakSet, WeakValueDictionary, WeakKeyDictionary), the traceback and frame Python objects, the PEP 263 source-encoding cookie, the %-style format operator, and the myreadline dispatch hook that lets a real line editor plug into the REPL.

None of these are individually flashy. Together they unblock about thirty downstream tests that had been parked waiting for one or another of them. After v0.10.1 lands, the runtime starts feeling like a runtime rather than a polished demo.

Highlights

Three things define this release.

compile, eval, exec

The first time a Python program calls eval, the runtime has to turn a string into bytecode and run it. The CPython surface for this lives in Python/bltinmodule.c in three sibling functions: builtin_compile_impl, builtin_eval_impl, builtin_exec_impl. They share machinery (parse, compile, evaluate against a globals / locals pair) but split on output (a code object, a returned value, no return).

We had stubbed eval to "raise NotImplementedError" since v0.3 and worked around it everywhere a test needed it. This release ports all three.

# compile / eval / exec land together.
code = compile('x + 1', '<string>', 'eval')
print(eval(code, {'x': 10})) # 11

exec('''
def greet(name):
print(f'hi {name}')
''', namespace := {})
namespace['greet']('world') # hi world

# Three valid modes for compile().
compile('x = 1', '<string>', 'exec') # statement-level
compile('x + 1', '<string>', 'eval') # expression-level
compile('x + 1', '<string>', 'single') # REPL-style (prints non-None results)

Each of the three honours its CPython signature in full. The compile builtin in builtins/compile.go takes the standard five arguments (source, filename, mode, flags, dont_inherit) and routes through the parser at the requested mode. The eval / exec builtins in builtins/eval.go and builtins/exec.go honor the explicit globals= and locals= mappings, fall back to the calling frame's namespace when omitted, and accept either a source string or a precompiled Code object. The frame-walking fallback is the CPython behaviour where calling eval('x') from inside a function reads x from the function's locals; we ported the frame walk from Python/bltinmodule.c because the heuristic is what makes interactive sessions feel native.

__build_class__, __slots__, super

These three pieces of machinery are what makes Python's class system work. Every class C(Base, metaclass=...): statement compiles to a call to __build_class__. Every class C: __slots__ = (...) reshapes the instance layout. Every super().method() walks the MRO to find the right method to call. We ported all three in this release.

class Base:
def hi(self):
return 'Base.hi'

class Sub(Base):
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def hi(self):
# super() without args picks up the class and self
# via the __class__ closure cell __build_class__ injects.
return super().hi() + ' from Sub'

s = Sub(1, 2)
print(s.hi()) # Base.hi from Sub
print(s.__slots__) # ('x', 'y')
# No __dict__: the slots ate it.
try:
s.z = 3
except AttributeError:
print('no __dict__, no extra attributes')

The trio cooperates in subtle ways. __build_class__ runs the class body in a fresh namespace, populates the __class__ closure cell so that super() without arguments can find both the defining class and the first argument of the surrounding method, and resolves the metaclass by walking the bases. Then __slots__ (if present) rewrites the type to use a fixed slot array instead of a per-instance dict. Then super uses the closure cell, plus the first positional argument, to bind the MRO walk.

We ported __build_class__ and super from Objects/typeobject.c type_new and Objects/typeobject.c super_init / super_getattro. __slots__ ports Objects/typeobject.c type_new_slots / type_new_descriptors. The MemberDescr that backs slot access is the same descriptor type CPython publishes; reading s.x walks through the descriptor protocol the way CPython 3.14 does, which matters because user-defined descriptors interleave correctly.

myreadline dispatch hook

The REPL has had a bufio.Reader.ReadString('\n') loop since v0.2. That works, but it's not what a REPL feels like. There's no line editing. There's no history. Ctrl-C kills the process instead of clearing the line. Hitting up-arrow inserts an escape sequence into the buffer.

CPython solves this with a dispatch hook. Parser/myreadline.c exposes a function pointer PyOS_ReadlineFunctionPointer that the optional readline extension overwrites at import time. If the extension is loaded, input() and the REPL prompt route through GNU readline (or libedit on macOS). If not, they fall back to a plain fgets.

We ported the dispatch hook in this release.

// myreadline/myreadline.go
type Reader interface {
Readline(prompt string) (string, error)
}

func SetReader(r Reader) { ... }
func Readline(prompt string) (string, error) { ... }

var ErrInterrupt = errors.New("Ctrl-C")

The default reader is the unedited bufio.Reader fallback that mirrors PyOS_StdioReadline. Calling SetReader swaps in a real line editor: peterh/liner, a cgo shim to GNU readline, a pure-Go libedit. The REPL doesn't care which one is installed; it calls myreadline.Readline and handles ErrInterrupt by printing KeyboardInterrupt and continuing.

The GNU readline / libedit cgo bridge is the v0.11 follow-up. Today the dispatch hook is in place; tomorrow's Modules/readline.c port plugs into it.

What's new

The full feature breakdown, grouped by package.

builtins/

The headline batch and the long tail of odds-and-ends from Python/bltinmodule.c.

  • compile.go. compile(source, filename, mode, ...) mirroring Python/bltinmodule.c builtin_compile_impl. Routes the source through the parser at the requested mode (exec, eval, single) and returns a Code object. The flags and dont_inherit arguments are parsed and validated; future-import flags are honored as the compiler builds the parse tree.
  • eval.go and exec.go. eval and exec honor the explicit globals= and locals= mappings, fall back to the calling frame's namespace when omitted, and accept either a source string or a precompiled Code object. Ports Python/bltinmodule.c builtin_eval_impl / builtin_exec_impl.
  • import.go. __import__(name, globals, locals, fromlist, level) porting Python/import.c PyImport_ImportModuleLevelObject. The from x import y lowering still routes through this entry, so every relative-import calculation, every package walk, every sys.modules consultation goes through one function rather than three competing call paths.
  • open.go. open(file, mode, buffering, encoding, errors, newline, closefd, opener) with full mode-string validation (per Lib/_pyio.py). Returns the new objects.File. Today the encoding / errors / newline / buffering arguments are parsed and validated but not honored; the file is utf-8 with universal newlines. The proper layer split lands in v0.11.
  • iter_map_filter.go. map and filter as real iterator types (Objects/bltinmodule.c map_object / filter_object) rather than thin wrappers. Real types means pickle.dumps on a partially-consumed map produces bytes that round-trip; thin wrappers would have surfaced a Go closure that pickle can't serialize.
  • aiter_anext.go, input.go, globals_locals.go, round.go. The remaining odds-and-ends from Python/bltinmodule.c that v0.7 parked. input() now routes through myreadline.Readline so prompts behave the way CPython's prompts do. round() honours both the banker's rounding rule for int and the float-precision rule with a ndigits argument.

objects/

The object layer changes that the language-level work needed.

  • class.go plus super.go. __build_class__ runs the class body in a fresh namespace, populates the __class__ closure cell so super() without arguments works, and resolves the metaclass per Objects/typeobject.c type_new. super ports Objects/typeobject.c super_init / super_getattro end to end. The closure cell trick is what lets a method body say super() with no arguments and still find the right type to walk: the compiler emits a closure that captures __class__, and super() reads from the cell. We ported the emit logic too; v0.7 had been hand-rolling this inline.
  • slots.go. __slots__ on a user-defined class drops the per-instance __dict__, allocates a fixed-layout slot array on the type, and routes attribute access through MemberDescr. Mirrors Objects/typeobject.c type_new_slots / type_new_descriptors. The fixed-layout array matters for programs that create millions of small objects: a slotted class costs roughly half the memory of a dict-backed one because there's no hash-table overhead per instance.
  • memoryview.go. memoryview(b) over a 1-D contiguous bytes buffer with __getitem__, __len__, tobytes, tolist, release, and the format / itemsize / nbytes / readonly attributes. Ports Objects/memoryobject.c PyMemoryView_FromObject. The N-D and non-contiguous cases (memoryview over a numpy array, for example) are deferred; the v0.10.1 surface covers what stdlib code reaches for.
  • weakref_collections.go. WeakSet, WeakValueDictionary, and WeakKeyDictionary from Lib/weakref.py, plus the proxy and CallableProxyType types from Objects/weakrefobject.c new_weakref. These build on the Weakref core that landed in v0.10.0; the collector clears weakrefs first, so the collections see the right "is the referent still alive" answer immediately.
  • traceback.go. traceback is now a real Python type (Python/traceback.c PyTraceBack_Type) with tb_frame, tb_lineno, tb_lasti, tb_next. Goes through tp_getset rather than the v0.7 hand-rolled getattr. The descriptor shape matters: traceback.print_tb and the traceback.extract_tb family both walk traceback objects through attribute access, and the getset shape is what makes attribute access consistent across the surface.
  • frame.go. The Python-level frame wrapper (Objects/frameobject.c PyFrame_Type) plus the FastToLocals snapshot that locals() inside a function leans on. FastToLocals is the function that turns the fast-local cells (the stack-allocated locals the bytecode reads directly) into a dict; without it, locals() inside a function returned the wrong thing.
  • ellipsis.go, capsule.go, simplenamespace.go. The remaining small singletons and containers: Ellipsis, PyCapsule, and types.SimpleNamespace. Each is a faithful port of its CPython counterpart (Objects/sliceobject.c Py_Ellipsis, Objects/capsule.c, Lib/types.py SimpleNamespace).
  • slice.go. PySlice_Unpack, PySlice_AdjustIndices, PySlice_GetIndicesEx, plus slice.indices(length). The range-iterator split out of range.go carries the __length_hint__ big-int math so partly-consumed iterators still report the right hint. Length hints matter because list(generator) uses them to size the output list in one allocation instead of growing it geometrically.
  • file.go. _io.File collapses RawIOBase / BufferedReader / TextIOWrapper into one object backed by *os.File plus a bufio.Reader / bufio.Writer. Methods (read, readline, write, close, flush, __enter__, __exit__, readable, writable) and attributes (name, mode, closed) match CPython 3.14. The split into separate raw / buffered / text layers is a 1689 follow-up that lands in v0.12.2 along with the full _io C module.

myreadline/

The dispatch hook that lets a real line editor plug into the REPL.

  • myreadline.go. Reader / SetReader / CurrentReader / Readline / StdioReadline / ErrInterrupt ports Parser/myreadline.c PyOS_Readline. The dispatch hook lets a line editor (peterh/liner, a cgo shim to GNU readline, libedit) plug in via SetReader without the REPL caring; the default is the unedited bufio.Reader fallback that mirrors PyOS_StdioReadline.
  • pythonrun/repl.go. InteractiveLoop now calls myreadline.Readline for every prompt, prints KeyboardInterrupt on ErrInterrupt and continues, and exits cleanly on io.EOF. PS2 continuation still feeds one logical line at a time; the parser doesn't yet surface "input incomplete".

vm/

Two opcodes from the async iteration family.

  • async.go. GET_AWAITABLE and the async-for opcodes (SEND / END_ASYNC_FOR / CLEANUP_THROW) from Python/ceval.c, plus the _PyCoro_GetAwaitableIter helper. Without these, async for x in y: couldn't dispatch the way the compiler emitted it.
  • cleanup_throw.go. CLEANUP_THROW now surfaces StopIteration.value so yield from and async-for terminate with the expected return value. The value-carrying StopIteration is the contract Lib/asyncio keys off; we matched it.

errors/

The exception family fills out.

  • exc_warning.go. Warning plus ten subclasses (UserWarning, DeprecationWarning, PendingDeprecationWarning, SyntaxWarning, RuntimeWarning, FutureWarning, ImportWarning, UnicodeWarning, BytesWarning, EncodingWarning, ResourceWarning). Mirrors Objects/exceptions.c.
  • exc_syntax.go. SyntaxError, IndentationError, TabError, _IncompleteInputError. The _IncompleteInputError is the one the REPL keys off to know whether to ask for more input; multi-line PS2 continuation needs it (deferred to v0.11).
  • exc_os.go. OSError plus 15 subclasses with errno mapping. The darwin-specific EAGAIN == EWOULDBLOCK dedup matches the CPython ADD_ERRNO order so cross-platform exception identity holds.
  • exc_unicode.go. UnicodeError plus UnicodeDecodeError / UnicodeEncodeError / UnicodeTranslateError. The three subclasses carry encoding, object, start, end, reason attributes the way CPython does, so error-handling code can read the failure context.
  • exc_group.go. BaseExceptionGroup and ExceptionGroup from PEP 654 (Python 3.11+). Both carry split, subgroup, derive, __class_getitem__ matching CPython 3.14.
  • FloatingPointError. Rare in modern Python but part of the documented exception family; we shipped it for completeness.

compile/

Two pieces of bytecode-emit machinery the v0.7 era had hand-rolled.

  • set_function_attribute.go. SET_FUNCTION_ATTRIBUTE for closure cells and defaults, replacing the v0.7 hand-rolled MAKE_FUNCTION trailer. Ports Python/compile.c compiler_make_closure. The v0.7 trailer worked for the cases we'd tested but emitted the wrong opcode sequence when defaults and closures were both present; the ported version walks the same decision tree CPython does.
  • load_global.go. LOAD_GLOBAL with the name index packed into bits 1+ and the PUSH_NULL flag in bit 0, matching CPython 3.11+. The encoding lets the dispatcher fuse the "load global, push null for the receiver slot" pair into one opcode when the global is being called.

gc/

The collector that landed in v0.10.0 picks up the debug surface and the resurrection path it deferred.

  • debug.go, stats.go. gc.set_debug / gc.get_debug / gc.get_stats / gc.is_finalized plus the callback hooks (gc.callbacks runs start / stop events). Ports Modules/gcmodule.c gc_set_debug_impl / gc_get_stats_impl. The callback hooks complete the work the v0.10.0 release parked.
  • garbage.go. DEBUG_SAVEALL populates gc.garbage with the unreclaimable objects; resurrection detection re-tracks objects whose finalizer pulled them back into the live graph.

lexer/

PEP 263 finally lands.

  • cookie.go. The source-encoding cookie. The first two physical lines are scanned for # -*- coding: ... -*-; the source is then transcoded into UTF-8 before tokenization. Ports Parser/tokenizer.c check_coding_spec. Most modern Python is UTF-8 already, but a surprising amount of legacy code carries Latin-1 or cp1252 cookies, and rejecting them breaks upstream-vendored test fixtures.

unicode/

The %-style formatter.

  • format.go. PyUnicode_Format ports the %-style formatter from Objects/unicodeobject.c do_string_format. All the format codes: %s, %r, %a, %d, %i, %u, %o, %x, %X, %e, %E, %f, %F, %g, %G, %c, %%, plus the flag / width / precision parser. Newer code uses f-strings, but %-style is still everywhere in stdlib and the legacy ecosystem.

imp/

One module-reloader port.

  • reload.go. imp.reload(module) ports Python/import.c PyImport_ReloadModule. Re-executes the module body against the existing module dict so live references stay valid. Important for interactive development: editing a module and calling reload(module) updates the module in place without invalidating the references other modules hold to it.

Why we built it this way

Three calls deserve a callout.

Why compile / eval / exec ship together

The three share more than they differ. They all parse, they all build a code object, they all evaluate against a globals / locals pair. The only differences are what they return (a code object vs. a value vs. nothing) and which mode they parse at.

We could have shipped compile in one release, eval in the next, exec in the third. Each would have been a quick PR. We chose to ship them together because the shared machinery (compileSource, the namespace coercion, the frame-walk fallback when globals is omitted) is the actual work, and splitting the three releases would have meant rewriting that machinery three times across three releases as each one revealed an edge case the previous shipping had not noticed.

A single release lets us write the namespace-coercion logic once, have all three exercise it, and tune it against the union of their edge cases. The behavior table at the bottom of Python/bltinmodule.c builtin_exec_impl was the spec; one Go function implements every row.

Why __build_class__ injects a closure cell

When you write super() with no arguments, the runtime needs two things: the class being defined (so it knows where to walk the MRO from) and the first positional argument of the surrounding method (so it knows what bound receiver to use). The second is easy, but the first is subtle: by the time super() runs, the class body has finished executing and the class object exists, but the method that calls super() doesn't have a direct reference to it.

CPython solves this with a compiler trick. If the parser sees super anywhere in a method body, the compiler emits a free variable named __class__, and __build_class__ populates that cell with the class object right after creating it. super() reads from the cell.

We ported the trick verbatim because the alternative (walking frame metadata at runtime to find the class) is slower and gives the wrong answer for nested classes. The closure-cell approach just works.

Why WeakSet and friends live in objects/ not stdlib/

CPython publishes WeakSet, WeakValueDictionary, and WeakKeyDictionary from Lib/weakref.py, a pure-Python module. We could have vendored that file and let the import machinery load it.

The catch: weakref.py imports _weakrefset and _collections_abc, both of which we didn't have yet. Vendoring weakref.py meant porting both of those too, plus the C bits they called into. The shortcut was to write the three collections as Go objects under objects/weakref_collections.go and publish them on the weakref module surface. Same observable behaviour, no dependency tail.

We'll re-vendor Lib/weakref.py once _collections_abc lands (v0.12.2). For v0.10.1 the Go implementation is what works.

Where it lives

  • builtins/compile.go, builtins/eval.go, builtins/exec.go. The three-way trio.
  • objects/class.go carries __build_class__; objects/super.go carries super; objects/slots.go carries __slots__.
  • objects/memoryview.go is the 1-D contiguous slice view.
  • myreadline/myreadline.go is the dispatch hook.
  • errors/exc_*.go is one file per exception family, matching the section split in Objects/exceptions.c.

Compatibility

A few user-visible changes are worth flagging.

  • compile / eval / exec now work. Code that detected the v0.3 stub and worked around it (custom AST walkers, alternative evaluators) can drop the workaround.
  • super() with no arguments now works inside methods. Code that explicitly passed super(cls, self) everywhere because the v0.7 stub didn't pick up the closure cell can drop the explicit arguments.
  • memoryview(b) now returns a real object. Previous releases raised TypeError; programs that fell back to bytes(b) to avoid the error can use the real memoryview.
  • open(...) accepts the full keyword-argument set. encoding, errors, and newline are still ignored (deferred to v0.11), but the call no longer raises TypeError on unfamiliar keywords. Programs that relied on the TypeError as a sentinel will need to switch to a feature-detection check.
  • PEP 263 source cookies are now honored. A file with # -*- coding: latin-1 -*- at the top will be decoded as Latin-1 before tokenization. Files that relied on the v0.10.0 behaviour of treating every byte as UTF-8 (and tolerating invalid sequences) may see a SyntaxError.

What's next

The remaining v0.10.1 follow-ups pin to v0.11:

  • _io layer split. Today _io.File collapses the raw, buffered, and text layers into one Go object. v0.11 starts the split into RawIOBase / BufferedReader / BufferedWriter / TextIOWrapper with a real codec layer (spec 1689). The full Modules/_io/* port lands in v0.12.2.
  • GNU readline / libedit cgo bridge. The myreadline.Reader dispatch hook is in place. The Modules/readline.c extension that plugs into it is the v0.11 ship.
  • Multi-line PS2 continuation in the REPL. The parser needs to surface "input incomplete" (bracket counting, triple quotes, indented block expected). Today the REPL feeds one logical line at a time.
  • The _io module surface beyond open. _io.open, _io.FileIO, _io.BufferedReader. v0.10.1 ships only the Python-level open builtin and a collapsed File object.

The v0.10.2 release (already in flight) is the parser drop: real Lib/test/test_grammar.py panel parsing, byte-identical ast.dump output against CPython, and the long tail of grammar helper-function ports.

Acknowledgments

This release closes a backlog accumulated across v0.2 through v0.10. The internal specs covered include the language-level machinery (compile / eval / exec / __build_class__ / super / __slots__), the myreadline dispatch hook, the weakref collections, the traceback and frame Python objects, and the long tail of exception subclasses. The pull request that shipped this release closed roughly twenty parked issues from the v0.7 - v0.9 stretch.