Skip to main content

v0.0.0 - The scaffold

Released earlier in development (pre-release).

When you type python at a terminal, an extraordinary amount of work is hidden behind the prompt. The interpreter parses your source, builds an AST, lowers that AST through a symbol table into bytecode, allocates a frame, sets up an evaluation stack, and then walks an instruction loop that has been incrementally refined for thirty years. Around that loop sits a garbage collector, a thread state, an import machinery, and a standard library that runs to several million lines. Reading the CPython source for the first time is humbling. Porting it is a multi-year project.

v0.0.0 is where we start that project. There is no parser yet. There is no compiler. There is no VM. What ships here is a Go module, a CLI entry point that prints a version banner, a build metadata package, and the surrounding infrastructure (CI, license, contributing guide, issue templates) that lets the rest of the work happen in the open. Every later release fills in the inside of the box this release puts on the table.

This page is a little shorter than the changelog entries that follow it, because there is genuinely less code in v0.0.0 than in any later cut. But the choices we made here (which Go version, which CPython version, what to name the binary, how to structure the module path) propagate through everything that comes next. So we want to walk through them before they vanish into the background.

Highlights

Two pieces of work define this release.

A Go module that compiles

The module is github.com/tamnd/gopy. It targets Go 1.26 or newer. The CLI entry point lives at cmd/gopy. You can build it and ask it to identify itself.

$ go install github.com/tamnd/gopy/cmd/gopy@v0.0.0
$ gopy --version
gopy 0.0.0 (compatible with CPython 3.14.0+) [darwin/arm64]
$ gopy --copyright
Copyright (c) 2026 Tam Nguyen. All rights reserved.
[...full Apache 2.0 notice and CPython attribution...]

That is the entirety of what the binary does at this stage. Pass anything else and it prints usage and exits non-zero. There is no REPL, no -c flag, no script execution. We are very deliberately not pretending the binary is a Python yet.

The reason the banner exists at all is that it lets us put CI in place and prove the toolchain works end to end before any interesting code lands. The first hundred contributors to a project spend a startling amount of time fighting build infrastructure; we wanted that fight resolved before there was a single line of port to argue about.

A version package that mirrors CPython's

CPython has four small C files that together identify a build: Python/getversion.c, Python/getplatform.c, Python/getcompiler.c, Python/getcopyright.c. Each exposes a single function. Together they produce the banner Python prints when it starts up.

We ported those four files to one Go package, build/. The API mirrors the upstream surface:

SymbolCPython counterpart
build.VersionPY_VERSION constant
build.PythonCompatVersionPY_VERSION we target
build.PlatformPy_GetPlatform()
build.CompilerPy_GetCompiler()
build.VersionStringPy_GetVersion()
build.CopyrightPy_GetCopyright()

build.Platform returns the host OS / arch the way Go's runtime reports it (darwin/arm64, linux/amd64, windows/amd64). build.Compiler reports the Go toolchain that built the binary, not the C compiler CPython would report, because the host language is Go.

// build/build.go
package build

const (
Version = "0.0.0"
PythonCompatVersion = "3.14.0"
)

func Platform() string { /* runtime.GOOS/GOARCH */ }
func Compiler() string { /* runtime.Version() */ }
func VersionString() string { /* gopy X.Y.Z (compatible with ...) [plat] */ }
func Copyright() string { /* Apache 2.0 + CPython attribution */ }

This package is the first one any future subsystem can import without circular dependency risk, so it is also where the project's import graph is anchored.

What's new

The full feature breakdown, grouped by area.

Module and entry point

  • Go module path github.com/tamnd/gopy. We picked the short gopy name early. It is unambiguous (the only other project with the same name was archived years ago) and types cleanly at a terminal. The CLI binary is also gopy. The symmetry matters: every future shell example in the docs starts with the same five letters.
  • go.mod targeting Go 1.26+. Go 1.26 ships with generic type aliases and the iterator improvements that CPython's port leans on heavily (every Python iterator becomes a Go iterator, and the type aliases let us share Object shapes across packages without verbose conversions). Older toolchains would force workarounds that we do not want to maintain.
  • cmd/gopy with --version and --copyright. The two flags are intentionally the only ones at this stage. Adding --help and -c and -m and the rest happens in v0.6 when the interpreter can actually do something with them.

Build metadata package

  • build/build.go. Constants for Version and PythonCompatVersion. The version constant is bumped on every release; PythonCompatVersion moves only when we move the CPython target.
  • build/platform.go. Platform string assembled from runtime.GOOS and runtime.GOARCH. Matches the convention Go's runtime/debug.BuildInfo uses, which makes downstream tooling (like crash reporters) trivially compatible.
  • build/compiler.go. Reports the Go toolchain version through runtime.Version(). We deliberately do not pretend to report a C compiler here, because the binary is a pure Go binary.
  • build/version.go. Assembles the banner CPython prints. Format: gopy X.Y.Z (compatible with CPython A.B.C+) [plat].
  • build/copyright.go. The Apache 2.0 notice plus a paragraph attributing the design and reference behavior to the CPython project under the PSF License.

License

  • Apache License 2.0. We chose Apache over MIT because the explicit patent grant matters for a runtime that may end up embedded in commercial products. We chose Apache over LGPL because we do not want to impose link-time obligations on users; this is a tool, not a library that recipients must preserve as a separable component.

Project policy documents

  • README.md. Project overview, install instructions, current status, link to the roadmap.
  • CONTRIBUTING.md. How to file an issue, how to write a PR description, the porting style we follow (1:1 from CPython with citations), the commit-message convention.
  • SECURITY.md. How to report a vulnerability privately. Because the long-term goal is to be a drop-in for CPython, any security advisory in CPython has to be evaluated against our port. The policy document points to the issue tracker and to a private email channel for embargoed reports.
  • CODE_OF_CONDUCT.md. The standard Contributor Covenant text adapted with project-specific contacts.

Continuous integration

  • CI workflow. Runs on Linux, macOS, and Windows under Go 1.26. Each platform runs go build ./..., go vet ./..., golangci-lint run, and go test ./.... Fast and quiet at this stage because there is nothing to test yet beyond the CLI flag parsing. The point is that the pipeline is green before any interesting code lands, so the first regression we see in any later cut is unambiguously a real regression.
  • Release workflow. Triggered by v* tags. Uses GoReleaser to cross-compile the gopy binary for the platform matrix, produces SHA256 checksums, and attaches GitHub attestations (build provenance) per the SLSA framework. Reproducible builds are not yet a goal but the workflow is structured so we can opt in without rewriting it.
  • Dependabot. Configured for both gomod and github-actions ecosystems. Weekly cadence. Keeping CI dependencies fresh is annoying to do manually and easy to automate.

Repository hygiene

  • .golangci.yml. Enables the strict-ish set: govet, staticcheck, errcheck, gosimple, ineffassign, unused, gofmt, goimports, revive. Plus a small project-local rule set: no interface{} (use any), no panic outside init (raise a Python exception instead), no time.Sleep in tests (use synchronization).
  • .editorconfig. Tabs for Go, spaces for everything else, LF line endings, trim trailing whitespace.
  • .gitattributes. Marks Go files and text files explicitly, marks binary fixtures as binary, sets the merge driver for go.sum to union.
  • .gitignore. Standard Go ignores plus the build cache and the editor swap files we expect to see.
  • Issue and PR templates. Bug report, feature request, port request (for "please port this CPython subsystem next"), and a PR template with the citation-and-test boxes the project requires.

Why we built it this way

A few of the choices in this release deserve their own callouts, because they affect everything downstream.

Why port CPython 3.14 rather than 3.12 or 3.13

The honest answer is that 3.14 is the first CPython release where the things we want to port are stable. Specifically:

  • The Tier 1 / Tier 2 interpreter split. 3.13 introduced the Tier 2 micro-op interpreter as an experiment. 3.14 is where the boundary firmed up enough that a port can target it without chasing every PR. Porting against 3.12 would mean porting against a single-tier loop and then re-porting on top of it; porting against 3.13 would mean chasing the micro-op set every month. 3.14 is the cut where we can plant a flag.
  • The free-threading work (PEP 703). 3.13 introduced the --disable-gil build as an experimental flag. 3.14 ships it as a supported configuration. The lock primitives, parking lot, and critical-section machinery we port in v0.1.0 are the 3.14 versions of those subsystems. Doing this against 3.12 would mean back-porting the design.
  • The JIT prep work. 3.13 and 3.14 reshape the bytecode-to-IR pipeline in preparation for a real JIT. We do not promise a JIT in gopy, but the bytecode we emit is the 3.14 bytecode, which means a future JIT effort starts from a sensible baseline rather than from an old one we would have to rewrite.

The cost of picking the newest stable release is that we sometimes hit bugs in upstream that have been fixed in master. We mirror those fixes back manually. It is less work than chasing two-version-behind compatibility would have been.

Why Go rather than Rust or C++

We considered all three. The choice came down to ergonomics and runtime fit.

Go's garbage collector matches Python's semantic model. Python objects can have cycles, can be reachable through dicts and lists, and need a tracing collector. Go gives us that collector for free. CPython itself uses reference counting plus a cycle collector; we can lean on Go's tracing GC to cover both jobs, modulo the cases where Python's semantics require deterministic destruction (file handles, locks). Those cases get explicit __del__ plumbing.

Go's goroutines match Python's threading model under free-threading. Each Python thread becomes a goroutine. The Go scheduler handles OS-thread mapping. We do not have to write the kind of scheduler glue CPython needs.

Go's standard library covers a lot of what Python's stdlib needs. encoding/json, crypto/*, net, os/exec, time, regexp (which we eventually replace; see v0.12.2) all give us a starting point. Rust would have given us many of the same building blocks at a lower level but more glue.

Go compiles fast and produces a single static binary. This matters more than it sounds. Iteration speed on a project this size is the rate-limiting factor for everything else. A Rust build that takes two minutes per change adds a day per week of waiting; a Go build that takes ten seconds gives that day back.

The downside of Go is that some CPython idioms (macros, struct layouts shared across compilation units, the inline cache machinery) do not translate directly. We handle each case as it comes up. The general rule: faithful port first, then profile, then optimize where it matters.

Why scaffold first, code second

A common pattern in new projects is to dump a thousand lines of code into the first commit and figure out the surrounding infrastructure later. We did the opposite. v0.0.0 has no interesting code at all. What it has is the infrastructure that lets later releases land without procedural friction: CI is green, the license is in place, the contributing guide is written, the issue templates exist.

The cost of doing this in v0.0.0 is one release that does nothing. The benefit is that every subsequent release lands against a working pipeline. Every code review references the contributing guide. Every CVE has a security policy to point at. Every PR has a template to follow. None of these are emergencies; all of them are friction we paid once.

Where it lives

The repository layout at v0.0.0 is intentionally small:

  • cmd/gopy/. The CLI entry point. main.go only.
  • build/. The metadata package. Four files mirroring the four CPython sources.
  • .github/workflows/. The CI and release pipelines.
  • .github/ISSUE_TEMPLATE/, .github/pull_request_template.md. The templates.
  • Top-level dotfiles. .golangci.yml, .editorconfig, .gitattributes, .gitignore, .goreleaser.yaml.
  • Top-level docs. README.md, CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, LICENSE.

The whole tree is under two thousand lines. The interesting code starts in v0.1.0.

Compatibility

Nothing user-visible runs at this stage, so there is nothing to break. But two policy choices set expectations for later releases.

  • Go 1.26 or newer. We will move the floor forward as Go releases land, but only when we actually use a feature from the newer version. No drive-by floor bumps.
  • CPython 3.14.0+ behavior. Every observable behavior we ship should match CPython 3.14 unless the changelog calls out a divergence and explains why. If you write Python that runs against CPython 3.14 and it does not run against gopy, that is a bug.

What's next

v0.1.0 is the first release with executable code. It ports the compiler-side memory arena, the threading primitives, the synchronization machinery, and the per-process hash secret. The interpreter still does not run Python code, but it has the underpinnings every later subsystem rests on.

Roughly:

  • v0.1. Foundation. Arena, threads, locks, hash secret.
  • v0.2. Object model. The core types up through dict.
  • v0.3. Exceptions.
  • v0.4. Strings, bytes, sets, the real hash machinery.
  • v0.5. Parser and AST.
  • v0.6. Compiler and VM. First Python actually runs.
  • v0.7. User-defined classes and the descriptor protocol.
  • v0.8. Generators, coroutines, async.
  • v0.9. Frame and code refinements, full VM coverage.
  • v0.10. Garbage collector and weak references.
  • v0.11. Import machinery and the first stdlib modules.
  • v0.12. Tier 2 interpreter, vendored stdlib, real regex.
  • v0.13. JIT scaffolding.
  • v0.14. Free-threading.

That roadmap is aspirational and shifts as we learn. The order holds; the individual cut dates do not.

Acknowledgments

The CPython project under the Python Software Foundation. Every file in build/ is a port from a file under CPython's Python/ directory, and the rest of the project is built the same way. The license attribution in build/copyright.go makes this explicit. We owe the CPython authors every architectural idea this project uses.