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, ...)mirroringPython/bltinmodule.c builtin_compile_impl. Routes the source through the parser at the requested mode (exec,eval,single) and returns aCodeobject. Theflagsanddont_inheritarguments are parsed and validated; future-import flags are honored as the compiler builds the parse tree.eval.goandexec.go.evalandexechonor the explicitglobals=andlocals=mappings, fall back to the calling frame's namespace when omitted, and accept either a source string or a precompiledCodeobject. PortsPython/bltinmodule.c builtin_eval_impl / builtin_exec_impl.import.go.__import__(name, globals, locals, fromlist, level)portingPython/import.c PyImport_ImportModuleLevelObject. Thefrom x import ylowering still routes through this entry, so every relative-import calculation, every package walk, everysys.modulesconsultation 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 (perLib/_pyio.py). Returns the newobjects.File. Today theencoding/errors/newline/bufferingarguments 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.mapandfilteras real iterator types (Objects/bltinmodule.c map_object / filter_object) rather than thin wrappers. Real types meanspickle.dumpson a partially-consumedmapproduces 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 fromPython/bltinmodule.cthat v0.7 parked.input()now routes throughmyreadline.Readlineso prompts behave the way CPython's prompts do.round()honours both thebanker's roundingrule forintand the float-precision rule with andigitsargument.
objects/
The object layer changes that the language-level work needed.
class.goplussuper.go.__build_class__runs the class body in a fresh namespace, populates the__class__closure cell sosuper()without arguments works, and resolves the metaclass perObjects/typeobject.c type_new.superportsObjects/typeobject.c super_init / super_getattroend to end. The closure cell trick is what lets a method body saysuper()with no arguments and still find the right type to walk: the compiler emits a closure that captures__class__, andsuper()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 throughMemberDescr. MirrorsObjects/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 theformat/itemsize/nbytes/readonlyattributes. PortsObjects/memoryobject.c PyMemoryView_FromObject. The N-D and non-contiguous cases (memoryviewover a numpy array, for example) are deferred; the v0.10.1 surface covers what stdlib code reaches for.weakref_collections.go.WeakSet,WeakValueDictionary, andWeakKeyDictionaryfromLib/weakref.py, plus theproxyandCallableProxyTypetypes fromObjects/weakrefobject.c new_weakref. These build on theWeakrefcore 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.tracebackis now a real Python type (Python/traceback.c PyTraceBack_Type) withtb_frame,tb_lineno,tb_lasti,tb_next. Goes throughtp_getsetrather than the v0.7 hand-rolledgetattr. The descriptor shape matters:traceback.print_tband thetraceback.extract_tbfamily 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 theFastToLocalssnapshot thatlocals()inside a function leans on.FastToLocalsis 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, andtypes.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, plusslice.indices(length). The range-iterator split out ofrange.gocarries the__length_hint__big-int math so partly-consumed iterators still report the right hint. Length hints matter becauselist(generator)uses them to size the output list in one allocation instead of growing it geometrically.file.go._io.FilecollapsesRawIOBase/BufferedReader/TextIOWrapperinto one object backed by*os.Fileplus abufio.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_ioC module.
myreadline/
The dispatch hook that lets a real line editor plug into the REPL.
myreadline.go.Reader/SetReader/CurrentReader/Readline/StdioReadline/ErrInterruptportsParser/myreadline.c PyOS_Readline. The dispatch hook lets a line editor (peterh/liner, a cgo shim to GNU readline, libedit) plug in viaSetReaderwithout the REPL caring; the default is the uneditedbufio.Readerfallback that mirrorsPyOS_StdioReadline.pythonrun/repl.go.InteractiveLoopnow callsmyreadline.Readlinefor every prompt, printsKeyboardInterruptonErrInterruptand continues, and exits cleanly onio.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_AWAITABLEand the async-for opcodes (SEND/END_ASYNC_FOR/CLEANUP_THROW) fromPython/ceval.c, plus the_PyCoro_GetAwaitableIterhelper. Without these,async for x in y:couldn't dispatch the way the compiler emitted it.cleanup_throw.go.CLEANUP_THROWnow surfacesStopIteration.valuesoyield fromand async-for terminate with the expected return value. The value-carrying StopIteration is the contractLib/asynciokeys off; we matched it.
errors/
The exception family fills out.
exc_warning.go.Warningplus ten subclasses (UserWarning,DeprecationWarning,PendingDeprecationWarning,SyntaxWarning,RuntimeWarning,FutureWarning,ImportWarning,UnicodeWarning,BytesWarning,EncodingWarning,ResourceWarning). MirrorsObjects/exceptions.c.exc_syntax.go.SyntaxError,IndentationError,TabError,_IncompleteInputError. The_IncompleteInputErroris 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.OSErrorplus 15 subclasses with errno mapping. The darwin-specificEAGAIN == EWOULDBLOCKdedup matches the CPythonADD_ERRNOorder so cross-platform exception identity holds.exc_unicode.go.UnicodeErrorplusUnicodeDecodeError/UnicodeEncodeError/UnicodeTranslateError. The three subclasses carryencoding,object,start,end,reasonattributes the way CPython does, so error-handling code can read the failure context.exc_group.go.BaseExceptionGroupandExceptionGroupfrom PEP 654 (Python 3.11+). Both carrysplit,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_ATTRIBUTEfor closure cells and defaults, replacing the v0.7 hand-rolledMAKE_FUNCTIONtrailer. PortsPython/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_GLOBALwith the name index packed into bits 1+ and thePUSH_NULLflag 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_finalizedplus the callback hooks (gc.callbacksrunsstart/stopevents). PortsModules/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_SAVEALLpopulatesgc.garbagewith 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. PortsParser/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_Formatports the%-style formatter fromObjects/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)portsPython/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 callingreload(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.gocarries__build_class__;objects/super.gocarriessuper;objects/slots.gocarries__slots__.objects/memoryview.gois the 1-D contiguous slice view.myreadline/myreadline.gois the dispatch hook.errors/exc_*.gois one file per exception family, matching the section split inObjects/exceptions.c.
Compatibility
A few user-visible changes are worth flagging.
compile/eval/execnow 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 passedsuper(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 tobytes(b)to avoid the error can use the real memoryview.open(...)accepts the full keyword-argument set.encoding,errors, andnewlineare 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:
_iolayer split. Today_io.Filecollapses the raw, buffered, and text layers into one Go object. v0.11 starts the split intoRawIOBase/BufferedReader/BufferedWriter/TextIOWrapperwith a real codec layer (spec 1689). The fullModules/_io/*port lands in v0.12.2.- GNU readline / libedit cgo bridge. The
myreadline.Readerdispatch hook is in place. TheModules/readline.cextension 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
_iomodule surface beyondopen._io.open,_io.FileIO,_io.BufferedReader. v0.10.1 ships only the Python-levelopenbuiltin and a collapsedFileobject.
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.