Reorganize upb file structure

This change moves almost everything in the `upb/` directory up one level, so
that for example `upb/upb/generated_code_support.h` becomes just
`upb/generated_code_support.h`. The only exceptions I made to this were that I
left `upb/cmake` and `upb/BUILD` where they are, mostly because that avoids
conflict with other files and the current locations seem reasonable for now.

The `python/` directory is a little bit of a challenge because we had to merge
the existing directory there with `upb/python/`. I made `upb/python/BUILD` into
the BUILD file for the merged directory, and it effectively loads the contents
of the other BUILD file via `python/build_targets.bzl`, but I plan to clean
this up soon.

PiperOrigin-RevId: 568651768
diff --git a/python/BUILD b/python/BUILD
new file mode 100644
index 0000000..c1e044c
--- /dev/null
+++ b/python/BUILD
@@ -0,0 +1,233 @@
+# Copyright (c) 2009-2021, Google LLC
+# All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+load("//python:py_extension.bzl", "py_extension")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
+load("//bazel:build_defs.bzl", "UPB_DEFAULT_COPTS")
+
+# begin:github_only
+load("@rules_pkg//:mappings.bzl", "pkg_files")
+load("//python:build_targets.bzl", "build_targets")
+build_targets(name = "python")
+# end:github_only
+
+licenses(["notice"])
+
+package(
+    # begin:google_only
+#     default_applicable_licenses = ["//upb:license"],
+    # end:google_only
+    default_visibility = ["//python/dist:__pkg__"],
+)
+
+LIMITED_API_FLAG_SELECT = {
+    ":limited_api_3.7": ["-DPy_LIMITED_API=0x03070000"],
+    ":limited_api_3.10": ["-DPy_LIMITED_API=0x030a0000"],
+    "//conditions:default": [],
+}
+
+bool_flag(
+    name = "limited_api",
+    build_setting_default = True,
+)
+
+string_flag(
+    name = "python_version",
+    build_setting_default = "system",
+    values = [
+        "system",
+        "37",
+        "38",
+        "39",
+        "310",
+    ],
+)
+
+config_setting(
+    name = "limited_api_3.7",
+    flag_values = {
+        ":limited_api": "True",
+        ":python_version": "37",
+    },
+)
+
+config_setting(
+    name = "full_api_3.7_win32",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "37",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "full_api_3.7_win64",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "37",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "full_api_3.7",
+    match_any = [
+        ":full_api_3.7_win32",
+        ":full_api_3.7_win64",
+    ],
+)
+
+config_setting(
+    name = "full_api_3.8_win32",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "38",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "full_api_3.8_win64",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "38",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "full_api_3.8",
+    match_any = [
+        ":full_api_3.8_win32",
+        ":full_api_3.8_win64",
+    ],
+)
+
+config_setting(
+    name = "full_api_3.9_win32",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "39",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "full_api_3.9_win64",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "39",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "full_api_3.9",
+    match_any = [
+        "full_api_3.9_win32",
+        ":full_api_3.9_win64",
+    ],
+)
+
+config_setting(
+    name = "limited_api_3.10_win32",
+    flag_values = {
+        ":limited_api": "True",
+        ":python_version": "310",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "limited_api_3.10_win64",
+    flag_values = {
+        ":limited_api": "True",
+        ":python_version": "310",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "limited_api_3.10",
+    match_any = [
+        ":limited_api_3.10_win32",
+        ":limited_api_3.10_win64",
+    ],
+)
+
+# begin:github_only
+_message_target_compatible_with = {
+   "@platforms//os:windows": ["@platforms//:incompatible"],
+   "@system_python//:none": ["@platforms//:incompatible"],
+   "@system_python//:unsupported": ["@platforms//:incompatible"],
+   "//conditions:default": [],
+}
+
+# end:github_only
+# begin:google_only
+# _message_target_compatible_with = {
+#     "@platforms//os:windows": ["@platforms//:incompatible"],
+#     "//conditions:default": [],
+# }
+# end:google_only
+
+filegroup(
+    name = "message_srcs",
+    srcs = [
+        "convert.c",
+        "convert.h",
+        "descriptor.c",
+        "descriptor.h",
+        "descriptor_containers.c",
+        "descriptor_containers.h",
+        "descriptor_pool.c",
+        "descriptor_pool.h",
+        "extension_dict.c",
+        "extension_dict.h",
+        "map.c",
+        "map.h",
+        "message.c",
+        "message.h",
+        "protobuf.c",
+        "protobuf.h",
+        "python_api.h",
+        "repeated.c",
+        "repeated.h",
+        "unknown_fields.c",
+        "unknown_fields.h",
+    ],
+    # begin:google_only
+#     compatible_with = ["//buildenv/target:non_prod"],
+    # end:google_only
+)
+
+py_extension(
+    name = "_message",
+    srcs = [":message_srcs"],
+    copts = UPB_DEFAULT_COPTS + select(LIMITED_API_FLAG_SELECT) + [
+        # The Python API requires patterns that are ISO C incompatible, like
+        # casts between function pointers and object pointers.
+        "-Wno-pedantic",
+    ],
+    target_compatible_with = select(_message_target_compatible_with),
+    deps = [
+        "//upb:collections",
+        "//upb:descriptor_upb_proto_reflection",
+        "//upb:eps_copy_input_stream",
+        "//upb:hash",
+        "//upb:message_copy",
+        "//upb:port",
+        "//upb:reflection",
+        "//upb:text",
+        "//upb:wire_reader",
+        "//upb:wire_types",
+        "//upb/util:compare",
+        "//upb/util:def_to_proto",
+        "//upb/util:required_fields",
+    ],
+)
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
deleted file mode 100644
index b42d0b2..0000000
--- a/python/BUILD.bazel
+++ /dev/null
@@ -1,6 +0,0 @@
-load("//python:build_targets.bzl", "build_targets")
-
-# The build targets for this package have been temporarily moved into a Bazel
-# macro to facilitate merging upb's Python support into this directory. Once
-# that merge is complete, we will move the build targets back here.
-build_targets(name = "python")
diff --git a/python/README.md b/python/README.md
index baa58c2..ab39350 100644
--- a/python/README.md
+++ b/python/README.md
@@ -21,8 +21,8 @@
 can use the following Bazel commands:
 
 ```
-$ bazel build //upb/python/dist:source_wheel
-$ bazel build //upb/python/dist:binary_wheel
+$ bazel build //python/dist:source_wheel
+$ bazel build //python/dist:binary_wheel
 ```
 
 The binary wheel will build against whatever version of Python is installed on
@@ -43,13 +43,12 @@
 following values:
 
 1.  **upb**: Built on the
-    [upb C library](https://github.com/protocolbuffers/upb), this is a new
-    extension module
+    [upb C library](https://github.com/protocolbuffers/protobuf/tree/main/upb),
+    this is a new extension module
     [released in 4.21.0](https://protobuf.dev/news/2022-05-06/). It offers
     better performance than any of the previous backends, and it is now the
     default. It is distributed in our PyPI packages, and requires no special
-    installation. The code for this module lives in
-    [upb/python](https://github.com/protocolbuffers/protobuf/tree/main/upb/python).
+    installation. The code for this module lives in this directory.
 1.  **cpp**: This extension module wraps the C++ protobuf library. It is
     deprecated and is no longer released in our PyPI packages, however it is
     still used in some legacy cases where apps want to perform zero-copy message
diff --git a/python/build_targets.bzl b/python/build_targets.bzl
index 546ef01..026292c 100644
--- a/python/build_targets.bzl
+++ b/python/build_targets.bzl
@@ -444,7 +444,7 @@
             "tox.ini",
         ],
         strip_prefix = "",
-        visibility = ["//upb:__subpackages__"],
+        visibility = ["//python/dist:__pkg__"],
     )
 
     pkg_files(
@@ -456,9 +456,10 @@
             "google/protobuf/pyext/*.cc",
             "google/protobuf/pyext/*.h",
         ]) + [
-            "BUILD.bazel",
+            "BUILD",
             "MANIFEST.in",
             "README.md",
+            "build_targets.bzl",
             "google/protobuf/proto_api.h",
             "google/protobuf/pyext/README",
             "google/protobuf/python_protobuf.h",
diff --git a/python/convert.c b/python/convert.c
new file mode 100644
index 0000000..98d9b75
--- /dev/null
+++ b/python/convert.c
@@ -0,0 +1,446 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/convert.h"
+
+#include "python/message.h"
+#include "python/protobuf.h"
+#include "upb/collections/map.h"
+#include "upb/reflection/message.h"
+#include "upb/util/compare.h"
+
+// Must be last.
+#include "upb/port/def.inc"
+
+PyObject* PyUpb_UpbToPy(upb_MessageValue val, const upb_FieldDef* f,
+                        PyObject* arena) {
+  switch (upb_FieldDef_CType(f)) {
+    case kUpb_CType_Enum:
+    case kUpb_CType_Int32:
+      return PyLong_FromLong(val.int32_val);
+    case kUpb_CType_Int64:
+      return PyLong_FromLongLong(val.int64_val);
+    case kUpb_CType_UInt32:
+      return PyLong_FromSize_t(val.uint32_val);
+    case kUpb_CType_UInt64:
+      return PyLong_FromUnsignedLongLong(val.uint64_val);
+    case kUpb_CType_Float:
+      return PyFloat_FromDouble(val.float_val);
+    case kUpb_CType_Double:
+      return PyFloat_FromDouble(val.double_val);
+    case kUpb_CType_Bool:
+      return PyBool_FromLong(val.bool_val);
+    case kUpb_CType_Bytes:
+      return PyBytes_FromStringAndSize(val.str_val.data, val.str_val.size);
+    case kUpb_CType_String: {
+      PyObject* ret =
+          PyUnicode_DecodeUTF8(val.str_val.data, val.str_val.size, NULL);
+      // If the string can't be decoded in UTF-8, just return a bytes object
+      // that contains the raw bytes. This can't happen if the value was
+      // assigned using the members of the Python message object, but can happen
+      // if the values were parsed from the wire (binary).
+      if (ret == NULL) {
+        PyErr_Clear();
+        ret = PyBytes_FromStringAndSize(val.str_val.data, val.str_val.size);
+      }
+      return ret;
+    }
+    case kUpb_CType_Message:
+      return PyUpb_Message_Get((upb_Message*)val.msg_val,
+                               upb_FieldDef_MessageSubDef(f), arena);
+    default:
+      PyErr_Format(PyExc_SystemError,
+                   "Getting a value from a field of unknown type %d",
+                   upb_FieldDef_CType(f));
+      return NULL;
+  }
+}
+
+static bool PyUpb_GetInt64(PyObject* obj, int64_t* val) {
+  // We require that the value is either an integer or has an __index__
+  // conversion.
+  obj = PyNumber_Index(obj);
+  if (!obj) return false;
+  // If the value is already a Python long, PyLong_AsLongLong() retrieves it.
+  // Otherwise is converts to integer using __int__.
+  *val = PyLong_AsLongLong(obj);
+  bool ok = true;
+  if (PyErr_Occurred()) {
+    assert(PyErr_ExceptionMatches(PyExc_OverflowError));
+    PyErr_Clear();
+    PyErr_Format(PyExc_ValueError, "Value out of range: %S", obj);
+    ok = false;
+  }
+  Py_DECREF(obj);
+  return ok;
+}
+
+static bool PyUpb_GetUint64(PyObject* obj, uint64_t* val) {
+  // We require that the value is either an integer or has an __index__
+  // conversion.
+  obj = PyNumber_Index(obj);
+  if (!obj) return false;
+  *val = PyLong_AsUnsignedLongLong(obj);
+  bool ok = true;
+  if (PyErr_Occurred()) {
+    assert(PyErr_ExceptionMatches(PyExc_OverflowError));
+    PyErr_Clear();
+    PyErr_Format(PyExc_ValueError, "Value out of range: %S", obj);
+    ok = false;
+  }
+  Py_DECREF(obj);
+  return ok;
+}
+
+static bool PyUpb_GetInt32(PyObject* obj, int32_t* val) {
+  int64_t i64;
+  if (!PyUpb_GetInt64(obj, &i64)) return false;
+  if (i64 < INT32_MIN || i64 > INT32_MAX) {
+    PyErr_Format(PyExc_ValueError, "Value out of range: %S", obj);
+    return false;
+  }
+  *val = i64;
+  return true;
+}
+
+static bool PyUpb_GetUint32(PyObject* obj, uint32_t* val) {
+  uint64_t u64;
+  if (!PyUpb_GetUint64(obj, &u64)) return false;
+  if (u64 > UINT32_MAX) {
+    PyErr_Format(PyExc_ValueError, "Value out of range: %S", obj);
+    return false;
+  }
+  *val = u64;
+  return true;
+}
+
+// If `arena` is specified, copies the string data into the given arena.
+// Otherwise aliases the given data.
+static upb_MessageValue PyUpb_MaybeCopyString(const char* ptr, size_t size,
+                                              upb_Arena* arena) {
+  upb_MessageValue ret;
+  ret.str_val.size = size;
+  if (arena) {
+    char* buf = upb_Arena_Malloc(arena, size);
+    memcpy(buf, ptr, size);
+    ret.str_val.data = buf;
+  } else {
+    ret.str_val.data = ptr;
+  }
+  return ret;
+}
+
+const char* upb_FieldDef_TypeString(const upb_FieldDef* f) {
+  switch (upb_FieldDef_CType(f)) {
+    case kUpb_CType_Double:
+      return "double";
+    case kUpb_CType_Float:
+      return "float";
+    case kUpb_CType_Int64:
+      return "int64";
+    case kUpb_CType_Int32:
+      return "int32";
+    case kUpb_CType_UInt64:
+      return "uint64";
+    case kUpb_CType_UInt32:
+      return "uint32";
+    case kUpb_CType_Enum:
+      return "enum";
+    case kUpb_CType_Bool:
+      return "bool";
+    case kUpb_CType_String:
+      return "string";
+    case kUpb_CType_Bytes:
+      return "bytes";
+    case kUpb_CType_Message:
+      return "message";
+  }
+  UPB_UNREACHABLE();
+}
+
+static bool PyUpb_PyToUpbEnum(PyObject* obj, const upb_EnumDef* e,
+                              upb_MessageValue* val) {
+  if (PyUnicode_Check(obj)) {
+    Py_ssize_t size;
+    const char* name = PyUnicode_AsUTF8AndSize(obj, &size);
+    const upb_EnumValueDef* ev =
+        upb_EnumDef_FindValueByNameWithSize(e, name, size);
+    if (!ev) {
+      PyErr_Format(PyExc_ValueError, "unknown enum label \"%s\"", name);
+      return false;
+    }
+    val->int32_val = upb_EnumValueDef_Number(ev);
+    return true;
+  } else {
+    int32_t i32;
+    if (!PyUpb_GetInt32(obj, &i32)) return false;
+    if (upb_FileDef_Syntax(upb_EnumDef_File(e)) == kUpb_Syntax_Proto2 &&
+        !upb_EnumDef_CheckNumber(e, i32)) {
+      PyErr_Format(PyExc_ValueError, "invalid enumerator %d", (int)i32);
+      return false;
+    }
+    val->int32_val = i32;
+    return true;
+  }
+}
+
+bool PyUpb_IsNumpyNdarray(PyObject* obj, const upb_FieldDef* f) {
+  PyObject* type_name_obj =
+      PyObject_GetAttrString((PyObject*)Py_TYPE(obj), "__name__");
+  bool is_ndarray = false;
+  if (!strcmp(PyUpb_GetStrData(type_name_obj), "ndarray")) {
+    PyErr_Format(PyExc_TypeError,
+                 "%S has type ndarray, but expected one of: %s", obj,
+                 upb_FieldDef_TypeString(f));
+    is_ndarray = true;
+  }
+  Py_DECREF(type_name_obj);
+  return is_ndarray;
+}
+
+bool PyUpb_PyToUpb(PyObject* obj, const upb_FieldDef* f, upb_MessageValue* val,
+                   upb_Arena* arena) {
+  switch (upb_FieldDef_CType(f)) {
+    case kUpb_CType_Enum:
+      return PyUpb_PyToUpbEnum(obj, upb_FieldDef_EnumSubDef(f), val);
+    case kUpb_CType_Int32:
+      return PyUpb_GetInt32(obj, &val->int32_val);
+    case kUpb_CType_Int64:
+      return PyUpb_GetInt64(obj, &val->int64_val);
+    case kUpb_CType_UInt32:
+      return PyUpb_GetUint32(obj, &val->uint32_val);
+    case kUpb_CType_UInt64:
+      return PyUpb_GetUint64(obj, &val->uint64_val);
+    case kUpb_CType_Float:
+      if (PyUpb_IsNumpyNdarray(obj, f)) return false;
+      val->float_val = PyFloat_AsDouble(obj);
+      return !PyErr_Occurred();
+    case kUpb_CType_Double:
+      if (PyUpb_IsNumpyNdarray(obj, f)) return false;
+      val->double_val = PyFloat_AsDouble(obj);
+      return !PyErr_Occurred();
+    case kUpb_CType_Bool:
+      if (PyUpb_IsNumpyNdarray(obj, f)) return false;
+      val->bool_val = PyLong_AsLong(obj);
+      return !PyErr_Occurred();
+    case kUpb_CType_Bytes: {
+      char* ptr;
+      Py_ssize_t size;
+      if (PyBytes_AsStringAndSize(obj, &ptr, &size) < 0) return false;
+      *val = PyUpb_MaybeCopyString(ptr, size, arena);
+      return true;
+    }
+    case kUpb_CType_String: {
+      Py_ssize_t size;
+      const char* ptr;
+      PyObject* unicode = NULL;
+      if (PyBytes_Check(obj)) {
+        unicode = obj = PyUnicode_FromEncodedObject(obj, "utf-8", NULL);
+        if (!obj) return false;
+      }
+      ptr = PyUnicode_AsUTF8AndSize(obj, &size);
+      if (PyErr_Occurred()) {
+        Py_XDECREF(unicode);
+        return false;
+      }
+      *val = PyUpb_MaybeCopyString(ptr, size, arena);
+      Py_XDECREF(unicode);
+      return true;
+    }
+    case kUpb_CType_Message:
+      PyErr_Format(PyExc_ValueError, "Message objects may not be assigned");
+      return false;
+    default:
+      PyErr_Format(PyExc_SystemError,
+                   "Getting a value from a field of unknown type %d",
+                   upb_FieldDef_CType(f));
+      return false;
+  }
+}
+
+bool upb_Message_IsEqual(const upb_Message* msg1, const upb_Message* msg2,
+                         const upb_MessageDef* m);
+
+// -----------------------------------------------------------------------------
+// Equal
+// -----------------------------------------------------------------------------
+
+bool PyUpb_ValueEq(upb_MessageValue val1, upb_MessageValue val2,
+                   const upb_FieldDef* f) {
+  switch (upb_FieldDef_CType(f)) {
+    case kUpb_CType_Bool:
+      return val1.bool_val == val2.bool_val;
+    case kUpb_CType_Int32:
+    case kUpb_CType_UInt32:
+    case kUpb_CType_Enum:
+      return val1.int32_val == val2.int32_val;
+    case kUpb_CType_Int64:
+    case kUpb_CType_UInt64:
+      return val1.int64_val == val2.int64_val;
+    case kUpb_CType_Float:
+      return val1.float_val == val2.float_val;
+    case kUpb_CType_Double:
+      return val1.double_val == val2.double_val;
+    case kUpb_CType_String:
+    case kUpb_CType_Bytes:
+      return val1.str_val.size == val2.str_val.size &&
+             memcmp(val1.str_val.data, val2.str_val.data, val1.str_val.size) ==
+                 0;
+    case kUpb_CType_Message:
+      return upb_Message_IsEqual(val1.msg_val, val2.msg_val,
+                                 upb_FieldDef_MessageSubDef(f));
+    default:
+      return false;
+  }
+}
+
+bool PyUpb_Map_IsEqual(const upb_Map* map1, const upb_Map* map2,
+                       const upb_FieldDef* f) {
+  assert(upb_FieldDef_IsMap(f));
+  if (map1 == map2) return true;
+
+  size_t size1 = map1 ? upb_Map_Size(map1) : 0;
+  size_t size2 = map2 ? upb_Map_Size(map2) : 0;
+  if (size1 != size2) return false;
+  if (size1 == 0) return true;
+
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+  size_t iter = kUpb_Map_Begin;
+
+  upb_MessageValue key, val1;
+  while (upb_Map_Next(map1, &key, &val1, &iter)) {
+    upb_MessageValue val2;
+    if (!upb_Map_Get(map2, key, &val2)) return false;
+    if (!PyUpb_ValueEq(val1, val2, val_f)) return false;
+  }
+
+  return true;
+}
+
+static bool PyUpb_ArrayElem_IsEqual(const upb_Array* arr1,
+                                    const upb_Array* arr2, size_t i,
+                                    const upb_FieldDef* f) {
+  assert(i < upb_Array_Size(arr1));
+  assert(i < upb_Array_Size(arr2));
+  upb_MessageValue val1 = upb_Array_Get(arr1, i);
+  upb_MessageValue val2 = upb_Array_Get(arr2, i);
+  return PyUpb_ValueEq(val1, val2, f);
+}
+
+bool PyUpb_Array_IsEqual(const upb_Array* arr1, const upb_Array* arr2,
+                         const upb_FieldDef* f) {
+  assert(upb_FieldDef_IsRepeated(f) && !upb_FieldDef_IsMap(f));
+  if (arr1 == arr2) return true;
+
+  size_t n1 = arr1 ? upb_Array_Size(arr1) : 0;
+  size_t n2 = arr2 ? upb_Array_Size(arr2) : 0;
+  if (n1 != n2) return false;
+
+  // Half the length rounded down.  Important: the empty list rounds to 0.
+  size_t half = n1 / 2;
+
+  // Search from the ends-in.  We expect differences to more quickly manifest
+  // at the ends than in the middle.  If the length is odd we will miss the
+  // middle element.
+  for (size_t i = 0; i < half; i++) {
+    if (!PyUpb_ArrayElem_IsEqual(arr1, arr2, i, f)) return false;
+    if (!PyUpb_ArrayElem_IsEqual(arr1, arr2, n1 - 1 - i, f)) return false;
+  }
+
+  // For an odd-lengthed list, pick up the middle element.
+  if (n1 & 1) {
+    if (!PyUpb_ArrayElem_IsEqual(arr1, arr2, half, f)) return false;
+  }
+
+  return true;
+}
+
+bool upb_Message_IsEqual(const upb_Message* msg1, const upb_Message* msg2,
+                         const upb_MessageDef* m) {
+  if (msg1 == msg2) return true;
+  if (upb_Message_ExtensionCount(msg1) != upb_Message_ExtensionCount(msg2))
+    return false;
+
+  // Compare messages field-by-field.  This is slightly tricky, because while
+  // we can iterate over normal fields in a predictable order, the extension
+  // order is unpredictable and may be different between msg1 and msg2.
+  // So we use the following strategy:
+  //   1. Iterate over all msg1 fields (including extensions).
+  //   2. For non-extension fields, we find the corresponding field by simply
+  //      using upb_Message_Next(msg2).  If the two messages have the same set
+  //      of fields, this will yield the same field.
+  //   3. For extension fields, we have to actually search for the corresponding
+  //      field, which we do with upb_Message_GetFieldByDef(msg2, ext_f1).
+  //   4. Once iteration over msg1 is complete, we call upb_Message_Next(msg2)
+  //   one
+  //      final time to verify that we have visited all of msg2's regular fields
+  //      (we pass NULL for ext_dict so that iteration will *not* return
+  //      extensions).
+  //
+  // We don't need to visit all of msg2's extensions, because we verified up
+  // front that both messages have the same number of extensions.
+  const upb_DefPool* symtab = upb_FileDef_Pool(upb_MessageDef_File(m));
+  const upb_FieldDef *f1, *f2;
+  upb_MessageValue val1, val2;
+  size_t iter1 = kUpb_Message_Begin;
+  size_t iter2 = kUpb_Message_Begin;
+  while (upb_Message_Next(msg1, m, symtab, &f1, &val1, &iter1)) {
+    if (upb_FieldDef_IsExtension(f1)) {
+      val2 = upb_Message_GetFieldByDef(msg2, f1);
+    } else {
+      if (!upb_Message_Next(msg2, m, NULL, &f2, &val2, &iter2) || f1 != f2) {
+        return false;
+      }
+    }
+
+    if (upb_FieldDef_IsMap(f1)) {
+      if (!PyUpb_Map_IsEqual(val1.map_val, val2.map_val, f1)) return false;
+    } else if (upb_FieldDef_IsRepeated(f1)) {
+      if (!PyUpb_Array_IsEqual(val1.array_val, val2.array_val, f1)) {
+        return false;
+      }
+    } else {
+      if (!PyUpb_ValueEq(val1, val2, f1)) return false;
+    }
+  }
+
+  if (upb_Message_Next(msg2, m, NULL, &f2, &val2, &iter2)) return false;
+
+  size_t usize1, usize2;
+  const char* uf1 = upb_Message_GetUnknown(msg1, &usize1);
+  const char* uf2 = upb_Message_GetUnknown(msg2, &usize2);
+  // 100 is arbitrary, we're trying to prevent stack overflow but it's not
+  // obvious how deep we should allow here.
+  return upb_Message_UnknownFieldsAreEqual(uf1, usize1, uf2, usize2, 100) ==
+         kUpb_UnknownCompareResult_Equal;
+}
+
+#include "upb/port/undef.inc"
diff --git a/python/convert.h b/python/convert.h
new file mode 100644
index 0000000..1c594d3
--- /dev/null
+++ b/python/convert.h
@@ -0,0 +1,66 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_CONVERT_H__
+#define PYUPB_CONVERT_H__
+
+#include "protobuf.h"
+#include "upb/reflection/def.h"
+#include "upb/reflection/message.h"
+
+// Converts `val` to a Python object according to the type information in `f`.
+// Any newly-created Python objects that reference non-primitive data from `val`
+// will take a reference on `arena`; the caller must ensure that `val` belongs
+// to `arena`. If the conversion cannot be performed, returns NULL and sets a
+// Python error.
+PyObject* PyUpb_UpbToPy(upb_MessageValue val, const upb_FieldDef* f,
+                        PyObject* arena);
+
+// Converts `obj` to a upb_MessageValue `*val` according to the type information
+// in `f`. If `arena` is provided, any string data will be copied into `arena`,
+// otherwise the returned value will alias the Python-owned data (this can be
+// useful for an ephemeral upb_MessageValue).  If the conversion cannot be
+// performed, returns false.
+bool PyUpb_PyToUpb(PyObject* obj, const upb_FieldDef* f, upb_MessageValue* val,
+                   upb_Arena* arena);
+
+// Returns true if the given values (of type `f`) are equal.
+bool PyUpb_ValueEq(upb_MessageValue val1, upb_MessageValue val2,
+                   const upb_FieldDef* f);
+
+// Returns true if the two arrays (with element type `f`) are equal.
+bool PyUpb_Array_IsEqual(const upb_Array* arr1, const upb_Array* arr2,
+                         const upb_FieldDef* f);
+
+// Returns true if the given messages (of type `m`) are equal.
+bool upb_Message_IsEqual(const upb_Message* msg1, const upb_Message* msg2,
+                         const upb_MessageDef* m);
+
+#endif  // PYUPB_CONVERT_H__
diff --git a/python/descriptor.c b/python/descriptor.c
new file mode 100644
index 0000000..d1726e4
--- /dev/null
+++ b/python/descriptor.c
@@ -0,0 +1,1770 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/descriptor.h"
+
+#include "python/convert.h"
+#include "python/descriptor_containers.h"
+#include "python/descriptor_pool.h"
+#include "python/message.h"
+#include "python/protobuf.h"
+#include "upb/reflection/def.h"
+#include "upb/util/def_to_proto.h"
+
+// -----------------------------------------------------------------------------
+// DescriptorBase
+// -----------------------------------------------------------------------------
+
+// This representation is used by all concrete descriptors.
+
+typedef struct {
+  PyObject_HEAD;
+  PyObject* pool;          // We own a ref.
+  const void* def;         // Type depends on the class. Kept alive by "pool".
+  PyObject* options;       // NULL if not present or not cached.
+  PyObject* message_meta;  // We own a ref.
+} PyUpb_DescriptorBase;
+
+PyObject* PyUpb_AnyDescriptor_GetPool(PyObject* desc) {
+  PyUpb_DescriptorBase* base = (void*)desc;
+  return base->pool;
+}
+
+const void* PyUpb_AnyDescriptor_GetDef(PyObject* desc) {
+  PyUpb_DescriptorBase* base = (void*)desc;
+  return base->def;
+}
+
+static PyUpb_DescriptorBase* PyUpb_DescriptorBase_DoCreate(
+    PyUpb_DescriptorType type, const void* def, const upb_FileDef* file) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyTypeObject* type_obj = state->descriptor_types[type];
+  assert(def);
+
+  PyUpb_DescriptorBase* base = (void*)PyType_GenericAlloc(type_obj, 0);
+  base->pool = PyUpb_DescriptorPool_Get(upb_FileDef_Pool(file));
+  base->def = def;
+  base->options = NULL;
+  base->message_meta = NULL;
+
+  PyUpb_ObjCache_Add(def, &base->ob_base);
+  return base;
+}
+
+// Returns a Python object wrapping |def|, of descriptor type |type|.  If a
+// wrapper was previously created for this def, returns it, otherwise creates a
+// new wrapper.
+static PyObject* PyUpb_DescriptorBase_Get(PyUpb_DescriptorType type,
+                                          const void* def,
+                                          const upb_FileDef* file) {
+  PyUpb_DescriptorBase* base = (PyUpb_DescriptorBase*)PyUpb_ObjCache_Get(def);
+
+  if (!base) {
+    base = PyUpb_DescriptorBase_DoCreate(type, def, file);
+  }
+
+  return &base->ob_base;
+}
+
+static PyUpb_DescriptorBase* PyUpb_DescriptorBase_Check(
+    PyObject* obj, PyUpb_DescriptorType type) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyTypeObject* type_obj = state->descriptor_types[type];
+  if (!PyObject_TypeCheck(obj, type_obj)) {
+    PyErr_Format(PyExc_TypeError, "Expected object of type %S, but got %R",
+                 type_obj, obj);
+    return NULL;
+  }
+  return (PyUpb_DescriptorBase*)obj;
+}
+
+static PyObject* PyUpb_DescriptorBase_GetOptions(PyUpb_DescriptorBase* self,
+                                                 const upb_Message* opts,
+                                                 const upb_MiniTable* layout,
+                                                 const char* msg_name) {
+  if (!self->options) {
+    // Load descriptors protos if they are not loaded already. We have to do
+    // this lazily, otherwise, it would lead to circular imports.
+    PyObject* mod = PyImport_ImportModuleLevel(PYUPB_DESCRIPTOR_MODULE, NULL,
+                                               NULL, NULL, 0);
+    if (mod == NULL) return NULL;
+    Py_DECREF(mod);
+
+    // Find the correct options message.
+    PyObject* default_pool = PyUpb_DescriptorPool_GetDefaultPool();
+    const upb_DefPool* symtab = PyUpb_DescriptorPool_GetSymtab(default_pool);
+    const upb_MessageDef* m = upb_DefPool_FindMessageByName(symtab, msg_name);
+    assert(m);
+
+    // Copy the options message from C to Python using serialize+parse.
+    // We don't wrap the C object directly because there is no guarantee that
+    // the descriptor_pb2 that was loaded at runtime has the same members or
+    // layout as the C types that were compiled in.
+    size_t size;
+    PyObject* py_arena = PyUpb_Arena_New();
+    upb_Arena* arena = PyUpb_Arena_Get(py_arena);
+    char* pb;
+    // TODO: Need to correctly handle failed return codes.
+    (void)upb_Encode(opts, layout, 0, arena, &pb, &size);
+    const upb_MiniTable* opts2_layout = upb_MessageDef_MiniTable(m);
+    upb_Message* opts2 = upb_Message_New(opts2_layout, arena);
+    assert(opts2);
+    upb_DecodeStatus ds =
+        upb_Decode(pb, size, opts2, opts2_layout,
+                   upb_DefPool_ExtensionRegistry(symtab), 0, arena);
+    (void)ds;
+    assert(ds == kUpb_DecodeStatus_Ok);
+
+    self->options = PyUpb_Message_Get(opts2, m, py_arena);
+    Py_DECREF(py_arena);
+  }
+
+  Py_INCREF(self->options);
+  return self->options;
+}
+
+typedef void* PyUpb_ToProto_Func(const void* def, upb_Arena* arena);
+
+static PyObject* PyUpb_DescriptorBase_GetSerializedProto(
+    PyObject* _self, PyUpb_ToProto_Func* func, const upb_MiniTable* layout) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  upb_Arena* arena = upb_Arena_New();
+  if (!arena) PYUPB_RETURN_OOM;
+  upb_Message* proto = func(self->def, arena);
+  if (!proto) goto oom;
+  size_t size;
+  char* pb;
+  upb_EncodeStatus status = upb_Encode(proto, layout, 0, arena, &pb, &size);
+  if (status) goto oom;  // TODO non-oom errors are possible here
+  PyObject* str = PyBytes_FromStringAndSize(pb, size);
+  upb_Arena_Free(arena);
+  return str;
+
+oom:
+  upb_Arena_Free(arena);
+  PyErr_SetNone(PyExc_MemoryError);
+  return NULL;
+}
+
+static PyObject* PyUpb_DescriptorBase_CopyToProto(PyObject* _self,
+                                                  PyUpb_ToProto_Func* func,
+                                                  const upb_MiniTable* layout,
+                                                  const char* expected_type,
+                                                  PyObject* py_proto) {
+  if (!PyUpb_Message_Verify(py_proto)) return NULL;
+  const upb_MessageDef* m = PyUpb_Message_GetMsgdef(py_proto);
+  const char* type = upb_MessageDef_FullName(m);
+  if (strcmp(type, expected_type) != 0) {
+    PyErr_Format(
+        PyExc_TypeError,
+        "CopyToProto: message is of incorrect type '%s' (expected '%s'", type,
+        expected_type);
+    return NULL;
+  }
+  PyObject* serialized =
+      PyUpb_DescriptorBase_GetSerializedProto(_self, func, layout);
+  if (!serialized) return NULL;
+  PyObject* ret = PyUpb_Message_MergeFromString(py_proto, serialized);
+  Py_DECREF(serialized);
+  return ret;
+}
+
+static void PyUpb_DescriptorBase_Dealloc(PyUpb_DescriptorBase* base) {
+  PyUpb_ObjCache_Delete(base->def);
+  Py_XDECREF(base->message_meta);
+  Py_DECREF(base->pool);
+  Py_XDECREF(base->options);
+  PyUpb_Dealloc(base);
+}
+
+static int PyUpb_Descriptor_Traverse(PyUpb_DescriptorBase* base,
+                                     visitproc visit, void* arg) {
+  Py_VISIT(base->message_meta);
+  return 0;
+}
+
+static int PyUpb_Descriptor_Clear(PyUpb_DescriptorBase* base) {
+  Py_CLEAR(base->message_meta);
+  return 0;
+}
+
+#define DESCRIPTOR_BASE_SLOTS                           \
+  {Py_tp_new, (void*)&PyUpb_Forbidden_New}, {           \
+    Py_tp_dealloc, (void*)&PyUpb_DescriptorBase_Dealloc \
+  }
+
+// -----------------------------------------------------------------------------
+// Descriptor
+// -----------------------------------------------------------------------------
+
+PyObject* PyUpb_Descriptor_Get(const upb_MessageDef* m) {
+  assert(m);
+  const upb_FileDef* file = upb_MessageDef_File(m);
+  return PyUpb_DescriptorBase_Get(kPyUpb_Descriptor, m, file);
+}
+
+PyObject* PyUpb_Descriptor_GetClass(const upb_MessageDef* m) {
+  PyObject* ret = PyUpb_ObjCache_Get(upb_MessageDef_MiniTable(m));
+  if (ret) return ret;
+
+  // On demand create the clss if not exist. However, if users repeatedly
+  // create and destroy a class, it could trigger a loop. This is not an
+  // issue now, but if we see CPU waste for repeatedly create and destroy
+  // in the future, we could make PyUpb_Descriptor_Get() append the descriptor
+  // to an internal list in DescriptorPool, let the pool keep descriptors alive.
+  PyObject* py_descriptor = PyUpb_Descriptor_Get(m);
+  if (py_descriptor == NULL) return NULL;
+  const char* name = upb_MessageDef_Name(m);
+  PyObject* dict = PyDict_New();
+  if (dict == NULL) goto err;
+  int status = PyDict_SetItemString(dict, "DESCRIPTOR", py_descriptor);
+  if (status < 0) goto err;
+  ret = PyUpb_MessageMeta_DoCreateClass(py_descriptor, name, dict);
+
+err:
+  Py_XDECREF(py_descriptor);
+  Py_XDECREF(dict);
+  return ret;
+}
+
+void PyUpb_Descriptor_SetClass(PyObject* py_descriptor, PyObject* meta) {
+  PyUpb_DescriptorBase* base = (PyUpb_DescriptorBase*)py_descriptor;
+  Py_XDECREF(base->message_meta);
+  base->message_meta = meta;
+  Py_INCREF(meta);
+}
+
+// The LookupNested*() functions provide name lookup for entities nested inside
+// a message.  This uses the symtab's table, which requires that the symtab is
+// not being mutated concurrently.  We can guarantee this for Python-owned
+// symtabs, but upb cannot guarantee it in general for an arbitrary
+// `const upb_MessageDef*`.
+
+static const void* PyUpb_Descriptor_LookupNestedMessage(const upb_MessageDef* m,
+                                                        const char* name) {
+  const upb_FileDef* filedef = upb_MessageDef_File(m);
+  const upb_DefPool* symtab = upb_FileDef_Pool(filedef);
+  PyObject* qname =
+      PyUnicode_FromFormat("%s.%s", upb_MessageDef_FullName(m), name);
+  const upb_MessageDef* ret = upb_DefPool_FindMessageByName(
+      symtab, PyUnicode_AsUTF8AndSize(qname, NULL));
+  Py_DECREF(qname);
+  return ret;
+}
+
+static const void* PyUpb_Descriptor_LookupNestedEnum(const upb_MessageDef* m,
+                                                     const char* name) {
+  const upb_FileDef* filedef = upb_MessageDef_File(m);
+  const upb_DefPool* symtab = upb_FileDef_Pool(filedef);
+  PyObject* qname =
+      PyUnicode_FromFormat("%s.%s", upb_MessageDef_FullName(m), name);
+  const upb_EnumDef* ret =
+      upb_DefPool_FindEnumByName(symtab, PyUnicode_AsUTF8AndSize(qname, NULL));
+  Py_DECREF(qname);
+  return ret;
+}
+
+static const void* PyUpb_Descriptor_LookupNestedExtension(
+    const upb_MessageDef* m, const char* name) {
+  const upb_FileDef* filedef = upb_MessageDef_File(m);
+  const upb_DefPool* symtab = upb_FileDef_Pool(filedef);
+  PyObject* qname =
+      PyUnicode_FromFormat("%s.%s", upb_MessageDef_FullName(m), name);
+  const upb_FieldDef* ret = upb_DefPool_FindExtensionByName(
+      symtab, PyUnicode_AsUTF8AndSize(qname, NULL));
+  Py_DECREF(qname);
+  return ret;
+}
+
+static PyObject* PyUpb_Descriptor_GetExtensionRanges(PyObject* _self,
+                                                     void* closure) {
+  PyUpb_DescriptorBase* self = (PyUpb_DescriptorBase*)_self;
+  int n = upb_MessageDef_ExtensionRangeCount(self->def);
+  PyObject* range_list = PyList_New(n);
+
+  for (int i = 0; i < n; i++) {
+    const upb_ExtensionRange* range =
+        upb_MessageDef_ExtensionRange(self->def, i);
+    PyObject* start = PyLong_FromLong(upb_ExtensionRange_Start(range));
+    PyObject* end = PyLong_FromLong(upb_ExtensionRange_End(range));
+    PyList_SetItem(range_list, i, PyTuple_Pack(2, start, end));
+  }
+
+  return range_list;
+}
+
+static PyObject* PyUpb_Descriptor_GetExtensions(PyObject* _self,
+                                                void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_MessageDef_NestedExtensionCount,
+      (void*)&upb_MessageDef_NestedExtension,
+      (void*)&PyUpb_FieldDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetExtensionsByName(PyObject* _self,
+                                                      void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_MessageDef_NestedExtensionCount,
+          (void*)&upb_MessageDef_NestedExtension,
+          (void*)&PyUpb_FieldDescriptor_Get,
+      },
+      (void*)&PyUpb_Descriptor_LookupNestedExtension,
+      (void*)&upb_FieldDef_Name,
+  };
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetEnumTypes(PyObject* _self, void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_MessageDef_NestedEnumCount,
+      (void*)&upb_MessageDef_NestedEnum,
+      (void*)&PyUpb_EnumDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetOneofs(PyObject* _self, void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_MessageDef_OneofCount,
+      (void*)&upb_MessageDef_Oneof,
+      (void*)&PyUpb_OneofDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetOptions(PyObject* _self, PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_MessageDef_Options(self->def), &google_protobuf_MessageOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".MessageOptions");
+}
+
+static PyObject* PyUpb_Descriptor_CopyToProto(PyObject* _self,
+                                              PyObject* py_proto) {
+  return PyUpb_DescriptorBase_CopyToProto(
+      _self, (PyUpb_ToProto_Func*)&upb_MessageDef_ToProto,
+      &google_protobuf_DescriptorProto_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".DescriptorProto", py_proto);
+}
+
+static PyObject* PyUpb_Descriptor_EnumValueName(PyObject* _self,
+                                                PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  const char* enum_name;
+  int number;
+  if (!PyArg_ParseTuple(args, "si", &enum_name, &number)) return NULL;
+  const upb_EnumDef* e =
+      PyUpb_Descriptor_LookupNestedEnum(self->def, enum_name);
+  if (!e) {
+    PyErr_SetString(PyExc_KeyError, enum_name);
+    return NULL;
+  }
+  const upb_EnumValueDef* ev = upb_EnumDef_FindValueByNumber(e, number);
+  if (!ev) {
+    PyErr_Format(PyExc_KeyError, "%d", number);
+    return NULL;
+  }
+  return PyUnicode_FromString(upb_EnumValueDef_Name(ev));
+}
+
+static PyObject* PyUpb_Descriptor_GetFieldsByName(PyObject* _self,
+                                                  void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_MessageDef_FieldCount,
+          (void*)&upb_MessageDef_Field,
+          (void*)&PyUpb_FieldDescriptor_Get,
+      },
+      (void*)&upb_MessageDef_FindFieldByName,
+      (void*)&upb_FieldDef_Name,
+  };
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetFieldsByCamelCaseName(PyObject* _self,
+                                                           void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_MessageDef_FieldCount,
+          (void*)&upb_MessageDef_Field,
+          (void*)&PyUpb_FieldDescriptor_Get,
+      },
+      (void*)&upb_MessageDef_FindByJsonName,
+      (void*)&upb_FieldDef_JsonName,
+  };
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetFieldsByNumber(PyObject* _self,
+                                                    void* closure) {
+  static PyUpb_ByNumberMap_Funcs funcs = {
+      {
+          (void*)&upb_MessageDef_FieldCount,
+          (void*)&upb_MessageDef_Field,
+          (void*)&PyUpb_FieldDescriptor_Get,
+      },
+      (void*)&upb_MessageDef_FindFieldByNumber,
+      (void*)&upb_FieldDef_Number,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNumberMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetNestedTypes(PyObject* _self,
+                                                 void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_MessageDef_NestedMessageCount,
+      (void*)&upb_MessageDef_NestedMessage,
+      (void*)&PyUpb_Descriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetNestedTypesByName(PyObject* _self,
+                                                       void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_MessageDef_NestedMessageCount,
+          (void*)&upb_MessageDef_NestedMessage,
+          (void*)&PyUpb_Descriptor_Get,
+      },
+      (void*)&PyUpb_Descriptor_LookupNestedMessage,
+      (void*)&upb_MessageDef_Name,
+  };
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetContainingType(PyObject* _self,
+                                                    void* closure) {
+  // upb does not natively store the lexical parent of a message type, but we
+  // can derive it with some string manipulation and a lookup.
+  PyUpb_DescriptorBase* self = (void*)_self;
+  const upb_MessageDef* m = self->def;
+  const upb_FileDef* file = upb_MessageDef_File(m);
+  const upb_DefPool* symtab = upb_FileDef_Pool(file);
+  const char* full_name = upb_MessageDef_FullName(m);
+  const char* last_dot = strrchr(full_name, '.');
+  if (!last_dot) Py_RETURN_NONE;
+  const upb_MessageDef* parent = upb_DefPool_FindMessageByNameWithSize(
+      symtab, full_name, last_dot - full_name);
+  if (!parent) Py_RETURN_NONE;
+  return PyUpb_Descriptor_Get(parent);
+}
+
+static PyObject* PyUpb_Descriptor_GetEnumTypesByName(PyObject* _self,
+                                                     void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_MessageDef_NestedEnumCount,
+          (void*)&upb_MessageDef_NestedEnum,
+          (void*)&PyUpb_EnumDescriptor_Get,
+      },
+      (void*)&PyUpb_Descriptor_LookupNestedEnum,
+      (void*)&upb_EnumDef_Name,
+  };
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetIsExtendable(PyObject* _self,
+                                                  void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  if (upb_MessageDef_ExtensionRangeCount(self->def) > 0) {
+    Py_RETURN_TRUE;
+  } else {
+    Py_RETURN_FALSE;
+  }
+}
+
+static PyObject* PyUpb_Descriptor_GetFullName(PyObject* self, void* closure) {
+  const upb_MessageDef* msgdef = PyUpb_Descriptor_GetDef(self);
+  return PyUnicode_FromString(upb_MessageDef_FullName(msgdef));
+}
+
+static PyObject* PyUpb_Descriptor_GetConcreteClass(PyObject* self,
+                                                   void* closure) {
+  const upb_MessageDef* msgdef = PyUpb_Descriptor_GetDef(self);
+  return PyUpb_ObjCache_Get(upb_MessageDef_MiniTable(msgdef));
+}
+
+static PyObject* PyUpb_Descriptor_GetFile(PyObject* self, void* closure) {
+  const upb_MessageDef* msgdef = PyUpb_Descriptor_GetDef(self);
+  return PyUpb_FileDescriptor_Get(upb_MessageDef_File(msgdef));
+}
+
+static PyObject* PyUpb_Descriptor_GetFields(PyObject* _self, void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_MessageDef_FieldCount,
+      (void*)&upb_MessageDef_Field,
+      (void*)&PyUpb_FieldDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetHasOptions(PyObject* _self,
+                                                void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyBool_FromLong(upb_MessageDef_HasOptions(self->def));
+}
+
+static PyObject* PyUpb_Descriptor_GetName(PyObject* self, void* closure) {
+  const upb_MessageDef* msgdef = PyUpb_Descriptor_GetDef(self);
+  return PyUnicode_FromString(upb_MessageDef_Name(msgdef));
+}
+
+static PyObject* PyUpb_Descriptor_GetEnumValuesByName(PyObject* _self,
+                                                      void* closure) {
+  // upb does not natively store any table containing all nested values.
+  // Consider:
+  //     message M {
+  //       enum E1 {
+  //         A = 0;
+  //         B = 1;
+  //       }
+  //       enum E2 {
+  //         C = 0;
+  //         D = 1;
+  //       }
+  //     }
+  //
+  // In this case, upb stores tables for E1 and E2, but it does not store a
+  // table for M that combines them (it is rarely needed and costs precious
+  // space and time to build).
+  //
+  // To work around this, we build an actual Python dict whenever a user
+  // actually asks for this.
+  PyUpb_DescriptorBase* self = (void*)_self;
+  PyObject* ret = PyDict_New();
+  if (!ret) return NULL;
+  int enum_count = upb_MessageDef_NestedEnumCount(self->def);
+  for (int i = 0; i < enum_count; i++) {
+    const upb_EnumDef* e = upb_MessageDef_NestedEnum(self->def, i);
+    int value_count = upb_EnumDef_ValueCount(e);
+    for (int j = 0; j < value_count; j++) {
+      // Collisions should be impossible here, as uniqueness is checked by
+      // protoc (this is an invariant of the protobuf language).  However this
+      // uniqueness constraint is not currently checked by upb/def.c at load
+      // time, so if the user supplies a manually-constructed descriptor that
+      // does not respect this constraint, a collision could be possible and the
+      // last-defined enumerator would win.  This could be seen as an argument
+      // for having upb actually build the table at load time, thus checking the
+      // constraint proactively, but upb is always checking a subset of the full
+      // validation performed by C++, and we have to pick and choose the biggest
+      // bang for the buck.
+      const upb_EnumValueDef* ev = upb_EnumDef_Value(e, j);
+      const char* name = upb_EnumValueDef_Name(ev);
+      PyObject* val = PyUpb_EnumValueDescriptor_Get(ev);
+      if (!val || PyDict_SetItemString(ret, name, val) < 0) {
+        Py_XDECREF(val);
+        Py_DECREF(ret);
+        return NULL;
+      }
+      Py_DECREF(val);
+    }
+  }
+  return ret;
+}
+
+static PyObject* PyUpb_Descriptor_GetOneofsByName(PyObject* _self,
+                                                  void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_MessageDef_OneofCount,
+          (void*)&upb_MessageDef_Oneof,
+          (void*)&PyUpb_OneofDescriptor_Get,
+      },
+      (void*)&upb_MessageDef_FindOneofByName,
+      (void*)&upb_OneofDef_Name,
+  };
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_Descriptor_GetSyntax(PyObject* self, void* closure) {
+  const upb_MessageDef* msgdef = PyUpb_Descriptor_GetDef(self);
+  const char* syntax =
+      upb_MessageDef_Syntax(msgdef) == kUpb_Syntax_Proto2 ? "proto2" : "proto3";
+  return PyUnicode_InternFromString(syntax);
+}
+
+static PyGetSetDef PyUpb_Descriptor_Getters[] = {
+    {"name", PyUpb_Descriptor_GetName, NULL, "Last name"},
+    {"full_name", PyUpb_Descriptor_GetFullName, NULL, "Full name"},
+    {"_concrete_class", PyUpb_Descriptor_GetConcreteClass, NULL,
+     "concrete class"},
+    {"file", PyUpb_Descriptor_GetFile, NULL, "File descriptor"},
+    {"fields", PyUpb_Descriptor_GetFields, NULL, "Fields sequence"},
+    {"fields_by_name", PyUpb_Descriptor_GetFieldsByName, NULL,
+     "Fields by name"},
+    {"fields_by_camelcase_name", PyUpb_Descriptor_GetFieldsByCamelCaseName,
+     NULL, "Fields by camelCase name"},
+    {"fields_by_number", PyUpb_Descriptor_GetFieldsByNumber, NULL,
+     "Fields by number"},
+    {"nested_types", PyUpb_Descriptor_GetNestedTypes, NULL,
+     "Nested types sequence"},
+    {"nested_types_by_name", PyUpb_Descriptor_GetNestedTypesByName, NULL,
+     "Nested types by name"},
+    {"extensions", PyUpb_Descriptor_GetExtensions, NULL, "Extensions Sequence"},
+    {"extensions_by_name", PyUpb_Descriptor_GetExtensionsByName, NULL,
+     "Extensions by name"},
+    {"extension_ranges", PyUpb_Descriptor_GetExtensionRanges, NULL,
+     "Extension ranges"},
+    {"enum_types", PyUpb_Descriptor_GetEnumTypes, NULL, "Enum sequence"},
+    {"enum_types_by_name", PyUpb_Descriptor_GetEnumTypesByName, NULL,
+     "Enum types by name"},
+    {"enum_values_by_name", PyUpb_Descriptor_GetEnumValuesByName, NULL,
+     "Enum values by name"},
+    {"oneofs_by_name", PyUpb_Descriptor_GetOneofsByName, NULL,
+     "Oneofs by name"},
+    {"oneofs", PyUpb_Descriptor_GetOneofs, NULL, "Oneofs Sequence"},
+    {"containing_type", PyUpb_Descriptor_GetContainingType, NULL,
+     "Containing type"},
+    {"is_extendable", PyUpb_Descriptor_GetIsExtendable, NULL},
+    {"has_options", PyUpb_Descriptor_GetHasOptions, NULL, "Has Options"},
+    // begin:github_only
+    {"syntax", &PyUpb_Descriptor_GetSyntax, NULL, "Syntax"},
+    // end:github_only
+    // begin:google_only
+//     // TODO Use this to open-source syntax deprecation.
+//     {"deprecated_syntax", &PyUpb_Descriptor_GetSyntax, NULL, "Syntax"},
+    // end:google_only
+    {NULL}};
+
+static PyMethodDef PyUpb_Descriptor_Methods[] = {
+    {"GetOptions", PyUpb_Descriptor_GetOptions, METH_NOARGS},
+    {"CopyToProto", PyUpb_Descriptor_CopyToProto, METH_O},
+    {"EnumValueName", PyUpb_Descriptor_EnumValueName, METH_VARARGS},
+    {NULL}};
+
+static PyType_Slot PyUpb_Descriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_Descriptor_Methods},
+    {Py_tp_getset, PyUpb_Descriptor_Getters},
+    {Py_tp_traverse, PyUpb_Descriptor_Traverse},
+    {Py_tp_clear, PyUpb_Descriptor_Clear},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_Descriptor_Spec = {
+    PYUPB_MODULE_NAME ".Descriptor",  // tp_name
+    sizeof(PyUpb_DescriptorBase),     // tp_basicsize
+    0,                                // tp_itemsize
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, // tp_flags
+    PyUpb_Descriptor_Slots,
+};
+
+const upb_MessageDef* PyUpb_Descriptor_GetDef(PyObject* _self) {
+  PyUpb_DescriptorBase* self =
+      PyUpb_DescriptorBase_Check(_self, kPyUpb_Descriptor);
+  return self ? self->def : NULL;
+}
+
+// -----------------------------------------------------------------------------
+// EnumDescriptor
+// -----------------------------------------------------------------------------
+
+PyObject* PyUpb_EnumDescriptor_Get(const upb_EnumDef* enumdef) {
+  const upb_FileDef* file = upb_EnumDef_File(enumdef);
+  return PyUpb_DescriptorBase_Get(kPyUpb_EnumDescriptor, enumdef, file);
+}
+
+const upb_EnumDef* PyUpb_EnumDescriptor_GetDef(PyObject* _self) {
+  PyUpb_DescriptorBase* self =
+      PyUpb_DescriptorBase_Check(_self, kPyUpb_EnumDescriptor);
+  return self ? self->def : NULL;
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetFullName(PyObject* self,
+                                                  void* closure) {
+  const upb_EnumDef* enumdef = PyUpb_EnumDescriptor_GetDef(self);
+  return PyUnicode_FromString(upb_EnumDef_FullName(enumdef));
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetName(PyObject* self, void* closure) {
+  const upb_EnumDef* enumdef = PyUpb_EnumDescriptor_GetDef(self);
+  return PyUnicode_FromString(upb_EnumDef_Name(enumdef));
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetFile(PyObject* self, void* closure) {
+  const upb_EnumDef* enumdef = PyUpb_EnumDescriptor_GetDef(self);
+  return PyUpb_FileDescriptor_Get(upb_EnumDef_File(enumdef));
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetValues(PyObject* _self,
+                                                void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_EnumDef_ValueCount,
+      (void*)&upb_EnumDef_Value,
+      (void*)&PyUpb_EnumValueDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetValuesByName(PyObject* _self,
+                                                      void* closure) {
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_EnumDef_ValueCount,
+          (void*)&upb_EnumDef_Value,
+          (void*)&PyUpb_EnumValueDescriptor_Get,
+      },
+      (void*)&upb_EnumDef_FindValueByName,
+      (void*)&upb_EnumValueDef_Name,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetValuesByNumber(PyObject* _self,
+                                                        void* closure) {
+  static PyUpb_ByNumberMap_Funcs funcs = {
+      {
+          (void*)&upb_EnumDef_ValueCount,
+          (void*)&upb_EnumDef_Value,
+          (void*)&PyUpb_EnumValueDescriptor_Get,
+      },
+      (void*)&upb_EnumDef_FindValueByNumber,
+      (void*)&upb_EnumValueDef_Number,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNumberMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetContainingType(PyObject* _self,
+                                                        void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  const upb_MessageDef* m = upb_EnumDef_ContainingType(self->def);
+  if (!m) Py_RETURN_NONE;
+  return PyUpb_Descriptor_Get(m);
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetHasOptions(PyObject* _self,
+                                                    void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyBool_FromLong(upb_EnumDef_HasOptions(self->def));
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetIsClosed(PyObject* _self,
+                                                  void* closure) {
+  const upb_EnumDef* enumdef = PyUpb_EnumDescriptor_GetDef(_self);
+  return PyBool_FromLong(upb_EnumDef_IsClosed(enumdef));
+}
+
+static PyObject* PyUpb_EnumDescriptor_GetOptions(PyObject* _self,
+                                                 PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_EnumDef_Options(self->def), &google_protobuf_EnumOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".EnumOptions");
+}
+
+static PyObject* PyUpb_EnumDescriptor_CopyToProto(PyObject* _self,
+                                                  PyObject* py_proto) {
+  return PyUpb_DescriptorBase_CopyToProto(
+      _self, (PyUpb_ToProto_Func*)&upb_EnumDef_ToProto,
+      &google_protobuf_EnumDescriptorProto_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".EnumDescriptorProto", py_proto);
+}
+
+static PyGetSetDef PyUpb_EnumDescriptor_Getters[] = {
+    {"full_name", PyUpb_EnumDescriptor_GetFullName, NULL, "Full name"},
+    {"name", PyUpb_EnumDescriptor_GetName, NULL, "last name"},
+    {"file", PyUpb_EnumDescriptor_GetFile, NULL, "File descriptor"},
+    {"values", PyUpb_EnumDescriptor_GetValues, NULL, "values"},
+    {"values_by_name", PyUpb_EnumDescriptor_GetValuesByName, NULL,
+     "Enum values by name"},
+    {"values_by_number", PyUpb_EnumDescriptor_GetValuesByNumber, NULL,
+     "Enum values by number"},
+    {"containing_type", PyUpb_EnumDescriptor_GetContainingType, NULL,
+     "Containing type"},
+    {"has_options", PyUpb_EnumDescriptor_GetHasOptions, NULL, "Has Options"},
+    {"is_closed", PyUpb_EnumDescriptor_GetIsClosed, NULL,
+     "Checks if the enum is closed"},
+    {NULL}};
+
+static PyMethodDef PyUpb_EnumDescriptor_Methods[] = {
+    {"GetOptions", PyUpb_EnumDescriptor_GetOptions, METH_NOARGS},
+    {"CopyToProto", PyUpb_EnumDescriptor_CopyToProto, METH_O},
+    {NULL}};
+
+static PyType_Slot PyUpb_EnumDescriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_EnumDescriptor_Methods},
+    {Py_tp_getset, PyUpb_EnumDescriptor_Getters},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_EnumDescriptor_Spec = {
+    PYUPB_MODULE_NAME ".EnumDescriptor",  // tp_name
+    sizeof(PyUpb_DescriptorBase),         // tp_basicsize
+    0,                                    // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                   // tp_flags
+    PyUpb_EnumDescriptor_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// EnumValueDescriptor
+// -----------------------------------------------------------------------------
+
+PyObject* PyUpb_EnumValueDescriptor_Get(const upb_EnumValueDef* ev) {
+  const upb_FileDef* file = upb_EnumDef_File(upb_EnumValueDef_Enum(ev));
+  return PyUpb_DescriptorBase_Get(kPyUpb_EnumValueDescriptor, ev, file);
+}
+
+static PyObject* PyUpb_EnumValueDescriptor_GetName(PyObject* self,
+                                                   void* closure) {
+  PyUpb_DescriptorBase* base = (PyUpb_DescriptorBase*)self;
+  return PyUnicode_FromString(upb_EnumValueDef_Name(base->def));
+}
+
+static PyObject* PyUpb_EnumValueDescriptor_GetNumber(PyObject* self,
+                                                     void* closure) {
+  PyUpb_DescriptorBase* base = (PyUpb_DescriptorBase*)self;
+  return PyLong_FromLong(upb_EnumValueDef_Number(base->def));
+}
+
+static PyObject* PyUpb_EnumValueDescriptor_GetIndex(PyObject* self,
+                                                    void* closure) {
+  PyUpb_DescriptorBase* base = (PyUpb_DescriptorBase*)self;
+  return PyLong_FromLong(upb_EnumValueDef_Index(base->def));
+}
+
+static PyObject* PyUpb_EnumValueDescriptor_GetType(PyObject* self,
+                                                   void* closure) {
+  PyUpb_DescriptorBase* base = (PyUpb_DescriptorBase*)self;
+  return PyUpb_EnumDescriptor_Get(upb_EnumValueDef_Enum(base->def));
+}
+
+static PyObject* PyUpb_EnumValueDescriptor_GetHasOptions(PyObject* _self,
+                                                         void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyBool_FromLong(upb_EnumValueDef_HasOptions(self->def));
+}
+
+static PyObject* PyUpb_EnumValueDescriptor_GetOptions(PyObject* _self,
+                                                      PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_EnumValueDef_Options(self->def),
+      &google_protobuf_EnumValueOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".EnumValueOptions");
+}
+
+static PyGetSetDef PyUpb_EnumValueDescriptor_Getters[] = {
+    {"name", PyUpb_EnumValueDescriptor_GetName, NULL, "name"},
+    {"number", PyUpb_EnumValueDescriptor_GetNumber, NULL, "number"},
+    {"index", PyUpb_EnumValueDescriptor_GetIndex, NULL, "index"},
+    {"type", PyUpb_EnumValueDescriptor_GetType, NULL, "index"},
+    {"has_options", PyUpb_EnumValueDescriptor_GetHasOptions, NULL,
+     "Has Options"},
+    {NULL}};
+
+static PyMethodDef PyUpb_EnumValueDescriptor_Methods[] = {
+    {
+        "GetOptions",
+        PyUpb_EnumValueDescriptor_GetOptions,
+        METH_NOARGS,
+    },
+    {NULL}};
+
+static PyType_Slot PyUpb_EnumValueDescriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_EnumValueDescriptor_Methods},
+    {Py_tp_getset, PyUpb_EnumValueDescriptor_Getters},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_EnumValueDescriptor_Spec = {
+    PYUPB_MODULE_NAME ".EnumValueDescriptor",  // tp_name
+    sizeof(PyUpb_DescriptorBase),              // tp_basicsize
+    0,                                         // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                        // tp_flags
+    PyUpb_EnumValueDescriptor_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// FieldDescriptor
+// -----------------------------------------------------------------------------
+
+const upb_FieldDef* PyUpb_FieldDescriptor_GetDef(PyObject* _self) {
+  PyUpb_DescriptorBase* self =
+      PyUpb_DescriptorBase_Check(_self, kPyUpb_FieldDescriptor);
+  return self ? self->def : NULL;
+}
+
+PyObject* PyUpb_FieldDescriptor_Get(const upb_FieldDef* field) {
+  const upb_FileDef* file = upb_FieldDef_File(field);
+  return PyUpb_DescriptorBase_Get(kPyUpb_FieldDescriptor, field, file);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetFullName(PyUpb_DescriptorBase* self,
+                                                   void* closure) {
+  return PyUnicode_FromString(upb_FieldDef_FullName(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetName(PyUpb_DescriptorBase* self,
+                                               void* closure) {
+  return PyUnicode_FromString(upb_FieldDef_Name(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetCamelCaseName(
+    PyUpb_DescriptorBase* self, void* closure) {
+  // TODO: Ok to use jsonname here?
+  return PyUnicode_FromString(upb_FieldDef_JsonName(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetJsonName(PyUpb_DescriptorBase* self,
+                                                   void* closure) {
+  return PyUnicode_FromString(upb_FieldDef_JsonName(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetFile(PyUpb_DescriptorBase* self,
+                                               void* closure) {
+  const upb_FileDef* file = upb_FieldDef_File(self->def);
+  if (!file) Py_RETURN_NONE;
+  return PyUpb_FileDescriptor_Get(file);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetType(PyUpb_DescriptorBase* self,
+                                               void* closure) {
+  return PyLong_FromLong(upb_FieldDef_Type(self->def));
+}
+
+// Enum values copied from descriptor.h in C++.
+enum CppType {
+  CPPTYPE_INT32 = 1,     // TYPE_INT32, TYPE_SINT32, TYPE_SFIXED32
+  CPPTYPE_INT64 = 2,     // TYPE_INT64, TYPE_SINT64, TYPE_SFIXED64
+  CPPTYPE_UINT32 = 3,    // TYPE_UINT32, TYPE_FIXED32
+  CPPTYPE_UINT64 = 4,    // TYPE_UINT64, TYPE_FIXED64
+  CPPTYPE_DOUBLE = 5,    // TYPE_DOUBLE
+  CPPTYPE_FLOAT = 6,     // TYPE_FLOAT
+  CPPTYPE_BOOL = 7,      // TYPE_BOOL
+  CPPTYPE_ENUM = 8,      // TYPE_ENUM
+  CPPTYPE_STRING = 9,    // TYPE_STRING, TYPE_BYTES
+  CPPTYPE_MESSAGE = 10,  // TYPE_MESSAGE, TYPE_GROUP
+};
+
+static PyObject* PyUpb_FieldDescriptor_GetCppType(PyUpb_DescriptorBase* self,
+                                                  void* closure) {
+  static const uint8_t cpp_types[] = {
+      -1,
+      [kUpb_CType_Int32] = CPPTYPE_INT32,
+      [kUpb_CType_Int64] = CPPTYPE_INT64,
+      [kUpb_CType_UInt32] = CPPTYPE_UINT32,
+      [kUpb_CType_UInt64] = CPPTYPE_UINT64,
+      [kUpb_CType_Double] = CPPTYPE_DOUBLE,
+      [kUpb_CType_Float] = CPPTYPE_FLOAT,
+      [kUpb_CType_Bool] = CPPTYPE_BOOL,
+      [kUpb_CType_Enum] = CPPTYPE_ENUM,
+      [kUpb_CType_String] = CPPTYPE_STRING,
+      [kUpb_CType_Bytes] = CPPTYPE_STRING,
+      [kUpb_CType_Message] = CPPTYPE_MESSAGE,
+  };
+  return PyLong_FromLong(cpp_types[upb_FieldDef_CType(self->def)]);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetLabel(PyUpb_DescriptorBase* self,
+                                                void* closure) {
+  return PyLong_FromLong(upb_FieldDef_Label(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetIsExtension(
+    PyUpb_DescriptorBase* self, void* closure) {
+  return PyBool_FromLong(upb_FieldDef_IsExtension(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetNumber(PyUpb_DescriptorBase* self,
+                                                 void* closure) {
+  return PyLong_FromLong(upb_FieldDef_Number(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetIndex(PyUpb_DescriptorBase* self,
+                                                void* closure) {
+  return PyLong_FromLong(upb_FieldDef_Index(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetMessageType(
+    PyUpb_DescriptorBase* self, void* closure) {
+  const upb_MessageDef* subdef = upb_FieldDef_MessageSubDef(self->def);
+  if (!subdef) Py_RETURN_NONE;
+  return PyUpb_Descriptor_Get(subdef);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetEnumType(PyUpb_DescriptorBase* self,
+                                                   void* closure) {
+  const upb_EnumDef* enumdef = upb_FieldDef_EnumSubDef(self->def);
+  if (!enumdef) Py_RETURN_NONE;
+  return PyUpb_EnumDescriptor_Get(enumdef);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetContainingType(
+    PyUpb_DescriptorBase* self, void* closure) {
+  const upb_MessageDef* m = upb_FieldDef_ContainingType(self->def);
+  if (!m) Py_RETURN_NONE;
+  return PyUpb_Descriptor_Get(m);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetExtensionScope(
+    PyUpb_DescriptorBase* self, void* closure) {
+  const upb_MessageDef* m = upb_FieldDef_ExtensionScope(self->def);
+  if (!m) Py_RETURN_NONE;
+  return PyUpb_Descriptor_Get(m);
+}
+
+static PyObject* PyUpb_FieldDescriptor_HasDefaultValue(
+    PyUpb_DescriptorBase* self, void* closure) {
+  return PyBool_FromLong(upb_FieldDef_HasDefault(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetDefaultValue(
+    PyUpb_DescriptorBase* self, void* closure) {
+  const upb_FieldDef* f = self->def;
+  if (upb_FieldDef_IsRepeated(f)) return PyList_New(0);
+  if (upb_FieldDef_IsSubMessage(f)) Py_RETURN_NONE;
+  return PyUpb_UpbToPy(upb_FieldDef_Default(self->def), self->def, NULL);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetContainingOneof(
+    PyUpb_DescriptorBase* self, void* closure) {
+  const upb_OneofDef* oneof = upb_FieldDef_ContainingOneof(self->def);
+  if (!oneof) Py_RETURN_NONE;
+  return PyUpb_OneofDescriptor_Get(oneof);
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetHasOptions(
+    PyUpb_DescriptorBase* _self, void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyBool_FromLong(upb_FieldDef_HasOptions(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetHasPresence(
+    PyUpb_DescriptorBase* _self, void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyBool_FromLong(upb_FieldDef_HasPresence(self->def));
+}
+
+static PyObject* PyUpb_FieldDescriptor_GetOptions(PyObject* _self,
+                                                  PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_FieldDef_Options(self->def), &google_protobuf_FieldOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".FieldOptions");
+}
+
+static PyGetSetDef PyUpb_FieldDescriptor_Getters[] = {
+    {"full_name", (getter)PyUpb_FieldDescriptor_GetFullName, NULL, "Full name"},
+    {"name", (getter)PyUpb_FieldDescriptor_GetName, NULL, "Unqualified name"},
+    {"camelcase_name", (getter)PyUpb_FieldDescriptor_GetCamelCaseName, NULL,
+     "CamelCase name"},
+    {"json_name", (getter)PyUpb_FieldDescriptor_GetJsonName, NULL, "Json name"},
+    {"file", (getter)PyUpb_FieldDescriptor_GetFile, NULL, "File Descriptor"},
+    {"type", (getter)PyUpb_FieldDescriptor_GetType, NULL, "Type"},
+    {"cpp_type", (getter)PyUpb_FieldDescriptor_GetCppType, NULL, "C++ Type"},
+    {"label", (getter)PyUpb_FieldDescriptor_GetLabel, NULL, "Label"},
+    {"number", (getter)PyUpb_FieldDescriptor_GetNumber, NULL, "Number"},
+    {"index", (getter)PyUpb_FieldDescriptor_GetIndex, NULL, "Index"},
+    {"default_value", (getter)PyUpb_FieldDescriptor_GetDefaultValue, NULL,
+     "Default Value"},
+    {"has_default_value", (getter)PyUpb_FieldDescriptor_HasDefaultValue},
+    {"is_extension", (getter)PyUpb_FieldDescriptor_GetIsExtension, NULL, "ID"},
+    // TODO
+    //{ "id", (getter)GetID, NULL, "ID"},
+    {"message_type", (getter)PyUpb_FieldDescriptor_GetMessageType, NULL,
+     "Message type"},
+    {"enum_type", (getter)PyUpb_FieldDescriptor_GetEnumType, NULL, "Enum type"},
+    {"containing_type", (getter)PyUpb_FieldDescriptor_GetContainingType, NULL,
+     "Containing type"},
+    {"extension_scope", (getter)PyUpb_FieldDescriptor_GetExtensionScope, NULL,
+     "Extension scope"},
+    {"containing_oneof", (getter)PyUpb_FieldDescriptor_GetContainingOneof, NULL,
+     "Containing oneof"},
+    {"has_options", (getter)PyUpb_FieldDescriptor_GetHasOptions, NULL,
+     "Has Options"},
+    {"has_presence", (getter)PyUpb_FieldDescriptor_GetHasPresence, NULL,
+     "Has Presence"},
+    // TODO
+    //{ "_options",
+    //(getter)NULL, (setter)SetOptions, "Options"}, { "_serialized_options",
+    //(getter)NULL, (setter)SetSerializedOptions, "Serialized Options"},
+    {NULL}};
+
+static PyMethodDef PyUpb_FieldDescriptor_Methods[] = {
+    {
+        "GetOptions",
+        PyUpb_FieldDescriptor_GetOptions,
+        METH_NOARGS,
+    },
+    {NULL}};
+
+static PyType_Slot PyUpb_FieldDescriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_FieldDescriptor_Methods},
+    {Py_tp_getset, PyUpb_FieldDescriptor_Getters},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_FieldDescriptor_Spec = {
+    PYUPB_MODULE_NAME ".FieldDescriptor",
+    sizeof(PyUpb_DescriptorBase),
+    0,  // tp_itemsize
+    Py_TPFLAGS_DEFAULT,
+    PyUpb_FieldDescriptor_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// FileDescriptor
+// -----------------------------------------------------------------------------
+
+PyObject* PyUpb_FileDescriptor_Get(const upb_FileDef* file) {
+  return PyUpb_DescriptorBase_Get(kPyUpb_FileDescriptor, file, file);
+}
+
+// These are not provided on upb_FileDef because they use the underlying
+// symtab's hash table. This works for Python because everything happens under
+// the GIL, but in general the caller has to guarantee that the symtab is not
+// being mutated concurrently.
+typedef const void* PyUpb_FileDescriptor_LookupFunc(const upb_DefPool*,
+                                                    const char*);
+
+static const void* PyUpb_FileDescriptor_NestedLookup(
+    const upb_FileDef* filedef, const char* name,
+    PyUpb_FileDescriptor_LookupFunc* func) {
+  const upb_DefPool* symtab = upb_FileDef_Pool(filedef);
+  const char* package = upb_FileDef_Package(filedef);
+  if (strlen(package)) {
+    PyObject* qname = PyUnicode_FromFormat("%s.%s", package, name);
+    const void* ret = func(symtab, PyUnicode_AsUTF8AndSize(qname, NULL));
+    Py_DECREF(qname);
+    return ret;
+  } else {
+    return func(symtab, name);
+  }
+}
+
+static const void* PyUpb_FileDescriptor_LookupMessage(
+    const upb_FileDef* filedef, const char* name) {
+  return PyUpb_FileDescriptor_NestedLookup(
+      filedef, name, (void*)&upb_DefPool_FindMessageByName);
+}
+
+static const void* PyUpb_FileDescriptor_LookupEnum(const upb_FileDef* filedef,
+                                                   const char* name) {
+  return PyUpb_FileDescriptor_NestedLookup(filedef, name,
+                                           (void*)&upb_DefPool_FindEnumByName);
+}
+
+static const void* PyUpb_FileDescriptor_LookupExtension(
+    const upb_FileDef* filedef, const char* name) {
+  return PyUpb_FileDescriptor_NestedLookup(
+      filedef, name, (void*)&upb_DefPool_FindExtensionByName);
+}
+
+static const void* PyUpb_FileDescriptor_LookupService(
+    const upb_FileDef* filedef, const char* name) {
+  return PyUpb_FileDescriptor_NestedLookup(
+      filedef, name, (void*)&upb_DefPool_FindServiceByName);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetName(PyUpb_DescriptorBase* self,
+                                              void* closure) {
+  return PyUnicode_FromString(upb_FileDef_Name(self->def));
+}
+
+static PyObject* PyUpb_FileDescriptor_GetPool(PyObject* _self, void* closure) {
+  PyUpb_DescriptorBase* self = (PyUpb_DescriptorBase*)_self;
+  Py_INCREF(self->pool);
+  return self->pool;
+}
+
+static PyObject* PyUpb_FileDescriptor_GetPackage(PyObject* _self,
+                                                 void* closure) {
+  PyUpb_DescriptorBase* self = (PyUpb_DescriptorBase*)_self;
+  return PyUnicode_FromString(upb_FileDef_Package(self->def));
+}
+
+static PyObject* PyUpb_FileDescriptor_GetSerializedPb(PyObject* self,
+                                                      void* closure) {
+  return PyUpb_DescriptorBase_GetSerializedProto(
+      self, (PyUpb_ToProto_Func*)&upb_FileDef_ToProto,
+      &google_protobuf_FileDescriptorProto_msg_init);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetMessageTypesByName(PyObject* _self,
+                                                            void* closure) {
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_FileDef_TopLevelMessageCount,
+          (void*)&upb_FileDef_TopLevelMessage,
+          (void*)&PyUpb_Descriptor_Get,
+      },
+      (void*)&PyUpb_FileDescriptor_LookupMessage,
+      (void*)&upb_MessageDef_Name,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetEnumTypesByName(PyObject* _self,
+                                                         void* closure) {
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_FileDef_TopLevelEnumCount,
+          (void*)&upb_FileDef_TopLevelEnum,
+          (void*)&PyUpb_EnumDescriptor_Get,
+      },
+      (void*)&PyUpb_FileDescriptor_LookupEnum,
+      (void*)&upb_EnumDef_Name,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetExtensionsByName(PyObject* _self,
+                                                          void* closure) {
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_FileDef_TopLevelExtensionCount,
+          (void*)&upb_FileDef_TopLevelExtension,
+          (void*)&PyUpb_FieldDescriptor_Get,
+      },
+      (void*)&PyUpb_FileDescriptor_LookupExtension,
+      (void*)&upb_FieldDef_Name,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetServicesByName(PyObject* _self,
+                                                        void* closure) {
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_FileDef_ServiceCount,
+          (void*)&upb_FileDef_Service,
+          (void*)&PyUpb_ServiceDescriptor_Get,
+      },
+      (void*)&PyUpb_FileDescriptor_LookupService,
+      (void*)&upb_ServiceDef_Name,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetDependencies(PyObject* _self,
+                                                      void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_FileDef_DependencyCount,
+      (void*)&upb_FileDef_Dependency,
+      (void*)&PyUpb_FileDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetPublicDependencies(PyObject* _self,
+                                                            void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_FileDef_PublicDependencyCount,
+      (void*)&upb_FileDef_PublicDependency,
+      (void*)&PyUpb_FileDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetSyntax(PyObject* _self,
+                                                void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  const char* syntax =
+      upb_FileDef_Syntax(self->def) == kUpb_Syntax_Proto2 ? "proto2" : "proto3";
+  return PyUnicode_FromString(syntax);
+}
+
+static PyObject* PyUpb_FileDescriptor_GetHasOptions(PyObject* _self,
+                                                    void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyBool_FromLong(upb_FileDef_HasOptions(self->def));
+}
+
+static PyObject* PyUpb_FileDescriptor_GetOptions(PyObject* _self,
+                                                 PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_FileDef_Options(self->def), &google_protobuf_FileOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".FileOptions");
+}
+
+static PyObject* PyUpb_FileDescriptor_CopyToProto(PyObject* _self,
+                                                  PyObject* py_proto) {
+  return PyUpb_DescriptorBase_CopyToProto(
+      _self, (PyUpb_ToProto_Func*)&upb_FileDef_ToProto,
+      &google_protobuf_FileDescriptorProto_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".FileDescriptorProto", py_proto);
+}
+
+static PyGetSetDef PyUpb_FileDescriptor_Getters[] = {
+    {"pool", PyUpb_FileDescriptor_GetPool, NULL, "pool"},
+    {"name", (getter)PyUpb_FileDescriptor_GetName, NULL, "name"},
+    {"package", PyUpb_FileDescriptor_GetPackage, NULL, "package"},
+    {"serialized_pb", PyUpb_FileDescriptor_GetSerializedPb},
+    {"message_types_by_name", PyUpb_FileDescriptor_GetMessageTypesByName, NULL,
+     "Messages by name"},
+    {"enum_types_by_name", PyUpb_FileDescriptor_GetEnumTypesByName, NULL,
+     "Enums by name"},
+    {"extensions_by_name", PyUpb_FileDescriptor_GetExtensionsByName, NULL,
+     "Extensions by name"},
+    {"services_by_name", PyUpb_FileDescriptor_GetServicesByName, NULL,
+     "Services by name"},
+    {"dependencies", PyUpb_FileDescriptor_GetDependencies, NULL,
+     "Dependencies"},
+    {"public_dependencies", PyUpb_FileDescriptor_GetPublicDependencies, NULL,
+     "Dependencies"},
+    {"has_options", PyUpb_FileDescriptor_GetHasOptions, NULL, "Has Options"},
+    // begin:github_only
+    {"syntax", PyUpb_FileDescriptor_GetSyntax, (setter)NULL, "Syntax"},
+    // end:github_only
+    // begin:google_only
+//     // TODO Use this to open-source syntax deprecation.
+//     {"deprecated_syntax", PyUpb_FileDescriptor_GetSyntax, (setter)NULL,
+//      "Syntax"},
+    // end:google_only
+    {NULL},
+};
+
+static PyMethodDef PyUpb_FileDescriptor_Methods[] = {
+    {"GetOptions", PyUpb_FileDescriptor_GetOptions, METH_NOARGS},
+    {"CopyToProto", PyUpb_FileDescriptor_CopyToProto, METH_O},
+    {NULL}};
+
+static PyType_Slot PyUpb_FileDescriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_FileDescriptor_Methods},
+    {Py_tp_getset, PyUpb_FileDescriptor_Getters},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_FileDescriptor_Spec = {
+    PYUPB_MODULE_NAME ".FileDescriptor",  // tp_name
+    sizeof(PyUpb_DescriptorBase),         // tp_basicsize
+    0,                                    // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                   // tp_flags
+    PyUpb_FileDescriptor_Slots,
+};
+
+const upb_FileDef* PyUpb_FileDescriptor_GetDef(PyObject* _self) {
+  PyUpb_DescriptorBase* self =
+      PyUpb_DescriptorBase_Check(_self, kPyUpb_FileDescriptor);
+  return self ? self->def : NULL;
+}
+
+// -----------------------------------------------------------------------------
+// MethodDescriptor
+// -----------------------------------------------------------------------------
+
+const upb_MethodDef* PyUpb_MethodDescriptor_GetDef(PyObject* _self) {
+  PyUpb_DescriptorBase* self =
+      PyUpb_DescriptorBase_Check(_self, kPyUpb_MethodDescriptor);
+  return self ? self->def : NULL;
+}
+
+PyObject* PyUpb_MethodDescriptor_Get(const upb_MethodDef* m) {
+  const upb_FileDef* file = upb_ServiceDef_File(upb_MethodDef_Service(m));
+  return PyUpb_DescriptorBase_Get(kPyUpb_MethodDescriptor, m, file);
+}
+
+static PyObject* PyUpb_MethodDescriptor_GetName(PyObject* self, void* closure) {
+  const upb_MethodDef* m = PyUpb_MethodDescriptor_GetDef(self);
+  return PyUnicode_FromString(upb_MethodDef_Name(m));
+}
+
+static PyObject* PyUpb_MethodDescriptor_GetFullName(PyObject* self,
+                                                    void* closure) {
+  const upb_MethodDef* m = PyUpb_MethodDescriptor_GetDef(self);
+  return PyUnicode_FromString(upb_MethodDef_FullName(m));
+}
+
+static PyObject* PyUpb_MethodDescriptor_GetIndex(PyObject* self,
+                                                 void* closure) {
+  const upb_MethodDef* oneof = PyUpb_MethodDescriptor_GetDef(self);
+  return PyLong_FromLong(upb_MethodDef_Index(oneof));
+}
+
+static PyObject* PyUpb_MethodDescriptor_GetContainingService(PyObject* self,
+                                                             void* closure) {
+  const upb_MethodDef* m = PyUpb_MethodDescriptor_GetDef(self);
+  return PyUpb_ServiceDescriptor_Get(upb_MethodDef_Service(m));
+}
+
+static PyObject* PyUpb_MethodDescriptor_GetInputType(PyObject* self,
+                                                     void* closure) {
+  const upb_MethodDef* m = PyUpb_MethodDescriptor_GetDef(self);
+  return PyUpb_Descriptor_Get(upb_MethodDef_InputType(m));
+}
+
+static PyObject* PyUpb_MethodDescriptor_GetOutputType(PyObject* self,
+                                                      void* closure) {
+  const upb_MethodDef* m = PyUpb_MethodDescriptor_GetDef(self);
+  return PyUpb_Descriptor_Get(upb_MethodDef_OutputType(m));
+}
+
+static PyObject* PyUpb_MethodDescriptor_GetOptions(PyObject* _self,
+                                                   PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_MethodDef_Options(self->def), &google_protobuf_MethodOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".MethodOptions");
+}
+
+static PyObject* PyUpb_MethodDescriptor_CopyToProto(PyObject* _self,
+                                                    PyObject* py_proto) {
+  return PyUpb_DescriptorBase_CopyToProto(
+      _self, (PyUpb_ToProto_Func*)&upb_MethodDef_ToProto,
+      &google_protobuf_MethodDescriptorProto_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".MethodDescriptorProto", py_proto);
+}
+
+static PyGetSetDef PyUpb_MethodDescriptor_Getters[] = {
+    {"name", PyUpb_MethodDescriptor_GetName, NULL, "Name", NULL},
+    {"full_name", PyUpb_MethodDescriptor_GetFullName, NULL, "Full name", NULL},
+    {"index", PyUpb_MethodDescriptor_GetIndex, NULL, "Index", NULL},
+    {"containing_service", PyUpb_MethodDescriptor_GetContainingService, NULL,
+     "Containing service", NULL},
+    {"input_type", PyUpb_MethodDescriptor_GetInputType, NULL, "Input type",
+     NULL},
+    {"output_type", PyUpb_MethodDescriptor_GetOutputType, NULL, "Output type",
+     NULL},
+    {NULL}};
+
+static PyMethodDef PyUpb_MethodDescriptor_Methods[] = {
+    {"GetOptions", PyUpb_MethodDescriptor_GetOptions, METH_NOARGS},
+    {"CopyToProto", PyUpb_MethodDescriptor_CopyToProto, METH_O},
+    {NULL}};
+
+static PyType_Slot PyUpb_MethodDescriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_MethodDescriptor_Methods},
+    {Py_tp_getset, PyUpb_MethodDescriptor_Getters},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_MethodDescriptor_Spec = {
+    PYUPB_MODULE_NAME ".MethodDescriptor",  // tp_name
+    sizeof(PyUpb_DescriptorBase),           // tp_basicsize
+    0,                                      // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                     // tp_flags
+    PyUpb_MethodDescriptor_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// OneofDescriptor
+// -----------------------------------------------------------------------------
+
+const upb_OneofDef* PyUpb_OneofDescriptor_GetDef(PyObject* _self) {
+  PyUpb_DescriptorBase* self =
+      PyUpb_DescriptorBase_Check(_self, kPyUpb_OneofDescriptor);
+  return self ? self->def : NULL;
+}
+
+PyObject* PyUpb_OneofDescriptor_Get(const upb_OneofDef* oneof) {
+  const upb_FileDef* file =
+      upb_MessageDef_File(upb_OneofDef_ContainingType(oneof));
+  return PyUpb_DescriptorBase_Get(kPyUpb_OneofDescriptor, oneof, file);
+}
+
+static PyObject* PyUpb_OneofDescriptor_GetName(PyObject* self, void* closure) {
+  const upb_OneofDef* oneof = PyUpb_OneofDescriptor_GetDef(self);
+  return PyUnicode_FromString(upb_OneofDef_Name(oneof));
+}
+
+static PyObject* PyUpb_OneofDescriptor_GetFullName(PyObject* self,
+                                                   void* closure) {
+  const upb_OneofDef* oneof = PyUpb_OneofDescriptor_GetDef(self);
+  return PyUnicode_FromFormat(
+      "%s.%s", upb_MessageDef_FullName(upb_OneofDef_ContainingType(oneof)),
+      upb_OneofDef_Name(oneof));
+}
+
+static PyObject* PyUpb_OneofDescriptor_GetIndex(PyObject* self, void* closure) {
+  const upb_OneofDef* oneof = PyUpb_OneofDescriptor_GetDef(self);
+  return PyLong_FromLong(upb_OneofDef_Index(oneof));
+}
+
+static PyObject* PyUpb_OneofDescriptor_GetContainingType(PyObject* self,
+                                                         void* closure) {
+  const upb_OneofDef* oneof = PyUpb_OneofDescriptor_GetDef(self);
+  return PyUpb_Descriptor_Get(upb_OneofDef_ContainingType(oneof));
+}
+
+static PyObject* PyUpb_OneofDescriptor_GetHasOptions(PyObject* _self,
+                                                     void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyBool_FromLong(upb_OneofDef_HasOptions(self->def));
+}
+
+static PyObject* PyUpb_OneofDescriptor_GetFields(PyObject* _self,
+                                                 void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_OneofDef_FieldCount,
+      (void*)&upb_OneofDef_Field,
+      (void*)&PyUpb_FieldDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_OneofDescriptor_GetOptions(PyObject* _self,
+                                                  PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_OneofDef_Options(self->def), &google_protobuf_OneofOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".OneofOptions");
+}
+
+static PyGetSetDef PyUpb_OneofDescriptor_Getters[] = {
+    {"name", PyUpb_OneofDescriptor_GetName, NULL, "Name"},
+    {"full_name", PyUpb_OneofDescriptor_GetFullName, NULL, "Full name"},
+    {"index", PyUpb_OneofDescriptor_GetIndex, NULL, "Index"},
+    {"containing_type", PyUpb_OneofDescriptor_GetContainingType, NULL,
+     "Containing type"},
+    {"has_options", PyUpb_OneofDescriptor_GetHasOptions, NULL, "Has Options"},
+    {"fields", PyUpb_OneofDescriptor_GetFields, NULL, "Fields"},
+    {NULL}};
+
+static PyMethodDef PyUpb_OneofDescriptor_Methods[] = {
+    {"GetOptions", PyUpb_OneofDescriptor_GetOptions, METH_NOARGS}, {NULL}};
+
+static PyType_Slot PyUpb_OneofDescriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_OneofDescriptor_Methods},
+    {Py_tp_getset, PyUpb_OneofDescriptor_Getters},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_OneofDescriptor_Spec = {
+    PYUPB_MODULE_NAME ".OneofDescriptor",  // tp_name
+    sizeof(PyUpb_DescriptorBase),          // tp_basicsize
+    0,                                     // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                    // tp_flags
+    PyUpb_OneofDescriptor_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// ServiceDescriptor
+// -----------------------------------------------------------------------------
+
+const upb_ServiceDef* PyUpb_ServiceDescriptor_GetDef(PyObject* _self) {
+  PyUpb_DescriptorBase* self =
+      PyUpb_DescriptorBase_Check(_self, kPyUpb_ServiceDescriptor);
+  return self ? self->def : NULL;
+}
+
+PyObject* PyUpb_ServiceDescriptor_Get(const upb_ServiceDef* s) {
+  const upb_FileDef* file = upb_ServiceDef_File(s);
+  return PyUpb_DescriptorBase_Get(kPyUpb_ServiceDescriptor, s, file);
+}
+
+static PyObject* PyUpb_ServiceDescriptor_GetFullName(PyObject* self,
+                                                     void* closure) {
+  const upb_ServiceDef* s = PyUpb_ServiceDescriptor_GetDef(self);
+  return PyUnicode_FromString(upb_ServiceDef_FullName(s));
+}
+
+static PyObject* PyUpb_ServiceDescriptor_GetName(PyObject* self,
+                                                 void* closure) {
+  const upb_ServiceDef* s = PyUpb_ServiceDescriptor_GetDef(self);
+  return PyUnicode_FromString(upb_ServiceDef_Name(s));
+}
+
+static PyObject* PyUpb_ServiceDescriptor_GetFile(PyObject* self,
+                                                 void* closure) {
+  const upb_ServiceDef* s = PyUpb_ServiceDescriptor_GetDef(self);
+  return PyUpb_FileDescriptor_Get(upb_ServiceDef_File(s));
+}
+
+static PyObject* PyUpb_ServiceDescriptor_GetIndex(PyObject* self,
+                                                  void* closure) {
+  const upb_ServiceDef* s = PyUpb_ServiceDescriptor_GetDef(self);
+  return PyLong_FromLong(upb_ServiceDef_Index(s));
+}
+
+static PyObject* PyUpb_ServiceDescriptor_GetMethods(PyObject* _self,
+                                                    void* closure) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  static PyUpb_GenericSequence_Funcs funcs = {
+      (void*)&upb_ServiceDef_MethodCount,
+      (void*)&upb_ServiceDef_Method,
+      (void*)&PyUpb_MethodDescriptor_Get,
+  };
+  return PyUpb_GenericSequence_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_ServiceDescriptor_GetMethodsByName(PyObject* _self,
+                                                          void* closure) {
+  static PyUpb_ByNameMap_Funcs funcs = {
+      {
+          (void*)&upb_ServiceDef_MethodCount,
+          (void*)&upb_ServiceDef_Method,
+          (void*)&PyUpb_MethodDescriptor_Get,
+      },
+      (void*)&upb_ServiceDef_FindMethodByName,
+      (void*)&upb_MethodDef_Name,
+  };
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_ByNameMap_New(&funcs, self->def, self->pool);
+}
+
+static PyObject* PyUpb_ServiceDescriptor_GetOptions(PyObject* _self,
+                                                    PyObject* args) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  return PyUpb_DescriptorBase_GetOptions(
+      self, upb_ServiceDef_Options(self->def), &google_protobuf_ServiceOptions_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".ServiceOptions");
+}
+
+static PyObject* PyUpb_ServiceDescriptor_CopyToProto(PyObject* _self,
+                                                     PyObject* py_proto) {
+  return PyUpb_DescriptorBase_CopyToProto(
+      _self, (PyUpb_ToProto_Func*)&upb_ServiceDef_ToProto,
+      &google_protobuf_ServiceDescriptorProto_msg_init,
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".ServiceDescriptorProto", py_proto);
+}
+
+static PyObject* PyUpb_ServiceDescriptor_FindMethodByName(PyObject* _self,
+                                                          PyObject* py_name) {
+  PyUpb_DescriptorBase* self = (void*)_self;
+  const char* name = PyUnicode_AsUTF8AndSize(py_name, NULL);
+  if (!name) return NULL;
+  const upb_MethodDef* method =
+      upb_ServiceDef_FindMethodByName(self->def, name);
+  if (method == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find method %.200s", name);
+  }
+  return PyUpb_MethodDescriptor_Get(method);
+}
+
+static PyGetSetDef PyUpb_ServiceDescriptor_Getters[] = {
+    {"name", PyUpb_ServiceDescriptor_GetName, NULL, "Name", NULL},
+    {"full_name", PyUpb_ServiceDescriptor_GetFullName, NULL, "Full name", NULL},
+    {"file", PyUpb_ServiceDescriptor_GetFile, NULL, "File descriptor"},
+    {"index", PyUpb_ServiceDescriptor_GetIndex, NULL, "Index", NULL},
+    {"methods", PyUpb_ServiceDescriptor_GetMethods, NULL, "Methods", NULL},
+    {"methods_by_name", PyUpb_ServiceDescriptor_GetMethodsByName, NULL,
+     "Methods by name", NULL},
+    {NULL}};
+
+static PyMethodDef PyUpb_ServiceDescriptor_Methods[] = {
+    {"GetOptions", PyUpb_ServiceDescriptor_GetOptions, METH_NOARGS},
+    {"CopyToProto", PyUpb_ServiceDescriptor_CopyToProto, METH_O},
+    {"FindMethodByName", PyUpb_ServiceDescriptor_FindMethodByName, METH_O},
+    {NULL}};
+
+static PyType_Slot PyUpb_ServiceDescriptor_Slots[] = {
+    DESCRIPTOR_BASE_SLOTS,
+    {Py_tp_methods, PyUpb_ServiceDescriptor_Methods},
+    {Py_tp_getset, PyUpb_ServiceDescriptor_Getters},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_ServiceDescriptor_Spec = {
+    PYUPB_MODULE_NAME ".ServiceDescriptor",  // tp_name
+    sizeof(PyUpb_DescriptorBase),            // tp_basicsize
+    0,                                       // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                      // tp_flags
+    PyUpb_ServiceDescriptor_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// Top Level
+// -----------------------------------------------------------------------------
+
+static bool PyUpb_SetIntAttr(PyObject* obj, const char* name, int val) {
+  PyObject* num = PyLong_FromLong(val);
+  if (!num) return false;
+  int status = PyObject_SetAttrString(obj, name, num);
+  Py_DECREF(num);
+  return status >= 0;
+}
+
+// These must be in the same order as PyUpb_DescriptorType in the header.
+static PyType_Spec* desc_specs[] = {
+    &PyUpb_Descriptor_Spec,          &PyUpb_EnumDescriptor_Spec,
+    &PyUpb_EnumValueDescriptor_Spec, &PyUpb_FieldDescriptor_Spec,
+    &PyUpb_FileDescriptor_Spec,      &PyUpb_MethodDescriptor_Spec,
+    &PyUpb_OneofDescriptor_Spec,     &PyUpb_ServiceDescriptor_Spec,
+};
+
+bool PyUpb_InitDescriptor(PyObject* m) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_GetFromModule(m);
+
+  for (size_t i = 0; i < kPyUpb_Descriptor_Count; i++) {
+    s->descriptor_types[i] = PyUpb_AddClass(m, desc_specs[i]);
+    if (!s->descriptor_types[i]) {
+      return false;
+    }
+  }
+
+  PyObject* fd = (PyObject*)s->descriptor_types[kPyUpb_FieldDescriptor];
+  return PyUpb_SetIntAttr(fd, "LABEL_OPTIONAL", kUpb_Label_Optional) &&
+         PyUpb_SetIntAttr(fd, "LABEL_REPEATED", kUpb_Label_Repeated) &&
+         PyUpb_SetIntAttr(fd, "LABEL_REQUIRED", kUpb_Label_Required) &&
+         PyUpb_SetIntAttr(fd, "TYPE_BOOL", kUpb_FieldType_Bool) &&
+         PyUpb_SetIntAttr(fd, "TYPE_BYTES", kUpb_FieldType_Bytes) &&
+         PyUpb_SetIntAttr(fd, "TYPE_DOUBLE", kUpb_FieldType_Double) &&
+         PyUpb_SetIntAttr(fd, "TYPE_ENUM", kUpb_FieldType_Enum) &&
+         PyUpb_SetIntAttr(fd, "TYPE_FIXED32", kUpb_FieldType_Fixed32) &&
+         PyUpb_SetIntAttr(fd, "TYPE_FIXED64", kUpb_FieldType_Fixed64) &&
+         PyUpb_SetIntAttr(fd, "TYPE_FLOAT", kUpb_FieldType_Float) &&
+         PyUpb_SetIntAttr(fd, "TYPE_GROUP", kUpb_FieldType_Group) &&
+         PyUpb_SetIntAttr(fd, "TYPE_INT32", kUpb_FieldType_Int32) &&
+         PyUpb_SetIntAttr(fd, "TYPE_INT64", kUpb_FieldType_Int64) &&
+         PyUpb_SetIntAttr(fd, "TYPE_MESSAGE", kUpb_FieldType_Message) &&
+         PyUpb_SetIntAttr(fd, "TYPE_SFIXED32", kUpb_FieldType_SFixed32) &&
+         PyUpb_SetIntAttr(fd, "TYPE_SFIXED64", kUpb_FieldType_SFixed64) &&
+         PyUpb_SetIntAttr(fd, "TYPE_SINT32", kUpb_FieldType_SInt32) &&
+         PyUpb_SetIntAttr(fd, "TYPE_SINT64", kUpb_FieldType_SInt64) &&
+         PyUpb_SetIntAttr(fd, "TYPE_STRING", kUpb_FieldType_String) &&
+         PyUpb_SetIntAttr(fd, "TYPE_UINT32", kUpb_FieldType_UInt32) &&
+         PyUpb_SetIntAttr(fd, "TYPE_UINT64", kUpb_FieldType_UInt64) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_INT32", CPPTYPE_INT32) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_INT64", CPPTYPE_INT64) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_UINT32", CPPTYPE_UINT32) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_UINT64", CPPTYPE_UINT64) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_DOUBLE", CPPTYPE_DOUBLE) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_FLOAT", CPPTYPE_FLOAT) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_BOOL", CPPTYPE_BOOL) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_ENUM", CPPTYPE_ENUM) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_STRING", CPPTYPE_STRING) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_BYTES", CPPTYPE_STRING) &&
+         PyUpb_SetIntAttr(fd, "CPPTYPE_MESSAGE", CPPTYPE_MESSAGE);
+}
diff --git a/python/descriptor.h b/python/descriptor.h
new file mode 100644
index 0000000..7fa0164
--- /dev/null
+++ b/python/descriptor.h
@@ -0,0 +1,85 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_DESCRIPTOR_H__
+#define PYUPB_DESCRIPTOR_H__
+
+#include <stdbool.h>
+
+#include "python/python_api.h"
+#include "upb/reflection/def.h"
+
+typedef enum {
+  kPyUpb_Descriptor = 0,
+  kPyUpb_EnumDescriptor = 1,
+  kPyUpb_EnumValueDescriptor = 2,
+  kPyUpb_FieldDescriptor = 3,
+  kPyUpb_FileDescriptor = 4,
+  kPyUpb_MethodDescriptor = 5,
+  kPyUpb_OneofDescriptor = 6,
+  kPyUpb_ServiceDescriptor = 7,
+  kPyUpb_Descriptor_Count = 8,
+} PyUpb_DescriptorType;
+
+// Given a descriptor object |desc|, returns a Python message class object for
+// the msgdef |m|, which must be from the same pool.
+PyObject* PyUpb_Descriptor_GetClass(const upb_MessageDef* m);
+
+// Set the message descriptor's meta class.
+void PyUpb_Descriptor_SetClass(PyObject* py_descriptor, PyObject* meta);
+
+// Returns a Python wrapper object for the given def. This will return an
+// existing object if one already exists, otherwise a new object will be
+// created.  The caller always owns a ref on the returned object.
+PyObject* PyUpb_Descriptor_Get(const upb_MessageDef* msgdef);
+PyObject* PyUpb_EnumDescriptor_Get(const upb_EnumDef* enumdef);
+PyObject* PyUpb_FieldDescriptor_Get(const upb_FieldDef* field);
+PyObject* PyUpb_FileDescriptor_Get(const upb_FileDef* file);
+PyObject* PyUpb_OneofDescriptor_Get(const upb_OneofDef* oneof);
+PyObject* PyUpb_EnumValueDescriptor_Get(const upb_EnumValueDef* enumval);
+PyObject* PyUpb_Descriptor_GetOrCreateWrapper(const upb_MessageDef* msg);
+PyObject* PyUpb_ServiceDescriptor_Get(const upb_ServiceDef* s);
+PyObject* PyUpb_MethodDescriptor_Get(const upb_MethodDef* s);
+
+// Returns the underlying |def| for a given wrapper object. The caller must
+// have already verified that the given Python object is of the expected type.
+const upb_FileDef* PyUpb_FileDescriptor_GetDef(PyObject* file);
+const upb_FieldDef* PyUpb_FieldDescriptor_GetDef(PyObject* file);
+const upb_MessageDef* PyUpb_Descriptor_GetDef(PyObject* _self);
+const void* PyUpb_AnyDescriptor_GetDef(PyObject* _self);
+
+// Returns the underlying |def| for a given wrapper object. The caller must
+// have already verified that the given Python object is of the expected type.
+const upb_FileDef* PyUpb_FileDescriptor_GetDef(PyObject* file);
+
+// Module-level init.
+bool PyUpb_InitDescriptor(PyObject* m);
+
+#endif  // PYUPB_DESCRIPTOR_H__
diff --git a/python/descriptor_containers.c b/python/descriptor_containers.c
new file mode 100644
index 0000000..e1eacb2
--- /dev/null
+++ b/python/descriptor_containers.c
@@ -0,0 +1,816 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/descriptor_containers.h"
+
+#include "python/descriptor.h"
+#include "python/protobuf.h"
+#include "upb/reflection/def.h"
+
+// Implements __repr__ as str(dict(self)).
+static PyObject* PyUpb_DescriptorMap_Repr(PyObject* _self) {
+  PyObject* dict = PyDict_New();
+  PyObject* ret = NULL;
+  if (!dict) goto err;
+  if (PyDict_Merge(dict, _self, 1) != 0) goto err;
+  ret = PyObject_Str(dict);
+
+err:
+  Py_XDECREF(dict);
+  return ret;
+}
+
+// -----------------------------------------------------------------------------
+// ByNameIterator
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  const PyUpb_ByNameMap_Funcs* funcs;
+  const void* parent;    // upb_MessageDef*, upb_DefPool*, etc.
+  PyObject* parent_obj;  // Python object that keeps parent alive, we own a ref.
+  int index;             // Current iterator index.
+} PyUpb_ByNameIterator;
+
+static PyUpb_ByNameIterator* PyUpb_ByNameIterator_Self(PyObject* obj) {
+  assert(Py_TYPE(obj) == PyUpb_ModuleState_Get()->by_name_iterator_type);
+  return (PyUpb_ByNameIterator*)obj;
+}
+
+static void PyUpb_ByNameIterator_Dealloc(PyObject* _self) {
+  PyUpb_ByNameIterator* self = PyUpb_ByNameIterator_Self(_self);
+  Py_DECREF(self->parent_obj);
+  PyUpb_Dealloc(self);
+}
+
+static PyObject* PyUpb_ByNameIterator_New(const PyUpb_ByNameMap_Funcs* funcs,
+                                          const void* parent,
+                                          PyObject* parent_obj) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  PyUpb_ByNameIterator* iter =
+      (void*)PyType_GenericAlloc(s->by_name_iterator_type, 0);
+  iter->funcs = funcs;
+  iter->parent = parent;
+  iter->parent_obj = parent_obj;
+  iter->index = 0;
+  Py_INCREF(iter->parent_obj);
+  return &iter->ob_base;
+}
+
+static PyObject* PyUpb_ByNameIterator_IterNext(PyObject* _self) {
+  PyUpb_ByNameIterator* self = PyUpb_ByNameIterator_Self(_self);
+  int size = self->funcs->base.get_elem_count(self->parent);
+  if (self->index >= size) return NULL;
+  const void* elem = self->funcs->base.index(self->parent, self->index);
+  self->index++;
+  return PyUnicode_FromString(self->funcs->get_elem_name(elem));
+}
+
+static PyType_Slot PyUpb_ByNameIterator_Slots[] = {
+    {Py_tp_dealloc, PyUpb_ByNameIterator_Dealloc},
+    {Py_tp_iter, PyObject_SelfIter},
+    {Py_tp_iternext, PyUpb_ByNameIterator_IterNext},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_ByNameIterator_Spec = {
+    PYUPB_MODULE_NAME "._ByNameIterator",  // tp_name
+    sizeof(PyUpb_ByNameIterator),          // tp_basicsize
+    0,                                     // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                    // tp_flags
+    PyUpb_ByNameIterator_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// ByNumberIterator
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  const PyUpb_ByNumberMap_Funcs* funcs;
+  const void* parent;    // upb_MessageDef*, upb_DefPool*, etc.
+  PyObject* parent_obj;  // Python object that keeps parent alive, we own a ref.
+  int index;             // Current iterator index.
+} PyUpb_ByNumberIterator;
+
+static PyUpb_ByNumberIterator* PyUpb_ByNumberIterator_Self(PyObject* obj) {
+  assert(Py_TYPE(obj) == PyUpb_ModuleState_Get()->by_number_iterator_type);
+  return (PyUpb_ByNumberIterator*)obj;
+}
+
+static void PyUpb_ByNumberIterator_Dealloc(PyObject* _self) {
+  PyUpb_ByNumberIterator* self = PyUpb_ByNumberIterator_Self(_self);
+  Py_DECREF(self->parent_obj);
+  PyUpb_Dealloc(self);
+}
+
+static PyObject* PyUpb_ByNumberIterator_New(
+    const PyUpb_ByNumberMap_Funcs* funcs, const void* parent,
+    PyObject* parent_obj) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  PyUpb_ByNumberIterator* iter =
+      (void*)PyType_GenericAlloc(s->by_number_iterator_type, 0);
+  iter->funcs = funcs;
+  iter->parent = parent;
+  iter->parent_obj = parent_obj;
+  iter->index = 0;
+  Py_INCREF(iter->parent_obj);
+  return &iter->ob_base;
+}
+
+static PyObject* PyUpb_ByNumberIterator_IterNext(PyObject* _self) {
+  PyUpb_ByNumberIterator* self = PyUpb_ByNumberIterator_Self(_self);
+  int size = self->funcs->base.get_elem_count(self->parent);
+  if (self->index >= size) return NULL;
+  const void* elem = self->funcs->base.index(self->parent, self->index);
+  self->index++;
+  return PyLong_FromLong(self->funcs->get_elem_num(elem));
+}
+
+static PyType_Slot PyUpb_ByNumberIterator_Slots[] = {
+    {Py_tp_dealloc, PyUpb_ByNumberIterator_Dealloc},
+    {Py_tp_iter, PyObject_SelfIter},
+    {Py_tp_iternext, PyUpb_ByNumberIterator_IterNext},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_ByNumberIterator_Spec = {
+    PYUPB_MODULE_NAME "._ByNumberIterator",  // tp_name
+    sizeof(PyUpb_ByNumberIterator),          // tp_basicsize
+    0,                                       // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                      // tp_flags
+    PyUpb_ByNumberIterator_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// GenericSequence
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  const PyUpb_GenericSequence_Funcs* funcs;
+  const void* parent;    // upb_MessageDef*, upb_DefPool*, etc.
+  PyObject* parent_obj;  // Python object that keeps parent alive, we own a ref.
+} PyUpb_GenericSequence;
+
+PyUpb_GenericSequence* PyUpb_GenericSequence_Self(PyObject* obj) {
+  assert(Py_TYPE(obj) == PyUpb_ModuleState_Get()->generic_sequence_type);
+  return (PyUpb_GenericSequence*)obj;
+}
+
+static void PyUpb_GenericSequence_Dealloc(PyObject* _self) {
+  PyUpb_GenericSequence* self = PyUpb_GenericSequence_Self(_self);
+  Py_CLEAR(self->parent_obj);
+  PyUpb_Dealloc(self);
+}
+
+PyObject* PyUpb_GenericSequence_New(const PyUpb_GenericSequence_Funcs* funcs,
+                                    const void* parent, PyObject* parent_obj) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  PyUpb_GenericSequence* seq =
+      (PyUpb_GenericSequence*)PyType_GenericAlloc(s->generic_sequence_type, 0);
+  seq->funcs = funcs;
+  seq->parent = parent;
+  seq->parent_obj = parent_obj;
+  Py_INCREF(parent_obj);
+  return &seq->ob_base;
+}
+
+static Py_ssize_t PyUpb_GenericSequence_Length(PyObject* _self) {
+  PyUpb_GenericSequence* self = PyUpb_GenericSequence_Self(_self);
+  return self->funcs->get_elem_count(self->parent);
+}
+
+static PyObject* PyUpb_GenericSequence_GetItem(PyObject* _self,
+                                               Py_ssize_t index) {
+  PyUpb_GenericSequence* self = PyUpb_GenericSequence_Self(_self);
+  Py_ssize_t size = self->funcs->get_elem_count(self->parent);
+  if (index < 0) {
+    index += size;
+  }
+  if (index < 0 || index >= size) {
+    PyErr_Format(PyExc_IndexError, "list index (%zd) out of range", index);
+    return NULL;
+  }
+  const void* elem = self->funcs->index(self->parent, index);
+  return self->funcs->get_elem_wrapper(elem);
+}
+
+// A sequence container can only be equal to another sequence container, or (for
+// backward compatibility) to a list containing the same items.
+// Returns 1 if equal, 0 if unequal, -1 on error.
+static int PyUpb_GenericSequence_IsEqual(PyUpb_GenericSequence* self,
+                                         PyObject* other) {
+  // Check the identity of C++ pointers.
+  if (PyObject_TypeCheck(other, Py_TYPE(self))) {
+    PyUpb_GenericSequence* other_seq = (void*)other;
+    return self->parent == other_seq->parent && self->funcs == other_seq->funcs;
+  }
+
+  if (!PyList_Check(other)) return 0;
+
+  // return list(self) == other
+  // We can clamp `i` to int because GenericSequence uses int for size (this
+  // is useful when we do int iteration below).
+  int n = PyUpb_GenericSequence_Length((PyObject*)self);
+  if ((Py_ssize_t)n != PyList_Size(other)) {
+    return false;
+  }
+
+  PyObject* item1;
+  for (int i = 0; i < n; i++) {
+    item1 = PyUpb_GenericSequence_GetItem((PyObject*)self, i);
+    PyObject* item2 = PyList_GetItem(other, i);
+    if (!item1 || !item2) goto error;
+    int cmp = PyObject_RichCompareBool(item1, item2, Py_EQ);
+    Py_DECREF(item1);
+    if (cmp != 1) return cmp;
+  }
+  // All items were found and equal
+  return 1;
+
+error:
+  Py_XDECREF(item1);
+  return -1;
+}
+
+static PyObject* PyUpb_GenericSequence_RichCompare(PyObject* _self,
+                                                   PyObject* other, int opid) {
+  PyUpb_GenericSequence* self = PyUpb_GenericSequence_Self(_self);
+  if (opid != Py_EQ && opid != Py_NE) {
+    Py_RETURN_NOTIMPLEMENTED;
+  }
+  bool ret = PyUpb_GenericSequence_IsEqual(self, other);
+  if (opid == Py_NE) ret = !ret;
+  return PyBool_FromLong(ret);
+}
+
+static PyObject* PyUpb_GenericSequence_Subscript(PyObject* _self,
+                                                 PyObject* item) {
+  PyUpb_GenericSequence* self = PyUpb_GenericSequence_Self(_self);
+  Py_ssize_t size = self->funcs->get_elem_count(self->parent);
+  Py_ssize_t idx, count, step;
+  if (!PyUpb_IndexToRange(item, size, &idx, &count, &step)) return NULL;
+  if (step == 0) {
+    return PyUpb_GenericSequence_GetItem(_self, idx);
+  } else {
+    PyObject* list = PyList_New(count);
+    for (Py_ssize_t i = 0; i < count; i++, idx += step) {
+      const void* elem = self->funcs->index(self->parent, idx);
+      PyList_SetItem(list, i, self->funcs->get_elem_wrapper(elem));
+    }
+    return list;
+  }
+}
+
+// Linear search.  Could optimize this in some cases (defs that have index),
+// but not all (FileDescriptor.dependencies).
+static int PyUpb_GenericSequence_Find(PyObject* _self, PyObject* item) {
+  PyUpb_GenericSequence* self = PyUpb_GenericSequence_Self(_self);
+  const void* item_ptr = PyUpb_AnyDescriptor_GetDef(item);
+  int count = self->funcs->get_elem_count(self->parent);
+  for (int i = 0; i < count; i++) {
+    if (self->funcs->index(self->parent, i) == item_ptr) {
+      return i;
+    }
+  }
+  return -1;
+}
+
+static PyObject* PyUpb_GenericSequence_Index(PyObject* self, PyObject* item) {
+  int position = PyUpb_GenericSequence_Find(self, item);
+  if (position < 0) {
+    PyErr_SetNone(PyExc_ValueError);
+    return NULL;
+  } else {
+    return PyLong_FromLong(position);
+  }
+}
+
+static PyObject* PyUpb_GenericSequence_Count(PyObject* _self, PyObject* item) {
+  PyUpb_GenericSequence* self = PyUpb_GenericSequence_Self(_self);
+  const void* item_ptr = PyUpb_AnyDescriptor_GetDef(item);
+  int n = self->funcs->get_elem_count(self->parent);
+  int count = 0;
+  for (int i = 0; i < n; i++) {
+    if (self->funcs->index(self->parent, i) == item_ptr) {
+      count++;
+    }
+  }
+  return PyLong_FromLong(count);
+}
+
+static PyObject* PyUpb_GenericSequence_Append(PyObject* self, PyObject* args) {
+  PyErr_Format(PyExc_TypeError, "'%R' is not a mutable sequence", self);
+  return NULL;
+}
+
+static PyMethodDef PyUpb_GenericSequence_Methods[] = {
+    {"index", PyUpb_GenericSequence_Index, METH_O},
+    {"count", PyUpb_GenericSequence_Count, METH_O},
+    {"append", PyUpb_GenericSequence_Append, METH_O},
+    // This was implemented for Python/C++ but so far has not been required.
+    //{ "__reversed__", (PyCFunction)Reversed, METH_NOARGS, },
+    {NULL}};
+
+static PyType_Slot PyUpb_GenericSequence_Slots[] = {
+    {Py_tp_dealloc, &PyUpb_GenericSequence_Dealloc},
+    {Py_tp_methods, &PyUpb_GenericSequence_Methods},
+    {Py_sq_length, PyUpb_GenericSequence_Length},
+    {Py_sq_item, PyUpb_GenericSequence_GetItem},
+    {Py_tp_richcompare, &PyUpb_GenericSequence_RichCompare},
+    {Py_mp_subscript, PyUpb_GenericSequence_Subscript},
+    // These were implemented for Python/C++ but so far have not been required.
+    // {Py_tp_repr, &PyUpb_GenericSequence_Repr},
+    // {Py_sq_contains, PyUpb_GenericSequence_Contains},
+    // {Py_mp_ass_subscript, PyUpb_GenericSequence_AssignSubscript},
+    {0, NULL},
+};
+
+static PyType_Spec PyUpb_GenericSequence_Spec = {
+    PYUPB_MODULE_NAME "._GenericSequence",  // tp_name
+    sizeof(PyUpb_GenericSequence),          // tp_basicsize
+    0,                                      // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                     // tp_flags
+    PyUpb_GenericSequence_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// ByNameMap
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  const PyUpb_ByNameMap_Funcs* funcs;
+  const void* parent;    // upb_MessageDef*, upb_DefPool*, etc.
+  PyObject* parent_obj;  // Python object that keeps parent alive, we own a ref.
+} PyUpb_ByNameMap;
+
+PyUpb_ByNameMap* PyUpb_ByNameMap_Self(PyObject* obj) {
+  assert(Py_TYPE(obj) == PyUpb_ModuleState_Get()->by_name_map_type);
+  return (PyUpb_ByNameMap*)obj;
+}
+
+static void PyUpb_ByNameMap_Dealloc(PyObject* _self) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  Py_DECREF(self->parent_obj);
+  PyUpb_Dealloc(self);
+}
+
+PyObject* PyUpb_ByNameMap_New(const PyUpb_ByNameMap_Funcs* funcs,
+                              const void* parent, PyObject* parent_obj) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  PyUpb_ByNameMap* map = (void*)PyType_GenericAlloc(s->by_name_map_type, 0);
+  map->funcs = funcs;
+  map->parent = parent;
+  map->parent_obj = parent_obj;
+  Py_INCREF(parent_obj);
+  return &map->ob_base;
+}
+
+static Py_ssize_t PyUpb_ByNameMap_Length(PyObject* _self) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  return self->funcs->base.get_elem_count(self->parent);
+}
+
+static PyObject* PyUpb_ByNameMap_Subscript(PyObject* _self, PyObject* key) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  const char* name = PyUpb_GetStrData(key);
+  const void* elem = name ? self->funcs->lookup(self->parent, name) : NULL;
+
+  if (!name && PyObject_Hash(key) == -1) return NULL;
+
+  if (elem) {
+    return self->funcs->base.get_elem_wrapper(elem);
+  } else {
+    PyErr_SetObject(PyExc_KeyError, key);
+    return NULL;
+  }
+}
+
+static int PyUpb_ByNameMap_AssignSubscript(PyObject* self, PyObject* key,
+                                           PyObject* value) {
+  PyErr_Format(PyExc_TypeError, PYUPB_MODULE_NAME
+               ".ByNameMap' object does not support item assignment");
+  return -1;
+}
+
+static int PyUpb_ByNameMap_Contains(PyObject* _self, PyObject* key) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  const char* name = PyUpb_GetStrData(key);
+  const void* elem = name ? self->funcs->lookup(self->parent, name) : NULL;
+  if (!name && PyObject_Hash(key) == -1) return -1;
+  return elem ? 1 : 0;
+}
+
+static PyObject* PyUpb_ByNameMap_Get(PyObject* _self, PyObject* args) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  PyObject* key;
+  PyObject* default_value = Py_None;
+  if (!PyArg_UnpackTuple(args, "get", 1, 2, &key, &default_value)) {
+    return NULL;
+  }
+
+  const char* name = PyUpb_GetStrData(key);
+  const void* elem = name ? self->funcs->lookup(self->parent, name) : NULL;
+
+  if (!name && PyObject_Hash(key) == -1) return NULL;
+
+  if (elem) {
+    return self->funcs->base.get_elem_wrapper(elem);
+  } else {
+    Py_INCREF(default_value);
+    return default_value;
+  }
+}
+
+static PyObject* PyUpb_ByNameMap_GetIter(PyObject* _self) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  return PyUpb_ByNameIterator_New(self->funcs, self->parent, self->parent_obj);
+}
+
+static PyObject* PyUpb_ByNameMap_Keys(PyObject* _self, PyObject* args) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  int n = self->funcs->base.get_elem_count(self->parent);
+  PyObject* ret = PyList_New(n);
+  if (!ret) return NULL;
+  for (int i = 0; i < n; i++) {
+    const void* elem = self->funcs->base.index(self->parent, i);
+    PyObject* key = PyUnicode_FromString(self->funcs->get_elem_name(elem));
+    if (!key) goto error;
+    PyList_SetItem(ret, i, key);
+  }
+  return ret;
+
+error:
+  Py_XDECREF(ret);
+  return NULL;
+}
+
+static PyObject* PyUpb_ByNameMap_Values(PyObject* _self, PyObject* args) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  int n = self->funcs->base.get_elem_count(self->parent);
+  PyObject* ret = PyList_New(n);
+  if (!ret) return NULL;
+  for (int i = 0; i < n; i++) {
+    const void* elem = self->funcs->base.index(self->parent, i);
+    PyObject* py_elem = self->funcs->base.get_elem_wrapper(elem);
+    if (!py_elem) goto error;
+    PyList_SetItem(ret, i, py_elem);
+  }
+  return ret;
+
+error:
+  Py_XDECREF(ret);
+  return NULL;
+}
+
+static PyObject* PyUpb_ByNameMap_Items(PyObject* _self, PyObject* args) {
+  PyUpb_ByNameMap* self = (PyUpb_ByNameMap*)_self;
+  int n = self->funcs->base.get_elem_count(self->parent);
+  PyObject* ret = PyList_New(n);
+  PyObject* item;
+  PyObject* py_elem;
+  if (!ret) return NULL;
+  for (int i = 0; i < n; i++) {
+    const void* elem = self->funcs->base.index(self->parent, i);
+    item = PyTuple_New(2);
+    py_elem = self->funcs->base.get_elem_wrapper(elem);
+    if (!item || !py_elem) goto error;
+    PyTuple_SetItem(item, 0,
+                    PyUnicode_FromString(self->funcs->get_elem_name(elem)));
+    PyTuple_SetItem(item, 1, py_elem);
+    PyList_SetItem(ret, i, item);
+  }
+  return ret;
+
+error:
+  Py_XDECREF(py_elem);
+  Py_XDECREF(item);
+  Py_XDECREF(ret);
+  return NULL;
+}
+
+// A mapping container can only be equal to another mapping container, or (for
+// backward compatibility) to a dict containing the same items.
+// Returns 1 if equal, 0 if unequal, -1 on error.
+static int PyUpb_ByNameMap_IsEqual(PyUpb_ByNameMap* self, PyObject* other) {
+  // Check the identity of C++ pointers.
+  if (PyObject_TypeCheck(other, Py_TYPE(self))) {
+    PyUpb_ByNameMap* other_map = (void*)other;
+    return self->parent == other_map->parent && self->funcs == other_map->funcs;
+  }
+
+  if (!PyDict_Check(other)) return 0;
+
+  PyObject* self_dict = PyDict_New();
+  PyDict_Merge(self_dict, (PyObject*)self, 0);
+  int eq = PyObject_RichCompareBool(self_dict, other, Py_EQ);
+  Py_DECREF(self_dict);
+  return eq;
+}
+
+static PyObject* PyUpb_ByNameMap_RichCompare(PyObject* _self, PyObject* other,
+                                             int opid) {
+  PyUpb_ByNameMap* self = PyUpb_ByNameMap_Self(_self);
+  if (opid != Py_EQ && opid != Py_NE) {
+    Py_RETURN_NOTIMPLEMENTED;
+  }
+  bool ret = PyUpb_ByNameMap_IsEqual(self, other);
+  if (opid == Py_NE) ret = !ret;
+  return PyBool_FromLong(ret);
+}
+
+static PyMethodDef PyUpb_ByNameMap_Methods[] = {
+    {"get", (PyCFunction)&PyUpb_ByNameMap_Get, METH_VARARGS},
+    {"keys", PyUpb_ByNameMap_Keys, METH_NOARGS},
+    {"values", PyUpb_ByNameMap_Values, METH_NOARGS},
+    {"items", PyUpb_ByNameMap_Items, METH_NOARGS},
+    {NULL}};
+
+static PyType_Slot PyUpb_ByNameMap_Slots[] = {
+    {Py_mp_ass_subscript, PyUpb_ByNameMap_AssignSubscript},
+    {Py_mp_length, PyUpb_ByNameMap_Length},
+    {Py_mp_subscript, PyUpb_ByNameMap_Subscript},
+    {Py_sq_contains, &PyUpb_ByNameMap_Contains},
+    {Py_tp_dealloc, &PyUpb_ByNameMap_Dealloc},
+    {Py_tp_iter, PyUpb_ByNameMap_GetIter},
+    {Py_tp_methods, &PyUpb_ByNameMap_Methods},
+    {Py_tp_repr, &PyUpb_DescriptorMap_Repr},
+    {Py_tp_richcompare, &PyUpb_ByNameMap_RichCompare},
+    {0, NULL},
+};
+
+static PyType_Spec PyUpb_ByNameMap_Spec = {
+    PYUPB_MODULE_NAME "._ByNameMap",  // tp_name
+    sizeof(PyUpb_ByNameMap),          // tp_basicsize
+    0,                                // tp_itemsize
+    Py_TPFLAGS_DEFAULT,               // tp_flags
+    PyUpb_ByNameMap_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// ByNumberMap
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  const PyUpb_ByNumberMap_Funcs* funcs;
+  const void* parent;    // upb_MessageDef*, upb_DefPool*, etc.
+  PyObject* parent_obj;  // Python object that keeps parent alive, we own a ref.
+} PyUpb_ByNumberMap;
+
+PyUpb_ByNumberMap* PyUpb_ByNumberMap_Self(PyObject* obj) {
+  assert(Py_TYPE(obj) == PyUpb_ModuleState_Get()->by_number_map_type);
+  return (PyUpb_ByNumberMap*)obj;
+}
+
+PyObject* PyUpb_ByNumberMap_New(const PyUpb_ByNumberMap_Funcs* funcs,
+                                const void* parent, PyObject* parent_obj) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  PyUpb_ByNumberMap* map = (void*)PyType_GenericAlloc(s->by_number_map_type, 0);
+  map->funcs = funcs;
+  map->parent = parent;
+  map->parent_obj = parent_obj;
+  Py_INCREF(parent_obj);
+  return &map->ob_base;
+}
+
+static void PyUpb_ByNumberMap_Dealloc(PyObject* _self) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  Py_DECREF(self->parent_obj);
+  PyUpb_Dealloc(self);
+}
+
+static Py_ssize_t PyUpb_ByNumberMap_Length(PyObject* _self) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  return self->funcs->base.get_elem_count(self->parent);
+}
+
+static const void* PyUpb_ByNumberMap_LookupHelper(PyUpb_ByNumberMap* self,
+                                                  PyObject* key) {
+  long num = PyLong_AsLong(key);
+  if (num == -1 && PyErr_Occurred()) {
+    PyErr_Clear();
+    // Ensure that the key is hashable (this will raise an error if not).
+    PyObject_Hash(key);
+    return NULL;
+  } else {
+    return self->funcs->lookup(self->parent, num);
+  }
+}
+
+static PyObject* PyUpb_ByNumberMap_Subscript(PyObject* _self, PyObject* key) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  const void* elem = PyUpb_ByNumberMap_LookupHelper(self, key);
+  if (elem) {
+    return self->funcs->base.get_elem_wrapper(elem);
+  } else {
+    if (!PyErr_Occurred()) {
+      PyErr_SetObject(PyExc_KeyError, key);
+    }
+    return NULL;
+  }
+}
+
+static int PyUpb_ByNumberMap_AssignSubscript(PyObject* self, PyObject* key,
+                                             PyObject* value) {
+  PyErr_Format(PyExc_TypeError, PYUPB_MODULE_NAME
+               ".ByNumberMap' object does not support item assignment");
+  return -1;
+}
+
+static PyObject* PyUpb_ByNumberMap_Get(PyObject* _self, PyObject* args) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  PyObject* key;
+  PyObject* default_value = Py_None;
+  if (!PyArg_UnpackTuple(args, "get", 1, 2, &key, &default_value)) {
+    return NULL;
+  }
+
+  const void* elem = PyUpb_ByNumberMap_LookupHelper(self, key);
+  if (elem) {
+    return self->funcs->base.get_elem_wrapper(elem);
+  } else if (PyErr_Occurred()) {
+    return NULL;
+  } else {
+    return PyUpb_NewRef(default_value);
+  }
+}
+
+static PyObject* PyUpb_ByNumberMap_GetIter(PyObject* _self) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  return PyUpb_ByNumberIterator_New(self->funcs, self->parent,
+                                    self->parent_obj);
+}
+
+static PyObject* PyUpb_ByNumberMap_Keys(PyObject* _self, PyObject* args) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  int n = self->funcs->base.get_elem_count(self->parent);
+  PyObject* ret = PyList_New(n);
+  if (!ret) return NULL;
+  for (int i = 0; i < n; i++) {
+    const void* elem = self->funcs->base.index(self->parent, i);
+    PyObject* key = PyLong_FromLong(self->funcs->get_elem_num(elem));
+    if (!key) goto error;
+    PyList_SetItem(ret, i, key);
+  }
+  return ret;
+
+error:
+  Py_XDECREF(ret);
+  return NULL;
+}
+
+static PyObject* PyUpb_ByNumberMap_Values(PyObject* _self, PyObject* args) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  int n = self->funcs->base.get_elem_count(self->parent);
+  PyObject* ret = PyList_New(n);
+  if (!ret) return NULL;
+  for (int i = 0; i < n; i++) {
+    const void* elem = self->funcs->base.index(self->parent, i);
+    PyObject* py_elem = self->funcs->base.get_elem_wrapper(elem);
+    if (!py_elem) goto error;
+    PyList_SetItem(ret, i, py_elem);
+  }
+  return ret;
+
+error:
+  Py_XDECREF(ret);
+  return NULL;
+}
+
+static PyObject* PyUpb_ByNumberMap_Items(PyObject* _self, PyObject* args) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  int n = self->funcs->base.get_elem_count(self->parent);
+  PyObject* ret = PyList_New(n);
+  PyObject* item;
+  PyObject* py_elem;
+  if (!ret) return NULL;
+  for (int i = 0; i < n; i++) {
+    const void* elem = self->funcs->base.index(self->parent, i);
+    int number = self->funcs->get_elem_num(elem);
+    item = PyTuple_New(2);
+    py_elem = self->funcs->base.get_elem_wrapper(elem);
+    if (!item || !py_elem) goto error;
+    PyTuple_SetItem(item, 0, PyLong_FromLong(number));
+    PyTuple_SetItem(item, 1, py_elem);
+    PyList_SetItem(ret, i, item);
+  }
+  return ret;
+
+error:
+  Py_XDECREF(py_elem);
+  Py_XDECREF(item);
+  Py_XDECREF(ret);
+  return NULL;
+}
+
+static int PyUpb_ByNumberMap_Contains(PyObject* _self, PyObject* key) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  const void* elem = PyUpb_ByNumberMap_LookupHelper(self, key);
+  if (elem) return 1;
+  if (PyErr_Occurred()) return -1;
+  return 0;
+}
+
+// A mapping container can only be equal to another mapping container, or (for
+// backward compatibility) to a dict containing the same items.
+// Returns 1 if equal, 0 if unequal, -1 on error.
+static int PyUpb_ByNumberMap_IsEqual(PyUpb_ByNumberMap* self, PyObject* other) {
+  // Check the identity of C++ pointers.
+  if (PyObject_TypeCheck(other, Py_TYPE(self))) {
+    PyUpb_ByNumberMap* other_map = (void*)other;
+    return self->parent == other_map->parent && self->funcs == other_map->funcs;
+  }
+
+  if (!PyDict_Check(other)) return 0;
+
+  PyObject* self_dict = PyDict_New();
+  PyDict_Merge(self_dict, (PyObject*)self, 0);
+  int eq = PyObject_RichCompareBool(self_dict, other, Py_EQ);
+  Py_DECREF(self_dict);
+  return eq;
+}
+
+static PyObject* PyUpb_ByNumberMap_RichCompare(PyObject* _self, PyObject* other,
+                                               int opid) {
+  PyUpb_ByNumberMap* self = PyUpb_ByNumberMap_Self(_self);
+  if (opid != Py_EQ && opid != Py_NE) {
+    Py_RETURN_NOTIMPLEMENTED;
+  }
+  bool ret = PyUpb_ByNumberMap_IsEqual(self, other);
+  if (opid == Py_NE) ret = !ret;
+  return PyBool_FromLong(ret);
+}
+
+static PyMethodDef PyUpb_ByNumberMap_Methods[] = {
+    {"get", (PyCFunction)&PyUpb_ByNumberMap_Get, METH_VARARGS},
+    {"keys", PyUpb_ByNumberMap_Keys, METH_NOARGS},
+    {"values", PyUpb_ByNumberMap_Values, METH_NOARGS},
+    {"items", PyUpb_ByNumberMap_Items, METH_NOARGS},
+    {NULL}};
+
+static PyType_Slot PyUpb_ByNumberMap_Slots[] = {
+    {Py_mp_ass_subscript, PyUpb_ByNumberMap_AssignSubscript},
+    {Py_mp_length, PyUpb_ByNumberMap_Length},
+    {Py_mp_subscript, PyUpb_ByNumberMap_Subscript},
+    {Py_sq_contains, &PyUpb_ByNumberMap_Contains},
+    {Py_tp_dealloc, &PyUpb_ByNumberMap_Dealloc},
+    {Py_tp_iter, PyUpb_ByNumberMap_GetIter},
+    {Py_tp_methods, &PyUpb_ByNumberMap_Methods},
+    {Py_tp_repr, &PyUpb_DescriptorMap_Repr},
+    {Py_tp_richcompare, &PyUpb_ByNumberMap_RichCompare},
+    {0, NULL},
+};
+
+static PyType_Spec PyUpb_ByNumberMap_Spec = {
+    PYUPB_MODULE_NAME "._ByNumberMap",  // tp_name
+    sizeof(PyUpb_ByNumberMap),          // tp_basicsize
+    0,                                  // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                 // tp_flags
+    PyUpb_ByNumberMap_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// Top Level
+// -----------------------------------------------------------------------------
+
+bool PyUpb_InitDescriptorContainers(PyObject* m) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_GetFromModule(m);
+
+  s->by_name_map_type = PyUpb_AddClass(m, &PyUpb_ByNameMap_Spec);
+  s->by_number_map_type = PyUpb_AddClass(m, &PyUpb_ByNumberMap_Spec);
+  s->by_name_iterator_type = PyUpb_AddClass(m, &PyUpb_ByNameIterator_Spec);
+  s->by_number_iterator_type = PyUpb_AddClass(m, &PyUpb_ByNumberIterator_Spec);
+  s->generic_sequence_type = PyUpb_AddClass(m, &PyUpb_GenericSequence_Spec);
+
+  return s->by_name_map_type && s->by_number_map_type &&
+         s->by_name_iterator_type && s->by_number_iterator_type &&
+         s->generic_sequence_type;
+}
diff --git a/python/descriptor_containers.h b/python/descriptor_containers.h
new file mode 100644
index 0000000..5b2b1fa
--- /dev/null
+++ b/python/descriptor_containers.h
@@ -0,0 +1,117 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_DESCRIPTOR_CONTAINERS_H__
+#define PYUPB_DESCRIPTOR_CONTAINERS_H__
+
+// This file defines immutable Python containiner types whose data comes from
+// an underlying descriptor (def).
+//
+// Because there are many instances of these types that vend different kinds of
+// data (fields, oneofs, enums, etc) these types accept a "vtable" of function
+// pointers. This saves us from having to define numerous distinct Python types
+// for each kind of data we want to vend.
+//
+// The underlying upb APIs follow a consistent pattern that allows us to use
+// those functions directly inside these vtables, greatly reducing the amount of
+// "adaptor" code we need to write.
+
+#include <stdbool.h>
+
+#include "protobuf.h"
+#include "upb/reflection/def.h"
+
+// -----------------------------------------------------------------------------
+// PyUpb_GenericSequence
+// -----------------------------------------------------------------------------
+
+// A Python object that vends a sequence of descriptors.
+
+typedef struct {
+  // Returns the number of elements in the map.
+  int (*get_elem_count)(const void* parent);
+  // Returns an element by index.
+  const void* (*index)(const void* parent, int idx);
+  // Returns a Python object wrapping this element, caller owns a ref.
+  PyObject* (*get_elem_wrapper)(const void* elem);
+} PyUpb_GenericSequence_Funcs;
+
+// Returns a new GenericSequence.  The vtable `funcs` must outlive this object
+// (generally it should be static).  The GenericSequence will take a ref on
+// `parent_obj`, which must be sufficient to keep `parent` alive.  The object
+// `parent` will be passed as an argument to the functions in `funcs`.
+PyObject* PyUpb_GenericSequence_New(const PyUpb_GenericSequence_Funcs* funcs,
+                                    const void* parent, PyObject* parent_obj);
+
+// -----------------------------------------------------------------------------
+// PyUpb_ByNameMap
+// -----------------------------------------------------------------------------
+
+// A Python object that vends a name->descriptor map.
+
+typedef struct {
+  PyUpb_GenericSequence_Funcs base;
+  // Looks up by name and returns either a pointer to the element or NULL.
+  const void* (*lookup)(const void* parent, const char* key);
+  // Returns the name associated with this element.
+  const char* (*get_elem_name)(const void* elem);
+} PyUpb_ByNameMap_Funcs;
+
+// Returns a new ByNameMap.  The vtable `funcs` must outlive this object
+// (generally it should be static).  The ByNameMap will take a ref on
+// `parent_obj`, which must be sufficient to keep `parent` alive.  The object
+// `parent` will be passed as an argument to the functions in `funcs`.
+PyObject* PyUpb_ByNameMap_New(const PyUpb_ByNameMap_Funcs* funcs,
+                              const void* parent, PyObject* parent_obj);
+
+// -----------------------------------------------------------------------------
+// PyUpb_ByNumberMap
+// -----------------------------------------------------------------------------
+
+// A Python object that vends a number->descriptor map.
+
+typedef struct {
+  PyUpb_GenericSequence_Funcs base;
+  // Looks up by name and returns either a pointer to the element or NULL.
+  const void* (*lookup)(const void* parent, int num);
+  // Returns the name associated with this element.
+  int (*get_elem_num)(const void* elem);
+} PyUpb_ByNumberMap_Funcs;
+
+// Returns a new ByNumberMap.  The vtable `funcs` must outlive this object
+// (generally it should be static).  The ByNumberMap will take a ref on
+// `parent_obj`, which must be sufficient to keep `parent` alive.  The object
+// `parent` will be passed as an argument to the functions in `funcs`.
+PyObject* PyUpb_ByNumberMap_New(const PyUpb_ByNumberMap_Funcs* funcs,
+                                const void* parent, PyObject* parent_obj);
+
+bool PyUpb_InitDescriptorContainers(PyObject* m);
+
+#endif  // PYUPB_DESCRIPTOR_CONTAINERS_H__
diff --git a/python/descriptor_pool.c b/python/descriptor_pool.c
new file mode 100644
index 0000000..ee41677
--- /dev/null
+++ b/python/descriptor_pool.c
@@ -0,0 +1,652 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/descriptor_pool.h"
+
+#include "google/protobuf/descriptor.upbdefs.h"
+#include "python/convert.h"
+#include "python/descriptor.h"
+#include "python/message.h"
+#include "python/protobuf.h"
+#include "upb/reflection/def.h"
+#include "upb/util/def_to_proto.h"
+
+// -----------------------------------------------------------------------------
+// DescriptorPool
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  upb_DefPool* symtab;
+  PyObject* db;  // The DescriptorDatabase underlying this pool.  May be NULL.
+} PyUpb_DescriptorPool;
+
+PyObject* PyUpb_DescriptorPool_GetDefaultPool(void) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  return s->default_pool;
+}
+
+const upb_MessageDef* PyUpb_DescriptorPool_GetFileProtoDef(void) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  if (!s->c_descriptor_symtab) {
+    s->c_descriptor_symtab = upb_DefPool_New();
+  }
+  return google_protobuf_FileDescriptorProto_getmsgdef(s->c_descriptor_symtab);
+}
+
+static PyObject* PyUpb_DescriptorPool_DoCreateWithCache(
+    PyTypeObject* type, PyObject* db, PyUpb_WeakMap* obj_cache) {
+  PyUpb_DescriptorPool* pool = (void*)PyType_GenericAlloc(type, 0);
+  pool->symtab = upb_DefPool_New();
+  pool->db = db;
+  Py_XINCREF(pool->db);
+  PyUpb_WeakMap_Add(obj_cache, pool->symtab, &pool->ob_base);
+  return &pool->ob_base;
+}
+
+static PyObject* PyUpb_DescriptorPool_DoCreate(PyTypeObject* type,
+                                               PyObject* db) {
+  return PyUpb_DescriptorPool_DoCreateWithCache(type, db,
+                                                PyUpb_ObjCache_Instance());
+}
+
+upb_DefPool* PyUpb_DescriptorPool_GetSymtab(PyObject* pool) {
+  return ((PyUpb_DescriptorPool*)pool)->symtab;
+}
+
+static int PyUpb_DescriptorPool_Traverse(PyUpb_DescriptorPool* self,
+                                         visitproc visit, void* arg) {
+  Py_VISIT(self->db);
+  return 0;
+}
+
+static int PyUpb_DescriptorPool_Clear(PyUpb_DescriptorPool* self) {
+  Py_CLEAR(self->db);
+  return 0;
+}
+
+PyObject* PyUpb_DescriptorPool_Get(const upb_DefPool* symtab) {
+  PyObject* pool = PyUpb_ObjCache_Get(symtab);
+  assert(pool);
+  return pool;
+}
+
+static void PyUpb_DescriptorPool_Dealloc(PyUpb_DescriptorPool* self) {
+  PyUpb_DescriptorPool_Clear(self);
+  upb_DefPool_Free(self->symtab);
+  PyUpb_ObjCache_Delete(self->symtab);
+  PyUpb_Dealloc(self);
+}
+
+/*
+ * DescriptorPool.__new__()
+ *
+ * Implements:
+ *   DescriptorPool(descriptor_db=None)
+ */
+static PyObject* PyUpb_DescriptorPool_New(PyTypeObject* type, PyObject* args,
+                                          PyObject* kwargs) {
+  char* kwlist[] = {"descriptor_db", 0};
+  PyObject* db = NULL;
+
+  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", kwlist, &db)) {
+    return NULL;
+  }
+
+  if (db == Py_None) db = NULL;
+  return PyUpb_DescriptorPool_DoCreate(type, db);
+}
+
+static PyObject* PyUpb_DescriptorPool_DoAdd(PyObject* _self,
+                                            PyObject* file_desc);
+
+static bool PyUpb_DescriptorPool_TryLoadFileProto(PyUpb_DescriptorPool* self,
+                                                  PyObject* proto) {
+  if (proto == NULL) {
+    if (PyErr_ExceptionMatches(PyExc_KeyError)) {
+      // Expected error: item was simply not found.
+      PyErr_Clear();
+      return true;  // We didn't accomplish our goal, but we didn't error out.
+    }
+    return false;
+  }
+  if (proto == Py_None) return true;
+  PyObject* ret = PyUpb_DescriptorPool_DoAdd((PyObject*)self, proto);
+  bool ok = ret != NULL;
+  Py_XDECREF(ret);
+  return ok;
+}
+
+static bool PyUpb_DescriptorPool_TryLoadSymbol(PyUpb_DescriptorPool* self,
+                                               PyObject* sym) {
+  if (!self->db) return false;
+  PyObject* file_proto =
+      PyObject_CallMethod(self->db, "FindFileContainingSymbol", "O", sym);
+  bool ret = PyUpb_DescriptorPool_TryLoadFileProto(self, file_proto);
+  Py_XDECREF(file_proto);
+  return ret;
+}
+
+static bool PyUpb_DescriptorPool_TryLoadFilename(PyUpb_DescriptorPool* self,
+                                                 PyObject* filename) {
+  if (!self->db) return false;
+  PyObject* file_proto =
+      PyObject_CallMethod(self->db, "FindFileByName", "O", filename);
+  bool ret = PyUpb_DescriptorPool_TryLoadFileProto(self, file_proto);
+  Py_XDECREF(file_proto);
+  return ret;
+}
+
+bool PyUpb_DescriptorPool_CheckNoDatabase(PyObject* _self) { return true; }
+
+static bool PyUpb_DescriptorPool_LoadDependentFiles(
+    PyUpb_DescriptorPool* self, google_protobuf_FileDescriptorProto* proto) {
+  size_t n;
+  const upb_StringView* deps =
+      google_protobuf_FileDescriptorProto_dependency(proto, &n);
+  for (size_t i = 0; i < n; i++) {
+    const upb_FileDef* dep = upb_DefPool_FindFileByNameWithSize(
+        self->symtab, deps[i].data, deps[i].size);
+    if (!dep) {
+      PyObject* filename =
+          PyUnicode_FromStringAndSize(deps[i].data, deps[i].size);
+      if (!filename) return false;
+      bool ok = PyUpb_DescriptorPool_TryLoadFilename(self, filename);
+      Py_DECREF(filename);
+      if (!ok) return false;
+    }
+  }
+  return true;
+}
+
+static PyObject* PyUpb_DescriptorPool_DoAddSerializedFile(
+    PyObject* _self, PyObject* serialized_pb) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+  upb_Arena* arena = upb_Arena_New();
+  if (!arena) PYUPB_RETURN_OOM;
+  PyObject* result = NULL;
+
+  char* buf;
+  Py_ssize_t size;
+  if (PyBytes_AsStringAndSize(serialized_pb, &buf, &size) < 0) {
+    goto done;
+  }
+
+  google_protobuf_FileDescriptorProto* proto =
+      google_protobuf_FileDescriptorProto_parse(buf, size, arena);
+  if (!proto) {
+    PyErr_SetString(PyExc_TypeError, "Couldn't parse file content!");
+    goto done;
+  }
+
+  upb_StringView name = google_protobuf_FileDescriptorProto_name(proto);
+  const upb_FileDef* file =
+      upb_DefPool_FindFileByNameWithSize(self->symtab, name.data, name.size);
+
+  if (file) {
+    // If the existing file is equal to the new file, then silently ignore the
+    // duplicate add.
+    google_protobuf_FileDescriptorProto* existing =
+        upb_FileDef_ToProto(file, arena);
+    if (!existing) {
+      PyErr_SetNone(PyExc_MemoryError);
+      goto done;
+    }
+    const upb_MessageDef* m = PyUpb_DescriptorPool_GetFileProtoDef();
+    if (upb_Message_IsEqual(proto, existing, m)) {
+      result = PyUpb_FileDescriptor_Get(file);
+      goto done;
+    }
+  }
+
+  if (self->db) {
+    if (!PyUpb_DescriptorPool_LoadDependentFiles(self, proto)) goto done;
+  }
+
+  upb_Status status;
+  upb_Status_Clear(&status);
+
+  const upb_FileDef* filedef =
+      upb_DefPool_AddFile(self->symtab, proto, &status);
+  if (!filedef) {
+    PyErr_Format(PyExc_TypeError,
+                 "Couldn't build proto file into descriptor pool: %s",
+                 upb_Status_ErrorMessage(&status));
+    goto done;
+  }
+
+  result = PyUpb_FileDescriptor_Get(filedef);
+
+done:
+  upb_Arena_Free(arena);
+  return result;
+}
+
+static PyObject* PyUpb_DescriptorPool_DoAdd(PyObject* _self,
+                                            PyObject* file_desc) {
+  if (!PyUpb_Message_Verify(file_desc)) return NULL;
+  const upb_MessageDef* m = PyUpb_Message_GetMsgdef(file_desc);
+  const char* file_proto_name =
+      PYUPB_DESCRIPTOR_PROTO_PACKAGE ".FileDescriptorProto";
+  if (strcmp(upb_MessageDef_FullName(m), file_proto_name) != 0) {
+    return PyErr_Format(PyExc_TypeError, "Can only add FileDescriptorProto");
+  }
+  PyObject* subargs = PyTuple_New(0);
+  if (!subargs) return NULL;
+  PyObject* serialized =
+      PyUpb_Message_SerializeToString(file_desc, subargs, NULL);
+  Py_DECREF(subargs);
+  if (!serialized) return NULL;
+  PyObject* ret = PyUpb_DescriptorPool_DoAddSerializedFile(_self, serialized);
+  Py_DECREF(serialized);
+  return ret;
+}
+
+/*
+ * PyUpb_DescriptorPool_AddSerializedFile()
+ *
+ * Implements:
+ *   DescriptorPool.AddSerializedFile(self, serialized_file_descriptor)
+ *
+ * Adds the given serialized FileDescriptorProto to the pool.
+ */
+static PyObject* PyUpb_DescriptorPool_AddSerializedFile(
+    PyObject* _self, PyObject* serialized_pb) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+  if (self->db) {
+    PyErr_SetString(
+        PyExc_ValueError,
+        "Cannot call AddSerializedFile on a DescriptorPool that uses a "
+        "DescriptorDatabase. Add your file to the underlying database.");
+    return false;
+  }
+  return PyUpb_DescriptorPool_DoAddSerializedFile(_self, serialized_pb);
+}
+
+static PyObject* PyUpb_DescriptorPool_Add(PyObject* _self,
+                                          PyObject* file_desc) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+  if (self->db) {
+    PyErr_SetString(
+        PyExc_ValueError,
+        "Cannot call Add on a DescriptorPool that uses a DescriptorDatabase. "
+        "Add your file to the underlying database.");
+    return false;
+  }
+  return PyUpb_DescriptorPool_DoAdd(_self, file_desc);
+}
+
+/*
+ * PyUpb_DescriptorPool_FindFileByName()
+ *
+ * Implements:
+ *   DescriptorPool.FindFileByName(self, name)
+ */
+static PyObject* PyUpb_DescriptorPool_FindFileByName(PyObject* _self,
+                                                     PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  const upb_FileDef* file = upb_DefPool_FindFileByName(self->symtab, name);
+  if (file == NULL && self->db) {
+    if (!PyUpb_DescriptorPool_TryLoadFilename(self, arg)) return NULL;
+    file = upb_DefPool_FindFileByName(self->symtab, name);
+  }
+  if (file == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find file %.200s", name);
+  }
+
+  return PyUpb_FileDescriptor_Get(file);
+}
+
+/*
+ * PyUpb_DescriptorPool_FindExtensionByName()
+ *
+ * Implements:
+ *   DescriptorPool.FindExtensionByName(self, name)
+ */
+static PyObject* PyUpb_DescriptorPool_FindExtensionByName(PyObject* _self,
+                                                          PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  const upb_FieldDef* field =
+      upb_DefPool_FindExtensionByName(self->symtab, name);
+  if (field == NULL && self->db) {
+    if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+    field = upb_DefPool_FindExtensionByName(self->symtab, name);
+  }
+  if (field == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find extension %.200s", name);
+  }
+
+  return PyUpb_FieldDescriptor_Get(field);
+}
+
+/*
+ * PyUpb_DescriptorPool_FindMessageTypeByName()
+ *
+ * Implements:
+ *   DescriptorPool.FindMessageTypeByName(self, name)
+ */
+static PyObject* PyUpb_DescriptorPool_FindMessageTypeByName(PyObject* _self,
+                                                            PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  const upb_MessageDef* m = upb_DefPool_FindMessageByName(self->symtab, name);
+  if (m == NULL && self->db) {
+    if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+    m = upb_DefPool_FindMessageByName(self->symtab, name);
+  }
+  if (m == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find message %.200s", name);
+  }
+
+  return PyUpb_Descriptor_Get(m);
+}
+
+// Splits a dotted symbol like foo.bar.baz on the last dot.  Returns the portion
+// after the last dot (baz) and updates `*parent_size` to the length of the
+// parent (foo.bar).  Returns NULL if no dots were present.
+static const char* PyUpb_DescriptorPool_SplitSymbolName(const char* sym,
+                                                        size_t* parent_size) {
+  const char* last_dot = strrchr(sym, '.');
+  if (!last_dot) return NULL;
+  *parent_size = last_dot - sym;
+  return last_dot + 1;
+}
+
+/*
+ * PyUpb_DescriptorPool_FindFieldByName()
+ *
+ * Implements:
+ *   DescriptorPool.FindFieldByName(self, name)
+ */
+static PyObject* PyUpb_DescriptorPool_FindFieldByName(PyObject* _self,
+                                                      PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  size_t parent_size;
+  const char* child = PyUpb_DescriptorPool_SplitSymbolName(name, &parent_size);
+  const upb_FieldDef* f = NULL;
+  if (child) {
+    const upb_MessageDef* parent =
+        upb_DefPool_FindMessageByNameWithSize(self->symtab, name, parent_size);
+    if (parent == NULL && self->db) {
+      if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+      parent = upb_DefPool_FindMessageByNameWithSize(self->symtab, name,
+                                                     parent_size);
+    }
+    if (parent) {
+      f = upb_MessageDef_FindFieldByName(parent, child);
+    }
+  }
+
+  if (!f) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find message %.200s", name);
+  }
+
+  return PyUpb_FieldDescriptor_Get(f);
+}
+
+/*
+ * PyUpb_DescriptorPool_FindEnumTypeByName()
+ *
+ * Implements:
+ *   DescriptorPool.FindEnumTypeByName(self, name)
+ */
+static PyObject* PyUpb_DescriptorPool_FindEnumTypeByName(PyObject* _self,
+                                                         PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  const upb_EnumDef* e = upb_DefPool_FindEnumByName(self->symtab, name);
+  if (e == NULL && self->db) {
+    if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+    e = upb_DefPool_FindEnumByName(self->symtab, name);
+  }
+  if (e == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find enum %.200s", name);
+  }
+
+  return PyUpb_EnumDescriptor_Get(e);
+}
+
+/*
+ * PyUpb_DescriptorPool_FindOneofByName()
+ *
+ * Implements:
+ *   DescriptorPool.FindOneofByName(self, name)
+ */
+static PyObject* PyUpb_DescriptorPool_FindOneofByName(PyObject* _self,
+                                                      PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  size_t parent_size;
+  const char* child = PyUpb_DescriptorPool_SplitSymbolName(name, &parent_size);
+
+  if (child) {
+    const upb_MessageDef* parent =
+        upb_DefPool_FindMessageByNameWithSize(self->symtab, name, parent_size);
+    if (parent == NULL && self->db) {
+      if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+      parent = upb_DefPool_FindMessageByNameWithSize(self->symtab, name,
+                                                     parent_size);
+    }
+    if (parent) {
+      const upb_OneofDef* o = upb_MessageDef_FindOneofByName(parent, child);
+      return PyUpb_OneofDescriptor_Get(o);
+    }
+  }
+
+  return PyErr_Format(PyExc_KeyError, "Couldn't find oneof %.200s", name);
+}
+
+static PyObject* PyUpb_DescriptorPool_FindServiceByName(PyObject* _self,
+                                                        PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  const upb_ServiceDef* s = upb_DefPool_FindServiceByName(self->symtab, name);
+  if (s == NULL && self->db) {
+    if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+    s = upb_DefPool_FindServiceByName(self->symtab, name);
+  }
+  if (s == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find service %.200s", name);
+  }
+
+  return PyUpb_ServiceDescriptor_Get(s);
+}
+
+static PyObject* PyUpb_DescriptorPool_FindMethodByName(PyObject* _self,
+                                                       PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+  size_t parent_size;
+  const char* child = PyUpb_DescriptorPool_SplitSymbolName(name, &parent_size);
+
+  if (!child) goto err;
+  const upb_ServiceDef* parent =
+      upb_DefPool_FindServiceByNameWithSize(self->symtab, name, parent_size);
+  if (parent == NULL && self->db) {
+    if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+    parent =
+        upb_DefPool_FindServiceByNameWithSize(self->symtab, name, parent_size);
+  }
+  if (!parent) goto err;
+  const upb_MethodDef* m = upb_ServiceDef_FindMethodByName(parent, child);
+  if (!m) goto err;
+  return PyUpb_MethodDescriptor_Get(m);
+
+err:
+  return PyErr_Format(PyExc_KeyError, "Couldn't find method %.200s", name);
+}
+
+static PyObject* PyUpb_DescriptorPool_FindFileContainingSymbol(PyObject* _self,
+                                                               PyObject* arg) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+
+  const char* name = PyUpb_VerifyStrData(arg);
+  if (!name) return NULL;
+
+  const upb_FileDef* f =
+      upb_DefPool_FindFileContainingSymbol(self->symtab, name);
+  if (f == NULL && self->db) {
+    if (!PyUpb_DescriptorPool_TryLoadSymbol(self, arg)) return NULL;
+    f = upb_DefPool_FindFileContainingSymbol(self->symtab, name);
+  }
+  if (f == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find symbol %.200s", name);
+  }
+
+  return PyUpb_FileDescriptor_Get(f);
+}
+
+static PyObject* PyUpb_DescriptorPool_FindExtensionByNumber(PyObject* _self,
+                                                            PyObject* args) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+  PyObject* message_descriptor;
+  int number;
+  if (!PyArg_ParseTuple(args, "Oi", &message_descriptor, &number)) {
+    return NULL;
+  }
+
+  const upb_FieldDef* f = upb_DefPool_FindExtensionByNumber(
+      self->symtab, PyUpb_Descriptor_GetDef(message_descriptor), number);
+  if (f == NULL) {
+    return PyErr_Format(PyExc_KeyError, "Couldn't find Extension %d", number);
+  }
+
+  return PyUpb_FieldDescriptor_Get(f);
+}
+
+static PyObject* PyUpb_DescriptorPool_FindAllExtensions(PyObject* _self,
+                                                        PyObject* msg_desc) {
+  PyUpb_DescriptorPool* self = (PyUpb_DescriptorPool*)_self;
+  const upb_MessageDef* m = PyUpb_Descriptor_GetDef(msg_desc);
+  size_t n;
+  const upb_FieldDef** ext = upb_DefPool_GetAllExtensions(self->symtab, m, &n);
+  PyObject* ret = PyList_New(n);
+  if (!ret) goto done;
+  for (size_t i = 0; i < n; i++) {
+    PyObject* field = PyUpb_FieldDescriptor_Get(ext[i]);
+    if (!field) {
+      Py_DECREF(ret);
+      ret = NULL;
+      goto done;
+    }
+    PyList_SetItem(ret, i, field);
+  }
+done:
+  free(ext);
+  return ret;
+}
+
+static PyMethodDef PyUpb_DescriptorPool_Methods[] = {
+    {"Add", PyUpb_DescriptorPool_Add, METH_O,
+     "Adds the FileDescriptorProto and its types to this pool."},
+    {"AddSerializedFile", PyUpb_DescriptorPool_AddSerializedFile, METH_O,
+     "Adds a serialized FileDescriptorProto to this pool."},
+    {"FindFileByName", PyUpb_DescriptorPool_FindFileByName, METH_O,
+     "Searches for a file descriptor by its .proto name."},
+    {"FindMessageTypeByName", PyUpb_DescriptorPool_FindMessageTypeByName,
+     METH_O, "Searches for a message descriptor by full name."},
+    {"FindFieldByName", PyUpb_DescriptorPool_FindFieldByName, METH_O,
+     "Searches for a field descriptor by full name."},
+    {"FindExtensionByName", PyUpb_DescriptorPool_FindExtensionByName, METH_O,
+     "Searches for extension descriptor by full name."},
+    {"FindEnumTypeByName", PyUpb_DescriptorPool_FindEnumTypeByName, METH_O,
+     "Searches for enum type descriptor by full name."},
+    {"FindOneofByName", PyUpb_DescriptorPool_FindOneofByName, METH_O,
+     "Searches for oneof descriptor by full name."},
+    {"FindServiceByName", PyUpb_DescriptorPool_FindServiceByName, METH_O,
+     "Searches for service descriptor by full name."},
+    {"FindMethodByName", PyUpb_DescriptorPool_FindMethodByName, METH_O,
+     "Searches for method descriptor by full name."},
+    {"FindFileContainingSymbol", PyUpb_DescriptorPool_FindFileContainingSymbol,
+     METH_O, "Gets the FileDescriptor containing the specified symbol."},
+    {"FindExtensionByNumber", PyUpb_DescriptorPool_FindExtensionByNumber,
+     METH_VARARGS, "Gets the extension descriptor for the given number."},
+    {"FindAllExtensions", PyUpb_DescriptorPool_FindAllExtensions, METH_O,
+     "Gets all known extensions of the given message descriptor."},
+    {NULL}};
+
+static PyType_Slot PyUpb_DescriptorPool_Slots[] = {
+    {Py_tp_clear, PyUpb_DescriptorPool_Clear},
+    {Py_tp_dealloc, PyUpb_DescriptorPool_Dealloc},
+    {Py_tp_methods, PyUpb_DescriptorPool_Methods},
+    {Py_tp_new, PyUpb_DescriptorPool_New},
+    {Py_tp_traverse, PyUpb_DescriptorPool_Traverse},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_DescriptorPool_Spec = {
+    PYUPB_MODULE_NAME ".DescriptorPool",
+    sizeof(PyUpb_DescriptorPool),
+    0,  // tp_itemsize
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
+    PyUpb_DescriptorPool_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// Top Level
+// -----------------------------------------------------------------------------
+
+bool PyUpb_InitDescriptorPool(PyObject* m) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_GetFromModule(m);
+  PyTypeObject* descriptor_pool_type =
+      PyUpb_AddClass(m, &PyUpb_DescriptorPool_Spec);
+
+  if (!descriptor_pool_type) return false;
+
+  state->default_pool = PyUpb_DescriptorPool_DoCreateWithCache(
+      descriptor_pool_type, NULL, state->obj_cache);
+  return state->default_pool &&
+         PyModule_AddObject(m, "default_pool", state->default_pool) == 0;
+}
diff --git a/python/descriptor_pool.h b/python/descriptor_pool.h
new file mode 100644
index 0000000..ae50ef0
--- /dev/null
+++ b/python/descriptor_pool.h
@@ -0,0 +1,51 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_DESCRIPTOR_POOL_H__
+#define PYUPB_DESCRIPTOR_POOL_H__
+
+#include <stdbool.h>
+
+#include "protobuf.h"
+
+// Returns a Python wrapper object for the given symtab. The symtab must have
+// been created from a Python DescriptorPool originally.
+PyObject* PyUpb_DescriptorPool_Get(const upb_DefPool* symtab);
+
+// Given a Python DescriptorPool, returns the underlying symtab.
+upb_DefPool* PyUpb_DescriptorPool_GetSymtab(PyObject* pool);
+
+// Returns the default DescriptorPool (a global singleton).
+PyObject* PyUpb_DescriptorPool_GetDefaultPool(void);
+
+// Module-level init.
+bool PyUpb_InitDescriptorPool(PyObject* m);
+
+#endif  // PYUPB_DESCRIPTOR_POOL_H__
diff --git a/python/dist/BUILD.bazel b/python/dist/BUILD.bazel
new file mode 100644
index 0000000..aa03fbe
--- /dev/null
+++ b/python/dist/BUILD.bazel
@@ -0,0 +1,453 @@
+# Copyright (c) 2009-2022, Google LLC
+# All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+load("@rules_pkg//:mappings.bzl", "pkg_files", "strip_prefix")
+load("@rules_pkg//:pkg.bzl", "pkg_tar")
+load("@rules_python//python:packaging.bzl", "py_wheel")
+load("@system_python//:version.bzl", "SYSTEM_PYTHON_VERSION")
+load("//:protobuf_version.bzl", "PROTOBUF_PYTHON_VERSION")
+load("//bazel:py_proto_library.bzl", "py_proto_library")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+load(":dist.bzl", "py_dist", "py_dist_module")
+
+licenses(["notice"])
+
+py_dist_module(
+    name = "message_mod",
+    extension = "//python:_message_binary",
+    module_name = "google._upb._message",
+)
+
+py_proto_library(
+    name = "well_known_proto_py_pb2",
+    deps = [
+        "//:any_proto",
+        "//:api_proto",
+        "//:descriptor_proto",
+        "//:duration_proto",
+        "//:empty_proto",
+        "//:field_mask_proto",
+        "//:source_context_proto",
+        "//:struct_proto",
+        "//:timestamp_proto",
+        "//:type_proto",
+        "//:wrappers_proto",
+    ],
+)
+
+py_proto_library(
+    name = "plugin_py_pb2",
+    deps = ["//:compiler_plugin_proto"],
+)
+
+config_setting(
+    name = "linux_aarch64_release",
+    flag_values = {
+        "//toolchain:release": "True",
+    },
+    values = {"cpu": "linux-aarch_64"},
+)
+
+config_setting(
+    name = "linux_aarch64_local",
+    constraint_values = [
+        "@platforms//os:linux",
+        "@platforms//cpu:aarch64",
+    ],
+    flag_values = {
+        "//toolchain:release": "False",
+    },
+)
+
+config_setting(
+    name = "linux_x86_64_release",
+    flag_values = {
+        "//toolchain:release": "True",
+    },
+    values = {"cpu": "linux-x86_64"},
+)
+
+config_setting(
+    name = "linux_x86_64_local",
+    constraint_values = [
+        "@platforms//os:linux",
+        "@platforms//cpu:x86_64",
+    ],
+    flag_values = {
+        "//toolchain:release": "False",
+    },
+)
+
+config_setting(
+    name = "osx_x86_64_release",
+    flag_values = {
+        "//toolchain:release": "True",
+    },
+    values = {"cpu": "osx-x86_64"},
+)
+
+config_setting(
+    name = "osx_x86_64_local",
+    constraint_values = [
+        "@platforms//os:osx",
+        "@platforms//cpu:x86_64",
+    ],
+    flag_values = {
+        "//toolchain:release": "False",
+    },
+)
+
+selects.config_setting_group(
+    name = "osx_x86_64",
+    match_any = [
+        ":osx_x86_64_release",
+        ":osx_x86_64_local",
+    ],
+)
+
+config_setting(
+    name = "osx_aarch64_release",
+    flag_values = {
+        "//toolchain:release": "True",
+    },
+    values = {"cpu": "osx-aarch_64"},
+)
+
+config_setting(
+    name = "osx_aarch64_local",
+    constraint_values = [
+        "@platforms//os:osx",
+        "@platforms//cpu:aarch64",
+    ],
+    flag_values = {
+        "//toolchain:release": "False",
+    },
+)
+
+selects.config_setting_group(
+    name = "osx_aarch64",
+    match_any = [
+        ":osx_aarch64_release",
+        ":osx_aarch64_local",
+    ],
+)
+
+config_setting(
+    name = "osx_universal2",
+    values = {"cpu": "osx-universal2"},
+)
+
+config_setting(
+    name = "windows_x86_32_release",
+    flag_values = {
+        "//toolchain:release": "True",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "windows_x86_32_local",
+    constraint_values = [
+        "@platforms//os:windows",
+        "@platforms//cpu:x86_32",
+    ],
+    flag_values = {
+        "//toolchain:release": "False",
+    },
+)
+
+selects.config_setting_group(
+    name = "windows_x86_32",
+    match_any = [
+        ":windows_x86_32_release",
+        ":windows_x86_32_local",
+    ],
+)
+
+config_setting(
+    name = "windows_x86_64_release",
+    flag_values = {
+        "//toolchain:release": "True",
+    },
+    values = {"cpu": "win64"},
+)
+
+config_setting(
+    name = "windows_x86_64_local",
+    constraint_values = [
+        "@platforms//os:windows",
+        "@platforms//cpu:x86_64",
+    ],
+    flag_values = {
+        "//toolchain:release": "False",
+    },
+)
+
+selects.config_setting_group(
+    name = "windows_x86_64",
+    match_any = [
+        ":windows_x86_64_release",
+        ":windows_x86_64_local",
+    ],
+)
+
+pkg_files(
+    name = "generated_wkt",
+    srcs = [
+        ":well_known_proto_py_pb2",
+        "//upb:descriptor_upb_minitable_proto",
+        "//upb:descriptor_upb_proto",
+        "//upb:descriptor_upb_proto_reflection",
+    ],
+    prefix = "google/protobuf",
+)
+
+pkg_files(
+    name = "generated_wkt_compiler",
+    srcs = [
+        ":plugin_py_pb2",
+    ],
+    prefix = "google/protobuf/compiler",
+)
+
+pkg_files(
+    name = "utf8_range_source_files",
+    srcs = ["@utf8_range//:utf8_range_srcs"],
+    prefix = "utf8_range",
+)
+
+pkg_files(
+    name = "dist_source_files",
+    srcs = [
+        "MANIFEST.in",
+        "setup.py",
+    ],
+)
+
+# Passing filegroups to pkg_tar directly results in incorrect
+# `protobuf/external/upb/` directory structure when built from the protobuf
+# repo. This can be removed once repositories are merged.
+pkg_files(
+    name = "filegroup_source_files",
+    srcs = [
+        "//:LICENSE",
+        "//python:message_srcs",
+        "//upb:source_files",
+        "//upb/base:source_files",
+        "//upb/collections:source_files",
+        "//upb/hash:source_files",
+        "//upb/lex:source_files",
+        "//upb/mem:source_files",
+        "//upb/message:source_files",
+        "//upb/mini_descriptor:source_files",
+        "//upb/mini_table:source_files",
+        "//upb/port:source_files",
+        "//upb/text:source_files",
+        "//upb/util:source_files",
+        "//upb/wire:source_files",
+    ],
+    strip_prefix = strip_prefix.from_root(""),
+)
+
+# NOTE: This package currently only works for macos and ubuntu, MSVC users
+# should use a binary wheel.
+pkg_tar(
+    name = "source_tarball",
+    srcs = [
+        ":dist_source_files",
+        ":filegroup_source_files",
+        ":generated_wkt",
+        ":generated_wkt_compiler",
+        ":utf8_range_source_files",
+        "//python:python_source_files",
+    ],
+    extension = "tar.gz",
+    package_dir = "protobuf",
+    package_file_name = "protobuf.tar.gz",
+    strip_prefix = ".",
+    target_compatible_with = select({
+        "@system_python//:none": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+)
+
+genrule(
+    name = "source_wheel",
+    srcs = [":source_tarball"],
+    outs = ["protobuf-%s.tar.gz" % PROTOBUF_PYTHON_VERSION],
+    cmd = """
+        set -eux
+        tar -xzvf $(location :source_tarball)
+        cd protobuf/
+        python3 setup.py sdist
+        cd ..
+        mv protobuf/dist/*.tar.gz $@
+    """,
+    target_compatible_with = select({
+        "@system_python//:none": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+)
+
+py_wheel(
+    name = "binary_wheel",
+    abi = select({
+        "//python:full_api_3.7": "cp37m",
+        "//python:full_api_3.8": "cp38",
+        "//python:full_api_3.9": "cp39",
+        "//conditions:default": "abi3",
+    }),
+    author = "protobuf@googlegroups.com",
+    author_email = "protobuf@googlegroups.com",
+    classifiers = [
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+    ],
+    distribution = "protobuf",
+    extra_distinfo_files = {
+        "//:LICENSE": "LICENSE",
+    },
+    homepage = "https://developers.google.com/protocol-buffers/",
+    license = "3-Clause BSD License",
+    platform = select({
+        ":linux_x86_64_local": "linux_x86_64",
+        ":linux_x86_64_release": "manylinux2014_x86_64",
+        ":linux_aarch64_local": "linux_aarch64",
+        ":linux_aarch64_release": "manylinux2014_aarch64",
+        ":osx_universal2": "macosx_10_9_universal2",
+        ":osx_aarch64": "macosx_11_0_arm64",
+        ":windows_x86_32": "win32",
+        ":windows_x86_64": "win_amd64",
+        "//conditions:default": "any",
+    }),
+    python_requires = ">=3.7",
+    python_tag = selects.with_or({
+        ("//python:limited_api_3.7", "//python:full_api_3.7"): "cp37",
+        "//python:full_api_3.8": "cp38",
+        "//python:full_api_3.9": "cp39",
+        "//python:limited_api_3.10": "cp310",
+        "//conditions:default": "cp" + SYSTEM_PYTHON_VERSION,
+    }),
+    strip_path_prefixes = [
+        "python/dist/",
+        "python/",
+        "src/",
+    ],
+    target_compatible_with = select({
+        "@system_python//:none": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+    version = PROTOBUF_PYTHON_VERSION,
+    deps = [
+        ":message_mod",
+        ":plugin_py_pb2",
+        ":well_known_proto_py_pb2",
+        "//:python_srcs",
+    ],
+)
+
+py_wheel(
+    name = "pure_python_wheel",
+    abi = "none",
+    author = "protobuf@googlegroups.com",
+    author_email = "protobuf@googlegroups.com",
+    classifiers = [
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+    ],
+    distribution = "protobuf",
+    extra_distinfo_files = {
+        "//:LICENSE": "LICENSE",
+    },
+    homepage = "https://developers.google.com/protocol-buffers/",
+    license = "3-Clause BSD License",
+    platform = "any",
+    python_requires = ">=3.7",
+    python_tag = "py3",
+    strip_path_prefixes = [
+        "python/",
+        "src/",
+    ],
+    target_compatible_with = select({
+        "@system_python//:none": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+    version = PROTOBUF_PYTHON_VERSION,
+    deps = [
+        ":plugin_py_pb2",
+        ":well_known_proto_py_pb2",
+        "//:python_srcs",
+    ],
+)
+
+py_wheel(
+    name = "test_wheel",
+    testonly = True,
+    abi = "none",
+    distribution = "protobuftests",
+    extra_distinfo_files = {
+        "//:LICENSE": "LICENSE",
+    },
+    platform = "any",
+    python_tag = "py3",
+    strip_path_prefixes = [
+        "python/",
+        "src/",
+    ],
+    target_compatible_with = select({
+        "@system_python//:none": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+    version = PROTOBUF_PYTHON_VERSION,
+    deps = [
+        "//:python_common_test_protos",
+        "//:python_specific_test_protos",
+        "//:python_test_srcs",
+        "//python/pb_unit_tests:test_files",
+        "//src/google/protobuf:testdata",
+    ],
+)
+
+py_dist(
+    name = "dist",
+    binary_wheel = ":binary_wheel",
+    full_api_cpus = [
+        # TODO: fix win32 build
+        "win32",
+        "win64",
+    ],
+    # Windows needs version-specific wheels until 3.10.
+    full_api_versions = [
+        "37",
+        "38",
+        "39",
+    ],
+    # Limited API: these wheels will satisfy any Python version >= the
+    # given version.
+    #
+    # Technically the limited API doesn't have the functions we need until
+    # 3.10, but on Linux we can get away with using 3.7 (see ../python_api.h for
+    # details).
+    limited_api_wheels = {
+        # TODO: fix win32 build
+        "win32": "310",
+        "win64": "310",
+        "linux-x86_64": "37",
+        "linux-aarch_64": "37",
+        "osx-universal2": "37",
+    },
+    pure_python_wheel = ":pure_python_wheel",
+    tags = ["manual"],
+)
diff --git a/python/dist/MANIFEST.in b/python/dist/MANIFEST.in
new file mode 100644
index 0000000..1b61936
--- /dev/null
+++ b/python/dist/MANIFEST.in
@@ -0,0 +1,2 @@
+global-include *.h
+global-include *.inc
\ No newline at end of file
diff --git a/python/dist/dist.bzl b/python/dist/dist.bzl
new file mode 100644
index 0000000..75c21c3
--- /dev/null
+++ b/python/dist/dist.bzl
@@ -0,0 +1,193 @@
+"""Rules to create python distribution files and properly name them"""
+
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
+load("@system_python//:version.bzl", "SYSTEM_PYTHON_VERSION")
+
+def _get_suffix(limited_api, python_version, cpu):
+    """Computes an ABI version tag for an extension module per PEP 3149."""
+    if "win32" in cpu or "win64" in cpu:
+        if limited_api:
+            return ".pyd"
+        if "win32" in cpu:
+            abi = "win32"
+        elif "win64" in cpu:
+            abi = "win_amd64"
+        else:
+            fail("Unsupported CPU: " + cpu)
+        return ".cp{}-{}.{}".format(python_version, abi, "pyd")
+
+    if python_version == "system":
+        python_version = SYSTEM_PYTHON_VERSION
+        if int(python_version) < 38:
+            python_version += "m"
+        abis = {
+            "darwin_arm64": "darwin",
+            "darwin_x86_64": "darwin",
+            "darwin": "darwin",
+            "osx-x86_64": "darwin",
+            "osx-aarch_64": "darwin",
+            "linux-aarch_64": "aarch64-linux-gnu",
+            "linux-x86_64": "x86_64-linux-gnu",
+            "k8": "x86_64-linux-gnu",
+        }
+
+        return ".cpython-{}-{}.{}".format(
+            python_version,
+            abis[cpu],
+            "so" if limited_api else "abi3.so",
+        )
+    elif limited_api:
+        return ".abi3.so"
+
+    fail("Unsupported combination of flags")
+
+def _declare_module_file(ctx, module_name, python_version, limited_api):
+    """Declares an output file for a Python module with this name, version, and limited api."""
+    base_filename = module_name.replace(".", "/")
+    suffix = _get_suffix(
+        python_version = python_version,
+        limited_api = limited_api,
+        cpu = ctx.var["TARGET_CPU"],
+    )
+    filename = base_filename + suffix
+    return ctx.actions.declare_file(filename)
+
+# --------------------------------------------------------------------------------------------------
+# py_dist_module()
+#
+# Creates a Python binary extension module that is ready for distribution.
+#
+#   py_dist_module(
+#       name = "message_mod",
+#       extension = "//python:_message_binary",
+#       module_name = "google._upb._message",
+#   )
+#
+# In the simple case, this simply involves copying the input file to the proper filename for
+# our current configuration (module_name, cpu, python_version, limited_abi).
+#
+# For multiarch platforms (osx-universal2), we must combine binaries for multiple architectures
+# into a single output binary using the "llvm-lipo" tool.  A config transition depends on multiple
+# architectures to get us the input files we need.
+
+def _py_multiarch_transition_impl(settings, attr):
+    if settings["//command_line_option:cpu"] == "osx-universal2":
+        return [{"//command_line_option:cpu": cpu} for cpu in ["osx-aarch_64", "osx-x86_64"]]
+    else:
+        return settings
+
+_py_multiarch_transition = transition(
+    implementation = _py_multiarch_transition_impl,
+    inputs = ["//command_line_option:cpu"],
+    outputs = ["//command_line_option:cpu"],
+)
+
+def _py_dist_module_impl(ctx):
+    output_file = _declare_module_file(
+        ctx = ctx,
+        module_name = ctx.attr.module_name,
+        python_version = ctx.attr._python_version[BuildSettingInfo].value,
+        limited_api = ctx.attr._limited_api[BuildSettingInfo].value,
+    )
+    if len(ctx.attr.extension) == 1:
+        src = ctx.attr.extension[0][DefaultInfo].files.to_list()[0]
+        ctx.actions.run(
+            executable = "cp",
+            arguments = [src.path, output_file.path],
+            inputs = [src],
+            outputs = [output_file],
+        )
+        return [
+            DefaultInfo(files = depset([output_file])),
+        ]
+    else:
+        srcs = [mod[DefaultInfo].files.to_list()[0] for mod in ctx.attr.extension]
+        ctx.actions.run(
+            executable = "/usr/local/bin/llvm-lipo",
+            arguments = ["-create", "-output", output_file.path] + [src.path for src in srcs],
+            inputs = srcs,
+            outputs = [output_file],
+        )
+        return [
+            DefaultInfo(files = depset([output_file])),
+        ]
+
+py_dist_module = rule(
+    output_to_genfiles = True,
+    implementation = _py_dist_module_impl,
+    attrs = {
+        "module_name": attr.string(mandatory = True),
+        "extension": attr.label(
+            mandatory = True,
+            cfg = _py_multiarch_transition,
+        ),
+        "_limited_api": attr.label(default = "//python:limited_api"),
+        "_python_version": attr.label(default = "//python:python_version"),
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
+    },
+)
+
+# --------------------------------------------------------------------------------------------------
+# py_dist()
+#
+# A rule that builds a collection of binary wheels, using transitions to depend on many different
+# python versions and cpus.
+
+def _py_dist_transition_impl(settings, attr):
+    _ignore = (settings)  # @unused
+    transitions = []
+
+    for cpu, version in attr.limited_api_wheels.items():
+        transitions.append({
+            "//command_line_option:cpu": cpu,
+            "//python:python_version": version,
+            "//python:limited_api": True,
+        })
+
+    for version in attr.full_api_versions:
+        for cpu in attr.full_api_cpus:
+            transitions.append({
+                "//command_line_option:cpu": cpu,
+                "//python:python_version": version,
+                "//python:limited_api": False,
+            })
+
+    return transitions
+
+_py_dist_transition = transition(
+    implementation = _py_dist_transition_impl,
+    inputs = [],
+    outputs = [
+        "//command_line_option:cpu",
+        "//python:python_version",
+        "//python:limited_api",
+    ],
+)
+
+def _py_dist_impl(ctx):
+    binary_files = [dep[DefaultInfo].files for dep in ctx.attr.binary_wheel]
+    pure_python_files = [ctx.attr.pure_python_wheel[DefaultInfo].files]
+    return [
+        DefaultInfo(files = depset(
+            transitive = binary_files + pure_python_files,
+        )),
+    ]
+
+py_dist = rule(
+    implementation = _py_dist_impl,
+    attrs = {
+        "binary_wheel": attr.label(
+            mandatory = True,
+            cfg = _py_dist_transition,
+        ),
+        "pure_python_wheel": attr.label(mandatory = True),
+        "limited_api_wheels": attr.string_dict(),
+        "full_api_versions": attr.string_list(),
+        "full_api_cpus": attr.string_list(),
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
+    },
+)
diff --git a/python/dist/setup.py b/python/dist/setup.py
new file mode 100755
index 0000000..b4fc21a
--- /dev/null
+++ b/python/dist/setup.py
@@ -0,0 +1,80 @@
+#! /usr/bin/env python
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+#
+# See README for usage instructions.
+
+import glob
+import os
+import sys
+import sysconfig
+
+# We must use setuptools, not distutils, because we need to use the
+# namespace_packages option for the "google" package.
+from setuptools import setup, Extension, find_packages
+
+
+def GetVersion():
+  """Reads and returns the version from google/protobuf/__init__.py.
+
+  Do not import google.protobuf.__init__ directly, because an installed
+  protobuf library may be loaded instead.
+
+  Returns:
+      The version.
+  """
+
+  with open(os.path.join('google', 'protobuf', '__init__.py')) as version_file:
+    file_globals = {}
+    exec(version_file.read(), file_globals)  # pylint:disable=exec-used
+    return file_globals["__version__"]
+
+
+current_dir = os.path.dirname(os.path.abspath(__file__))
+extra_link_args = []
+
+if sys.platform.startswith('win'):
+  extra_link_args = ['-static']
+
+setup(
+    name='protobuf',
+    version=GetVersion(),
+    description='Protocol Buffers',
+    download_url='https://github.com/protocolbuffers/protobuf/releases',
+    long_description="Protocol Buffers are Google's data interchange format",
+    url='https://developers.google.com/protocol-buffers/',
+    project_urls={
+        'Source': 'https://github.com/protocolbuffers/protobuf',
+    },
+    maintainer='protobuf@googlegroups.com',
+    maintainer_email='protobuf@googlegroups.com',
+    license='BSD-3-Clause',
+    classifiers=[
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
+    ],
+    namespace_packages=['google'],
+    packages=find_packages(),
+    install_requires=[],
+    ext_modules=[
+        Extension(
+            'google._upb._message',
+            glob.glob('google/protobuf/*.c')
+            + glob.glob('python/*.c')
+            + glob.glob('upb/**/*.c', recursive=True)
+            + glob.glob('utf8_range/*.c'),
+            include_dirs=[current_dir, os.path.join(current_dir, 'utf8_range')],
+            language='c',
+            extra_link_args=extra_link_args,
+        )
+    ],
+    python_requires='>=3.7',
+)
diff --git a/python/extension_dict.c b/python/extension_dict.c
new file mode 100644
index 0000000..d4b4dda
--- /dev/null
+++ b/python/extension_dict.c
@@ -0,0 +1,256 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/extension_dict.h"
+
+#include "python/message.h"
+#include "python/protobuf.h"
+#include "upb/reflection/def.h"
+
+// -----------------------------------------------------------------------------
+// ExtensionDict
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  PyObject* msg;  // Owning ref to our parent pessage.
+} PyUpb_ExtensionDict;
+
+PyObject* PyUpb_ExtensionDict_New(PyObject* msg) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyUpb_ExtensionDict* ext_dict =
+      (void*)PyType_GenericAlloc(state->extension_dict_type, 0);
+  ext_dict->msg = msg;
+  Py_INCREF(ext_dict->msg);
+  return &ext_dict->ob_base;
+}
+
+static PyObject* PyUpb_ExtensionDict_FindExtensionByName(PyObject* _self,
+                                                         PyObject* key) {
+  PyUpb_ExtensionDict* self = (PyUpb_ExtensionDict*)_self;
+  const char* name = PyUpb_GetStrData(key);
+  if (!name) {
+    PyErr_Format(PyExc_TypeError, "_FindExtensionByName expect a str");
+    return NULL;
+  }
+  const upb_MessageDef* m = PyUpb_Message_GetMsgdef(self->msg);
+  const upb_FileDef* file = upb_MessageDef_File(m);
+  const upb_DefPool* symtab = upb_FileDef_Pool(file);
+  const upb_FieldDef* ext = upb_DefPool_FindExtensionByName(symtab, name);
+  if (ext) {
+    return PyUpb_FieldDescriptor_Get(ext);
+  } else {
+    Py_RETURN_NONE;
+  }
+}
+
+static PyObject* PyUpb_ExtensionDict_FindExtensionByNumber(PyObject* _self,
+                                                           PyObject* arg) {
+  PyUpb_ExtensionDict* self = (PyUpb_ExtensionDict*)_self;
+  const upb_MessageDef* m = PyUpb_Message_GetMsgdef(self->msg);
+  const upb_MiniTable* l = upb_MessageDef_MiniTable(m);
+  const upb_FileDef* file = upb_MessageDef_File(m);
+  const upb_DefPool* symtab = upb_FileDef_Pool(file);
+  const upb_ExtensionRegistry* reg = upb_DefPool_ExtensionRegistry(symtab);
+  int64_t number = PyLong_AsLong(arg);
+  if (number == -1 && PyErr_Occurred()) return NULL;
+  const upb_MiniTableExtension* ext =
+      (upb_MiniTableExtension*)upb_ExtensionRegistry_Lookup(reg, l, number);
+  if (ext) {
+    const upb_FieldDef* f = upb_DefPool_FindExtensionByMiniTable(symtab, ext);
+    return PyUpb_FieldDescriptor_Get(f);
+  } else {
+    Py_RETURN_NONE;
+  }
+}
+
+static void PyUpb_ExtensionDict_Dealloc(PyUpb_ExtensionDict* self) {
+  PyUpb_Message_ClearExtensionDict(self->msg);
+  Py_DECREF(self->msg);
+  PyUpb_Dealloc(self);
+}
+
+static PyObject* PyUpb_ExtensionDict_RichCompare(PyObject* _self,
+                                                 PyObject* _other, int opid) {
+  // Only equality comparisons are implemented.
+  if (opid != Py_EQ && opid != Py_NE) {
+    Py_INCREF(Py_NotImplemented);
+    return Py_NotImplemented;
+  }
+  PyUpb_ExtensionDict* self = (PyUpb_ExtensionDict*)_self;
+  bool equals = false;
+  if (PyObject_TypeCheck(_other, Py_TYPE(_self))) {
+    PyUpb_ExtensionDict* other = (PyUpb_ExtensionDict*)_other;
+    equals = self->msg == other->msg;
+  }
+  bool ret = opid == Py_EQ ? equals : !equals;
+  return PyBool_FromLong(ret);
+}
+
+static int PyUpb_ExtensionDict_Contains(PyObject* _self, PyObject* key) {
+  PyUpb_ExtensionDict* self = (PyUpb_ExtensionDict*)_self;
+  const upb_FieldDef* f = PyUpb_Message_GetExtensionDef(self->msg, key);
+  if (!f) return -1;
+  upb_Message* msg = PyUpb_Message_GetIfReified(self->msg);
+  if (!msg) return 0;
+  if (upb_FieldDef_IsRepeated(f)) {
+    upb_MessageValue val = upb_Message_GetFieldByDef(msg, f);
+    return upb_Array_Size(val.array_val) > 0;
+  } else {
+    return upb_Message_HasFieldByDef(msg, f);
+  }
+}
+
+static Py_ssize_t PyUpb_ExtensionDict_Length(PyObject* _self) {
+  PyUpb_ExtensionDict* self = (PyUpb_ExtensionDict*)_self;
+  upb_Message* msg = PyUpb_Message_GetIfReified(self->msg);
+  return msg ? upb_Message_ExtensionCount(msg) : 0;
+}
+
+static PyObject* PyUpb_ExtensionDict_Subscript(PyObject* _self, PyObject* key) {
+  PyUpb_ExtensionDict* self = (PyUpb_ExtensionDict*)_self;
+  const upb_FieldDef* f = PyUpb_Message_GetExtensionDef(self->msg, key);
+  if (!f) return NULL;
+  return PyUpb_Message_GetFieldValue(self->msg, f);
+}
+
+static int PyUpb_ExtensionDict_AssignSubscript(PyObject* _self, PyObject* key,
+                                               PyObject* val) {
+  PyUpb_ExtensionDict* self = (PyUpb_ExtensionDict*)_self;
+  const upb_FieldDef* f = PyUpb_Message_GetExtensionDef(self->msg, key);
+  if (!f) return -1;
+  if (val) {
+    return PyUpb_Message_SetFieldValue(self->msg, f, val, PyExc_TypeError);
+  } else {
+    PyUpb_Message_DoClearField(self->msg, f);
+    return 0;
+  }
+}
+
+static PyObject* PyUpb_ExtensionIterator_New(PyObject* _ext_dict);
+
+static PyMethodDef PyUpb_ExtensionDict_Methods[] = {
+    {"_FindExtensionByName", PyUpb_ExtensionDict_FindExtensionByName, METH_O,
+     "Finds an extension by name."},
+    {"_FindExtensionByNumber", PyUpb_ExtensionDict_FindExtensionByNumber,
+     METH_O, "Finds an extension by number."},
+    {NULL, NULL},
+};
+
+static PyType_Slot PyUpb_ExtensionDict_Slots[] = {
+    {Py_tp_dealloc, PyUpb_ExtensionDict_Dealloc},
+    {Py_tp_methods, PyUpb_ExtensionDict_Methods},
+    //{Py_tp_getset, PyUpb_ExtensionDict_Getters},
+    //{Py_tp_hash, PyObject_HashNotImplemented},
+    {Py_tp_richcompare, PyUpb_ExtensionDict_RichCompare},
+    {Py_tp_iter, PyUpb_ExtensionIterator_New},
+    {Py_sq_contains, PyUpb_ExtensionDict_Contains},
+    {Py_sq_length, PyUpb_ExtensionDict_Length},
+    {Py_mp_length, PyUpb_ExtensionDict_Length},
+    {Py_mp_subscript, PyUpb_ExtensionDict_Subscript},
+    {Py_mp_ass_subscript, PyUpb_ExtensionDict_AssignSubscript},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_ExtensionDict_Spec = {
+    PYUPB_MODULE_NAME ".ExtensionDict",  // tp_name
+    sizeof(PyUpb_ExtensionDict),         // tp_basicsize
+    0,                                   // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                  // tp_flags
+    PyUpb_ExtensionDict_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// ExtensionIterator
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  PyObject* msg;
+  size_t iter;
+} PyUpb_ExtensionIterator;
+
+static PyObject* PyUpb_ExtensionIterator_New(PyObject* _ext_dict) {
+  PyUpb_ExtensionDict* ext_dict = (PyUpb_ExtensionDict*)_ext_dict;
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyUpb_ExtensionIterator* iter =
+      (void*)PyType_GenericAlloc(state->extension_iterator_type, 0);
+  if (!iter) return NULL;
+  iter->msg = ext_dict->msg;
+  iter->iter = kUpb_Message_Begin;
+  Py_INCREF(iter->msg);
+  return &iter->ob_base;
+}
+
+static void PyUpb_ExtensionIterator_Dealloc(void* _self) {
+  PyUpb_ExtensionIterator* self = (PyUpb_ExtensionIterator*)_self;
+  Py_DECREF(self->msg);
+  PyUpb_Dealloc(_self);
+}
+
+PyObject* PyUpb_ExtensionIterator_IterNext(PyObject* _self) {
+  PyUpb_ExtensionIterator* self = (PyUpb_ExtensionIterator*)_self;
+  upb_Message* msg = PyUpb_Message_GetIfReified(self->msg);
+  if (!msg) return NULL;
+  const upb_MessageDef* m = PyUpb_Message_GetMsgdef(self->msg);
+  const upb_DefPool* symtab = upb_FileDef_Pool(upb_MessageDef_File(m));
+  while (true) {
+    const upb_FieldDef* f;
+    upb_MessageValue val;
+    if (!upb_Message_Next(msg, m, symtab, &f, &val, &self->iter)) return NULL;
+    if (upb_FieldDef_IsExtension(f)) return PyUpb_FieldDescriptor_Get(f);
+  }
+}
+
+static PyType_Slot PyUpb_ExtensionIterator_Slots[] = {
+    {Py_tp_dealloc, PyUpb_ExtensionIterator_Dealloc},
+    {Py_tp_iter, PyObject_SelfIter},
+    {Py_tp_iternext, PyUpb_ExtensionIterator_IterNext},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_ExtensionIterator_Spec = {
+    PYUPB_MODULE_NAME ".ExtensionIterator",  // tp_name
+    sizeof(PyUpb_ExtensionIterator),         // tp_basicsize
+    0,                                       // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                      // tp_flags
+    PyUpb_ExtensionIterator_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// Top Level
+// -----------------------------------------------------------------------------
+
+bool PyUpb_InitExtensionDict(PyObject* m) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_GetFromModule(m);
+
+  s->extension_dict_type = PyUpb_AddClass(m, &PyUpb_ExtensionDict_Spec);
+  s->extension_iterator_type = PyUpb_AddClass(m, &PyUpb_ExtensionIterator_Spec);
+
+  return s->extension_dict_type && s->extension_iterator_type;
+}
diff --git a/python/extension_dict.h b/python/extension_dict.h
new file mode 100644
index 0000000..99d2add
--- /dev/null
+++ b/python/extension_dict.h
@@ -0,0 +1,42 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_EXTENSION_DICT_H__
+#define PYUPB_EXTENSION_DICT_H__
+
+#include <stdbool.h>
+
+#include "python/python_api.h"
+
+PyObject* PyUpb_ExtensionDict_New(PyObject* msg);
+
+bool PyUpb_InitExtensionDict(PyObject* m);
+
+#endif  // PYUPB_EXTENSION_DICT_H__
diff --git a/python/google/protobuf/internal/numpy/BUILD.bazel b/python/google/protobuf/internal/numpy/BUILD.bazel
index 5c1c166..ffdc4e5 100644
--- a/python/google/protobuf/internal/numpy/BUILD.bazel
+++ b/python/google/protobuf/internal/numpy/BUILD.bazel
@@ -6,18 +6,18 @@
 
 # TODO: b/278896688 - Remove this target and replace with py_library
 exports_files([
-  "__init__.py",
-  "numpy_test.py",
+    "__init__.py",
+    "numpy_test.py",
 ])
 
 internal_py_test(
     name = "numpy_test",
     srcs = ["numpy_test.py"],
+    visibility = [
+        "//python:__pkg__",
+        "//python/pb_unit_tests:__pkg__",
+    ],
     deps = [
         requirement("numpy"),
     ],
-    visibility = [
-      "//python:__pkg__",
-      "//upb/python/pb_unit_tests:__pkg__",
-    ]
 )
diff --git a/python/map.c b/python/map.c
new file mode 100644
index 0000000..bd9022d
--- /dev/null
+++ b/python/map.c
@@ -0,0 +1,529 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/map.h"
+
+#include "python/convert.h"
+#include "python/message.h"
+#include "python/protobuf.h"
+#include "upb/collections/map.h"
+#include "upb/reflection/def.h"
+
+// -----------------------------------------------------------------------------
+// MapContainer
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  PyObject* arena;
+  // The field descriptor (upb_FieldDef*).
+  // The low bit indicates whether the container is reified (see ptr below).
+  //   - low bit set: repeated field is a stub (empty map, no underlying data).
+  //   - low bit clear: repeated field is reified (points to upb_Array).
+  uintptr_t field;
+  union {
+    PyObject* parent;  // stub: owning pointer to parent message.
+    upb_Map* map;      // reified: the data for this array.
+  } ptr;
+  int version;
+} PyUpb_MapContainer;
+
+static PyObject* PyUpb_MapIterator_New(PyUpb_MapContainer* map);
+
+static bool PyUpb_MapContainer_IsStub(PyUpb_MapContainer* self) {
+  return self->field & 1;
+}
+
+// If the map is reified, returns it.  Otherwise, returns NULL.
+// If NULL is returned, the object is empty and has no underlying data.
+static upb_Map* PyUpb_MapContainer_GetIfReified(PyUpb_MapContainer* self) {
+  return PyUpb_MapContainer_IsStub(self) ? NULL : self->ptr.map;
+}
+
+static const upb_FieldDef* PyUpb_MapContainer_GetField(
+    PyUpb_MapContainer* self) {
+  return (const upb_FieldDef*)(self->field & ~(uintptr_t)1);
+}
+
+static void PyUpb_MapContainer_Dealloc(void* _self) {
+  PyUpb_MapContainer* self = _self;
+  Py_DECREF(self->arena);
+  if (PyUpb_MapContainer_IsStub(self)) {
+    PyUpb_Message_CacheDelete(self->ptr.parent,
+                              PyUpb_MapContainer_GetField(self));
+    Py_DECREF(self->ptr.parent);
+  } else {
+    PyUpb_ObjCache_Delete(self->ptr.map);
+  }
+  PyUpb_Dealloc(_self);
+}
+
+PyTypeObject* PyUpb_MapContainer_GetClass(const upb_FieldDef* f) {
+  assert(upb_FieldDef_IsMap(f));
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  const upb_FieldDef* val =
+      upb_MessageDef_Field(upb_FieldDef_MessageSubDef(f), 1);
+  assert(upb_FieldDef_Number(val) == 2);
+  return upb_FieldDef_IsSubMessage(val) ? state->message_map_container_type
+                                        : state->scalar_map_container_type;
+}
+
+PyObject* PyUpb_MapContainer_NewStub(PyObject* parent, const upb_FieldDef* f,
+                                     PyObject* arena) {
+  // We only create stubs when the parent is reified, by convention.  However
+  // this is not an invariant: the parent could become reified at any time.
+  assert(PyUpb_Message_GetIfReified(parent) == NULL);
+  PyTypeObject* cls = PyUpb_MapContainer_GetClass(f);
+  PyUpb_MapContainer* map = (void*)PyType_GenericAlloc(cls, 0);
+  map->arena = arena;
+  map->field = (uintptr_t)f | 1;
+  map->ptr.parent = parent;
+  map->version = 0;
+  Py_INCREF(arena);
+  Py_INCREF(parent);
+  return &map->ob_base;
+}
+
+void PyUpb_MapContainer_Reify(PyObject* _self, upb_Map* map) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  if (!map) {
+    const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+    upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+    const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+    const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+    const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+    map = upb_Map_New(arena, upb_FieldDef_CType(key_f),
+                      upb_FieldDef_CType(val_f));
+  }
+  PyUpb_ObjCache_Add(map, &self->ob_base);
+  Py_DECREF(self->ptr.parent);
+  self->ptr.map = map;  // Overwrites self->ptr.parent.
+  self->field &= ~(uintptr_t)1;
+  assert(!PyUpb_MapContainer_IsStub(self));
+}
+
+void PyUpb_MapContainer_Invalidate(PyObject* obj) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)obj;
+  self->version++;
+}
+
+upb_Map* PyUpb_MapContainer_EnsureReified(PyObject* _self) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  self->version++;
+  upb_Map* map = PyUpb_MapContainer_GetIfReified(self);
+  if (map) return map;  // Already writable.
+
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+  const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+  map =
+      upb_Map_New(arena, upb_FieldDef_CType(key_f), upb_FieldDef_CType(val_f));
+  upb_MessageValue msgval = {.map_val = map};
+  PyUpb_Message_SetConcreteSubobj(self->ptr.parent, f, msgval);
+  PyUpb_MapContainer_Reify((PyObject*)self, map);
+  return map;
+}
+
+bool PyUpb_MapContainer_Set(PyUpb_MapContainer* self, upb_Map* map,
+                            upb_MessageValue key, upb_MessageValue val,
+                            upb_Arena* arena) {
+  switch (upb_Map_Insert(map, key, val, arena)) {
+    case kUpb_MapInsertStatus_Inserted:
+      return true;
+    case kUpb_MapInsertStatus_Replaced:
+      // We did not insert a new key, undo the previous invalidate.
+      self->version--;
+      return true;
+    case kUpb_MapInsertStatus_OutOfMemory:
+      return false;
+  }
+  return false;  // Unreachable, silence compiler warning.
+}
+
+int PyUpb_MapContainer_AssignSubscript(PyObject* _self, PyObject* key,
+                                       PyObject* val) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  upb_Map* map = PyUpb_MapContainer_EnsureReified(_self);
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+  const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  upb_MessageValue u_key, u_val;
+  if (!PyUpb_PyToUpb(key, key_f, &u_key, arena)) return -1;
+
+  if (val) {
+    if (!PyUpb_PyToUpb(val, val_f, &u_val, arena)) return -1;
+    if (!PyUpb_MapContainer_Set(self, map, u_key, u_val, arena)) return -1;
+  } else {
+    if (!upb_Map_Delete(map, u_key, NULL)) {
+      PyErr_Format(PyExc_KeyError, "Key not present in map");
+      return -1;
+    }
+  }
+  return 0;
+}
+
+PyObject* PyUpb_MapContainer_Subscript(PyObject* _self, PyObject* key) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  upb_Map* map = PyUpb_MapContainer_GetIfReified(self);
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+  const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  upb_MessageValue u_key, u_val;
+  if (!PyUpb_PyToUpb(key, key_f, &u_key, arena)) return NULL;
+  if (!map || !upb_Map_Get(map, u_key, &u_val)) {
+    map = PyUpb_MapContainer_EnsureReified(_self);
+    upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+    if (upb_FieldDef_IsSubMessage(val_f)) {
+      const upb_Message* m = upb_FieldDef_MessageSubDef(val_f);
+      const upb_MiniTable* layout = upb_MessageDef_MiniTable(m);
+      u_val.msg_val = upb_Message_New(layout, arena);
+    } else {
+      memset(&u_val, 0, sizeof(u_val));
+    }
+    if (!PyUpb_MapContainer_Set(self, map, u_key, u_val, arena)) return false;
+  }
+  return PyUpb_UpbToPy(u_val, val_f, self->arena);
+}
+
+PyObject* PyUpb_MapContainer_Contains(PyObject* _self, PyObject* key) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  upb_Map* map = PyUpb_MapContainer_GetIfReified(self);
+  if (!map) Py_RETURN_FALSE;
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+  upb_MessageValue u_key;
+  if (!PyUpb_PyToUpb(key, key_f, &u_key, NULL)) return NULL;
+  if (upb_Map_Get(map, u_key, NULL)) {
+    Py_RETURN_TRUE;
+  } else {
+    Py_RETURN_FALSE;
+  }
+}
+
+PyObject* PyUpb_MapContainer_Clear(PyObject* _self, PyObject* key) {
+  upb_Map* map = PyUpb_MapContainer_EnsureReified(_self);
+  upb_Map_Clear(map);
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_MapContainer_Get(PyObject* _self, PyObject* args,
+                                        PyObject* kwargs) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  static const char* kwlist[] = {"key", "default", NULL};
+  PyObject* key;
+  PyObject* default_value = NULL;
+  upb_Map* map = PyUpb_MapContainer_GetIfReified(self);
+  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", (char**)kwlist, &key,
+                                   &default_value)) {
+    return NULL;
+  }
+
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+  const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  upb_MessageValue u_key, u_val;
+  if (!PyUpb_PyToUpb(key, key_f, &u_key, arena)) return NULL;
+  if (map && upb_Map_Get(map, u_key, &u_val)) {
+    return PyUpb_UpbToPy(u_val, val_f, self->arena);
+  }
+  if (default_value) {
+    Py_INCREF(default_value);
+    return default_value;
+  }
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_MapContainer_GetEntryClass(PyObject* _self,
+                                                  PyObject* arg) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  return PyUpb_Descriptor_GetClass(entry_m);
+}
+
+Py_ssize_t PyUpb_MapContainer_Length(PyObject* _self) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  upb_Map* map = PyUpb_MapContainer_GetIfReified(self);
+  return map ? upb_Map_Size(map) : 0;
+}
+
+PyUpb_MapContainer* PyUpb_MapContainer_Check(PyObject* _self) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  if (!PyObject_TypeCheck(_self, state->message_map_container_type) &&
+      !PyObject_TypeCheck(_self, state->scalar_map_container_type)) {
+    PyErr_Format(PyExc_TypeError, "Expected protobuf map, but got %R", _self);
+    return NULL;
+  }
+  return (PyUpb_MapContainer*)_self;
+}
+
+int PyUpb_Message_InitMapAttributes(PyObject* map, PyObject* value,
+                                    const upb_FieldDef* f);
+
+static PyObject* PyUpb_MapContainer_MergeFrom(PyObject* _self, PyObject* _arg) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+
+  if (PyDict_Check(_arg)) {
+    return PyErr_Format(PyExc_AttributeError, "Merging of dict is not allowed");
+  }
+
+  if (PyUpb_Message_InitMapAttributes(_self, _arg, f) < 0) {
+    return NULL;
+  }
+
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_MapContainer_Repr(PyObject* _self) {
+  PyUpb_MapContainer* self = (PyUpb_MapContainer*)_self;
+  upb_Map* map = PyUpb_MapContainer_GetIfReified(self);
+  PyObject* dict = PyDict_New();
+  if (map) {
+    const upb_FieldDef* f = PyUpb_MapContainer_GetField(self);
+    const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+    const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+    const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+    size_t iter = kUpb_Map_Begin;
+    upb_MessageValue map_key, map_val;
+    while (upb_Map_Next(map, &map_key, &map_val, &iter)) {
+      PyObject* key = PyUpb_UpbToPy(map_key, key_f, self->arena);
+      PyObject* val = PyUpb_UpbToPy(map_val, val_f, self->arena);
+      if (!key || !val) {
+        Py_XDECREF(key);
+        Py_XDECREF(val);
+        Py_DECREF(dict);
+        return NULL;
+      }
+      PyDict_SetItem(dict, key, val);
+      Py_DECREF(key);
+      Py_DECREF(val);
+    }
+  }
+  PyObject* repr = PyObject_Repr(dict);
+  Py_DECREF(dict);
+  return repr;
+}
+
+PyObject* PyUpb_MapContainer_GetOrCreateWrapper(upb_Map* map,
+                                                const upb_FieldDef* f,
+                                                PyObject* arena) {
+  PyUpb_MapContainer* ret = (void*)PyUpb_ObjCache_Get(map);
+  if (ret) return &ret->ob_base;
+
+  PyTypeObject* cls = PyUpb_MapContainer_GetClass(f);
+  ret = (void*)PyType_GenericAlloc(cls, 0);
+  ret->arena = arena;
+  ret->field = (uintptr_t)f;
+  ret->ptr.map = map;
+  ret->version = 0;
+  Py_INCREF(arena);
+  PyUpb_ObjCache_Add(map, &ret->ob_base);
+  return &ret->ob_base;
+}
+
+// -----------------------------------------------------------------------------
+// ScalarMapContainer
+// -----------------------------------------------------------------------------
+
+static PyMethodDef PyUpb_ScalarMapContainer_Methods[] = {
+    {"__contains__", PyUpb_MapContainer_Contains, METH_O,
+     "Tests whether a key is a member of the map."},
+    {"clear", PyUpb_MapContainer_Clear, METH_NOARGS,
+     "Removes all elements from the map."},
+    {"get", (PyCFunction)PyUpb_MapContainer_Get, METH_VARARGS | METH_KEYWORDS,
+     "Gets the value for the given key if present, or otherwise a default"},
+    {"GetEntryClass", PyUpb_MapContainer_GetEntryClass, METH_NOARGS,
+     "Return the class used to build Entries of (key, value) pairs."},
+    {"MergeFrom", PyUpb_MapContainer_MergeFrom, METH_O,
+     "Merges a map into the current map."},
+    /*
+   { "__deepcopy__", (PyCFunction)DeepCopy, METH_VARARGS,
+     "Makes a deep copy of the class." },
+   { "__reduce__", (PyCFunction)Reduce, METH_NOARGS,
+     "Outputs picklable representation of the repeated field." },
+   */
+    {NULL, NULL},
+};
+
+static PyType_Slot PyUpb_ScalarMapContainer_Slots[] = {
+    {Py_tp_dealloc, PyUpb_MapContainer_Dealloc},
+    {Py_mp_length, PyUpb_MapContainer_Length},
+    {Py_mp_subscript, PyUpb_MapContainer_Subscript},
+    {Py_mp_ass_subscript, PyUpb_MapContainer_AssignSubscript},
+    {Py_tp_methods, PyUpb_ScalarMapContainer_Methods},
+    {Py_tp_iter, PyUpb_MapIterator_New},
+    {Py_tp_repr, PyUpb_MapContainer_Repr},
+    {0, NULL},
+};
+
+static PyType_Spec PyUpb_ScalarMapContainer_Spec = {
+    PYUPB_MODULE_NAME ".ScalarMapContainer",
+    sizeof(PyUpb_MapContainer),
+    0,
+    Py_TPFLAGS_DEFAULT,
+    PyUpb_ScalarMapContainer_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// MessageMapContainer
+// -----------------------------------------------------------------------------
+
+static PyMethodDef PyUpb_MessageMapContainer_Methods[] = {
+    {"__contains__", PyUpb_MapContainer_Contains, METH_O,
+     "Tests whether the map contains this element."},
+    {"clear", PyUpb_MapContainer_Clear, METH_NOARGS,
+     "Removes all elements from the map."},
+    {"get", (PyCFunction)PyUpb_MapContainer_Get, METH_VARARGS | METH_KEYWORDS,
+     "Gets the value for the given key if present, or otherwise a default"},
+    {"get_or_create", PyUpb_MapContainer_Subscript, METH_O,
+     "Alias for getitem, useful to make explicit that the map is mutated."},
+    {"GetEntryClass", PyUpb_MapContainer_GetEntryClass, METH_NOARGS,
+     "Return the class used to build Entries of (key, value) pairs."},
+    {"MergeFrom", PyUpb_MapContainer_MergeFrom, METH_O,
+     "Merges a map into the current map."},
+    /*
+   { "__deepcopy__", (PyCFunction)DeepCopy, METH_VARARGS,
+     "Makes a deep copy of the class." },
+   { "__reduce__", (PyCFunction)Reduce, METH_NOARGS,
+     "Outputs picklable representation of the repeated field." },
+   */
+    {NULL, NULL},
+};
+
+static PyType_Slot PyUpb_MessageMapContainer_Slots[] = {
+    {Py_tp_dealloc, PyUpb_MapContainer_Dealloc},
+    {Py_mp_length, PyUpb_MapContainer_Length},
+    {Py_mp_subscript, PyUpb_MapContainer_Subscript},
+    {Py_mp_ass_subscript, PyUpb_MapContainer_AssignSubscript},
+    {Py_tp_methods, PyUpb_MessageMapContainer_Methods},
+    {Py_tp_iter, PyUpb_MapIterator_New},
+    {Py_tp_repr, PyUpb_MapContainer_Repr},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_MessageMapContainer_Spec = {
+    PYUPB_MODULE_NAME ".MessageMapContainer", sizeof(PyUpb_MapContainer), 0,
+    Py_TPFLAGS_DEFAULT, PyUpb_MessageMapContainer_Slots};
+
+// -----------------------------------------------------------------------------
+// MapIterator
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  PyUpb_MapContainer* map;  // We own a reference.
+  size_t iter;
+  int version;
+} PyUpb_MapIterator;
+
+static PyObject* PyUpb_MapIterator_New(PyUpb_MapContainer* map) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyUpb_MapIterator* iter =
+      (void*)PyType_GenericAlloc(state->map_iterator_type, 0);
+  iter->map = map;
+  iter->iter = kUpb_Map_Begin;
+  iter->version = map->version;
+  Py_INCREF(map);
+  return &iter->ob_base;
+}
+
+static void PyUpb_MapIterator_Dealloc(void* _self) {
+  PyUpb_MapIterator* self = (PyUpb_MapIterator*)_self;
+  Py_DECREF(&self->map->ob_base);
+  PyUpb_Dealloc(_self);
+}
+
+PyObject* PyUpb_MapIterator_IterNext(PyObject* _self) {
+  PyUpb_MapIterator* self = (PyUpb_MapIterator*)_self;
+  if (self->version != self->map->version) {
+    return PyErr_Format(PyExc_RuntimeError, "Map modified during iteration.");
+  }
+  upb_Map* map = PyUpb_MapContainer_GetIfReified(self->map);
+  if (!map) return NULL;
+  upb_MessageValue key, val;
+  if (!upb_Map_Next(map, &key, &val, &self->iter)) return NULL;
+  const upb_FieldDef* f = PyUpb_MapContainer_GetField(self->map);
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* key_f = upb_MessageDef_Field(entry_m, 0);
+  return PyUpb_UpbToPy(key, key_f, self->map->arena);
+}
+
+static PyType_Slot PyUpb_MapIterator_Slots[] = {
+    {Py_tp_dealloc, PyUpb_MapIterator_Dealloc},
+    {Py_tp_iter, PyObject_SelfIter},
+    {Py_tp_iternext, PyUpb_MapIterator_IterNext},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_MapIterator_Spec = {
+    PYUPB_MODULE_NAME ".MapIterator", sizeof(PyUpb_MapIterator), 0,
+    Py_TPFLAGS_DEFAULT, PyUpb_MapIterator_Slots};
+
+// -----------------------------------------------------------------------------
+// Top Level
+// -----------------------------------------------------------------------------
+
+static PyObject* GetMutableMappingBase(void) {
+  PyObject* collections = NULL;
+  PyObject* mapping = NULL;
+  PyObject* bases = NULL;
+  if ((collections = PyImport_ImportModule("collections.abc")) &&
+      (mapping = PyObject_GetAttrString(collections, "MutableMapping"))) {
+    bases = Py_BuildValue("(O)", mapping);
+  }
+  Py_XDECREF(collections);
+  Py_XDECREF(mapping);
+  return bases;
+}
+
+bool PyUpb_Map_Init(PyObject* m) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_GetFromModule(m);
+  PyObject* bases = GetMutableMappingBase();
+  if (!bases) return false;
+
+  state->message_map_container_type =
+      PyUpb_AddClassWithBases(m, &PyUpb_MessageMapContainer_Spec, bases);
+  state->scalar_map_container_type =
+      PyUpb_AddClassWithBases(m, &PyUpb_ScalarMapContainer_Spec, bases);
+  state->map_iterator_type = PyUpb_AddClass(m, &PyUpb_MapIterator_Spec);
+
+  Py_DECREF(bases);
+
+  return state->message_map_container_type &&
+         state->scalar_map_container_type && state->map_iterator_type;
+}
diff --git a/python/map.h b/python/map.h
new file mode 100644
index 0000000..6c2c47d
--- /dev/null
+++ b/python/map.h
@@ -0,0 +1,69 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_MAP_H__
+#define PYUPB_MAP_H__
+
+#include <stdbool.h>
+
+#include "python/python_api.h"
+#include "upb/reflection/def.h"
+
+// Creates a new repeated field stub for field `f` of message object `parent`.
+// Precondition: `parent` must be a stub.
+PyObject* PyUpb_MapContainer_NewStub(PyObject* parent, const upb_FieldDef* f,
+                                     PyObject* arena);
+
+// Returns a map object wrapping `map`, of field type `f`, which must be on
+// `arena`.  If an existing wrapper object exists, it will be returned,
+// otherwise a new object will be created.  The caller always owns a ref on the
+// returned value.
+PyObject* PyUpb_MapContainer_GetOrCreateWrapper(upb_Map* map,
+                                                const upb_FieldDef* f,
+                                                PyObject* arena);
+
+// Reifies a map stub to point to the concrete data in `map`.
+// If `map` is NULL, an appropriate empty map will be constructed.
+void PyUpb_MapContainer_Reify(PyObject* self, upb_Map* map);
+
+// Reifies this map object if it is not already reified.
+upb_Map* PyUpb_MapContainer_EnsureReified(PyObject* self);
+
+// Assigns `self[key] = val` for the map `self`.
+int PyUpb_MapContainer_AssignSubscript(PyObject* self, PyObject* key,
+                                       PyObject* val);
+
+// Invalidates any existing iterators for the map `obj`.
+void PyUpb_MapContainer_Invalidate(PyObject* obj);
+
+// Module-level init.
+bool PyUpb_Map_Init(PyObject* m);
+
+#endif  // PYUPB_MAP_H__
diff --git a/python/message.c b/python/message.c
new file mode 100644
index 0000000..0777e7e
--- /dev/null
+++ b/python/message.c
@@ -0,0 +1,2019 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/message.h"
+
+#include "python/convert.h"
+#include "python/descriptor.h"
+#include "python/extension_dict.h"
+#include "python/map.h"
+#include "python/repeated.h"
+#include "upb/message/copy.h"
+#include "upb/reflection/def.h"
+#include "upb/reflection/message.h"
+#include "upb/text/encode.h"
+#include "upb/util/required_fields.h"
+
+static const upb_MessageDef* PyUpb_MessageMeta_GetMsgdef(PyObject* cls);
+static PyObject* PyUpb_MessageMeta_GetAttr(PyObject* self, PyObject* name);
+
+// -----------------------------------------------------------------------------
+// CPythonBits
+// -----------------------------------------------------------------------------
+
+// This struct contains a few things that are not exposed directly through the
+// limited API, but that we can get at in somewhat more roundabout ways. The
+// roundabout ways are slower, so we cache the values here.
+//
+// These values are valid to cache in a global, even across sub-interpreters,
+// because they are not pointers to interpreter state.  They are process
+// globals that will be the same for any interpreter in this process.
+typedef struct {
+  // For each member, we note the equivalent expression that we could use in the
+  // full (non-limited) API.
+  newfunc type_new;            // PyTypeObject.tp_new
+  destructor type_dealloc;     // PyTypeObject.tp_dealloc
+  getattrofunc type_getattro;  // PyTypeObject.tp_getattro
+  setattrofunc type_setattro;  // PyTypeObject.tp_setattro
+  size_t type_basicsize;       // sizeof(PyHeapTypeObject)
+  traverseproc type_traverse;  // PyTypeObject.tp_traverse
+  inquiry type_clear;          // PyTypeObject.tp_clear
+
+  // While we can refer to PY_VERSION_HEX in the limited API, this will give us
+  // the version of Python we were compiled against, which may be different
+  // than the version we are dynamically linked against.  Here we want the
+  // version that is actually running in this process.
+  long python_version_hex;  // PY_VERSION_HEX
+} PyUpb_CPythonBits;
+
+// A global containing the values for this process.
+PyUpb_CPythonBits cpython_bits;
+
+destructor upb_Pre310_PyType_GetDeallocSlot(PyTypeObject* type_subclass) {
+  // This is a bit desperate.  We need type_dealloc(), but PyType_GetSlot(type,
+  // Py_tp_dealloc) will return subtype_dealloc().  There appears to be no way
+  // whatsoever to fetch type_dealloc() through the limited API until Python
+  // 3.10.
+  //
+  // To work around this so we attempt to find it by looking for the offset of
+  // tp_dealloc in PyTypeObject, then memcpy() it directly.  This should always
+  // work in practice.
+  //
+  // Starting with Python 3.10 on you can call PyType_GetSlot() on non-heap
+  // types.  We will be able to replace all this hack with just:
+  //
+  //   PyType_GetSlot(&PyType_Type, Py_tp_dealloc)
+  //
+  destructor subtype_dealloc = PyType_GetSlot(type_subclass, Py_tp_dealloc);
+  for (size_t i = 0; i < 2000; i += sizeof(uintptr_t)) {
+    destructor maybe_subtype_dealloc;
+    memcpy(&maybe_subtype_dealloc, (char*)type_subclass + i,
+           sizeof(destructor));
+    if (maybe_subtype_dealloc == subtype_dealloc) {
+      destructor type_dealloc;
+      memcpy(&type_dealloc, (char*)&PyType_Type + i, sizeof(destructor));
+      return type_dealloc;
+    }
+  }
+  assert(false);
+  return NULL;
+}
+
+static bool PyUpb_CPythonBits_Init(PyUpb_CPythonBits* bits) {
+  PyObject* bases = NULL;
+  PyTypeObject* type = NULL;
+  PyObject* size = NULL;
+  PyObject* sys = NULL;
+  PyObject* hex_version = NULL;
+  bool ret = false;
+
+  // PyType_GetSlot() only works on heap types, so we cannot use it on
+  // &PyType_Type directly. Instead we create our own (temporary) type derived
+  // from PyType_Type: this will inherit all of the slots from PyType_Type, but
+  // as a heap type it can be queried with PyType_GetSlot().
+  static PyType_Slot dummy_slots[] = {{0, NULL}};
+
+  static PyType_Spec dummy_spec = {
+      "module.DummyClass",  // tp_name
+      0,  // To be filled in by size of base     // tp_basicsize
+      0,  // tp_itemsize
+      Py_TPFLAGS_DEFAULT,  // tp_flags
+      dummy_slots,
+  };
+
+  bases = Py_BuildValue("(O)", &PyType_Type);
+  if (!bases) goto err;
+  type = (PyTypeObject*)PyType_FromSpecWithBases(&dummy_spec, bases);
+  if (!type) goto err;
+
+  bits->type_new = PyType_GetSlot(type, Py_tp_new);
+  bits->type_dealloc = upb_Pre310_PyType_GetDeallocSlot(type);
+  bits->type_getattro = PyType_GetSlot(type, Py_tp_getattro);
+  bits->type_setattro = PyType_GetSlot(type, Py_tp_setattro);
+  bits->type_traverse = PyType_GetSlot(type, Py_tp_traverse);
+  bits->type_clear = PyType_GetSlot(type, Py_tp_clear);
+
+  size = PyObject_GetAttrString((PyObject*)&PyType_Type, "__basicsize__");
+  if (!size) goto err;
+  bits->type_basicsize = PyLong_AsLong(size);
+  if (bits->type_basicsize == -1) goto err;
+
+  assert(bits->type_new);
+  assert(bits->type_dealloc);
+  assert(bits->type_getattro);
+  assert(bits->type_setattro);
+  assert(bits->type_traverse);
+  assert(bits->type_clear);
+
+#ifndef Py_LIMITED_API
+  assert(bits->type_new == PyType_Type.tp_new);
+  assert(bits->type_dealloc == PyType_Type.tp_dealloc);
+  assert(bits->type_getattro == PyType_Type.tp_getattro);
+  assert(bits->type_setattro == PyType_Type.tp_setattro);
+  assert(bits->type_basicsize == sizeof(PyHeapTypeObject));
+  assert(bits->type_traverse == PyType_Type.tp_traverse);
+  assert(bits->type_clear == PyType_Type.tp_clear);
+#endif
+
+  sys = PyImport_ImportModule("sys");
+  hex_version = PyObject_GetAttrString(sys, "hexversion");
+  bits->python_version_hex = PyLong_AsLong(hex_version);
+  ret = true;
+
+err:
+  Py_XDECREF(bases);
+  Py_XDECREF(type);
+  Py_XDECREF(size);
+  Py_XDECREF(sys);
+  Py_XDECREF(hex_version);
+  return ret;
+}
+
+// -----------------------------------------------------------------------------
+// Message
+// -----------------------------------------------------------------------------
+
+// The main message object.  The type of the object (PyUpb_Message.ob_type)
+// will be an instance of the PyUpb_MessageMeta type (defined below).  So the
+// chain is:
+//   FooMessage = MessageMeta(...)
+//   foo = FooMessage()
+//
+// Which becomes:
+//   Object             C Struct Type        Python type (ob_type)
+//   -----------------  -----------------    ---------------------
+//   foo                PyUpb_Message        FooMessage
+//   FooMessage         PyUpb_MessageMeta    message_meta_type
+//   message_meta_type  PyTypeObject         'type' in Python
+//
+// A message object can be in one of two states: present or non-present.  When
+// a message is non-present, it stores a reference to its parent, and a write
+// to any attribute will trigger the message to become present in its parent.
+// The parent may also be non-present, in which case a mutation will trigger a
+// chain reaction.
+typedef struct PyUpb_Message {
+  PyObject_HEAD;
+  PyObject* arena;
+  uintptr_t def;  // Tagged, low bit 1 == upb_FieldDef*, else upb_MessageDef*
+  union {
+    // when def is msgdef, the data for this msg.
+    upb_Message* msg;
+    // when def is fielddef, owning pointer to parent
+    struct PyUpb_Message* parent;
+  } ptr;
+  PyObject* ext_dict;  // Weak pointer to extension dict, if any.
+  // name->obj dict for non-present msg/map/repeated, NULL if none.
+  PyUpb_WeakMap* unset_subobj_map;
+  int version;
+} PyUpb_Message;
+
+static PyObject* PyUpb_Message_GetAttr(PyObject* _self, PyObject* attr);
+
+bool PyUpb_Message_IsStub(PyUpb_Message* msg) { return msg->def & 1; }
+
+const upb_FieldDef* PyUpb_Message_GetFieldDef(PyUpb_Message* msg) {
+  assert(PyUpb_Message_IsStub(msg));
+  return (void*)(msg->def & ~(uintptr_t)1);
+}
+
+static const upb_MessageDef* _PyUpb_Message_GetMsgdef(PyUpb_Message* msg) {
+  return PyUpb_Message_IsStub(msg)
+             ? upb_FieldDef_MessageSubDef(PyUpb_Message_GetFieldDef(msg))
+             : (void*)msg->def;
+}
+
+const upb_MessageDef* PyUpb_Message_GetMsgdef(PyObject* self) {
+  return _PyUpb_Message_GetMsgdef((PyUpb_Message*)self);
+}
+
+static upb_Message* PyUpb_Message_GetMsg(PyUpb_Message* self) {
+  assert(!PyUpb_Message_IsStub(self));
+  return self->ptr.msg;
+}
+
+bool PyUpb_Message_TryCheck(PyObject* self) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyObject* type = (PyObject*)Py_TYPE(self);
+  return Py_TYPE(type) == state->message_meta_type;
+}
+
+bool PyUpb_Message_Verify(PyObject* self) {
+  if (!PyUpb_Message_TryCheck(self)) {
+    PyErr_Format(PyExc_TypeError, "Expected a message object, but got %R.",
+                 self);
+    return false;
+  }
+  return true;
+}
+
+// If the message is reified, returns it.  Otherwise, returns NULL.
+// If NULL is returned, the object is empty and has no underlying data.
+upb_Message* PyUpb_Message_GetIfReified(PyObject* _self) {
+  PyUpb_Message* self = (void*)_self;
+  return PyUpb_Message_IsStub(self) ? NULL : self->ptr.msg;
+}
+
+static PyObject* PyUpb_Message_New(PyObject* cls, PyObject* unused_args,
+                                   PyObject* unused_kwargs) {
+  const upb_MessageDef* msgdef = PyUpb_MessageMeta_GetMsgdef(cls);
+  const upb_MiniTable* layout = upb_MessageDef_MiniTable(msgdef);
+  PyUpb_Message* msg = (void*)PyType_GenericAlloc((PyTypeObject*)cls, 0);
+  msg->def = (uintptr_t)msgdef;
+  msg->arena = PyUpb_Arena_New();
+  msg->ptr.msg = upb_Message_New(layout, PyUpb_Arena_Get(msg->arena));
+  msg->unset_subobj_map = NULL;
+  msg->ext_dict = NULL;
+  msg->version = 0;
+
+  PyObject* ret = &msg->ob_base;
+  PyUpb_ObjCache_Add(msg->ptr.msg, ret);
+  return ret;
+}
+
+/*
+ * PyUpb_Message_LookupName()
+ *
+ * Tries to find a field or oneof named `py_name` in the message object `self`.
+ * The user must pass `f` and/or `o` to indicate whether a field or a oneof name
+ * is expected.  If the name is found and it has an expected type, the function
+ * sets `*f` or `*o` respectively and returns true.  Otherwise returns false
+ * and sets an exception of type `exc_type` if provided.
+ */
+static bool PyUpb_Message_LookupName(PyUpb_Message* self, PyObject* py_name,
+                                     const upb_FieldDef** f,
+                                     const upb_OneofDef** o,
+                                     PyObject* exc_type) {
+  assert(f || o);
+  Py_ssize_t size;
+  const char* name = NULL;
+  if (PyUnicode_Check(py_name)) {
+    name = PyUnicode_AsUTF8AndSize(py_name, &size);
+  } else if (PyBytes_Check(py_name)) {
+    PyBytes_AsStringAndSize(py_name, (char**)&name, &size);
+  }
+  if (!name) {
+    PyErr_Format(exc_type,
+                 "Expected a field name, but got non-string argument %S.",
+                 py_name);
+    return false;
+  }
+  const upb_MessageDef* msgdef = _PyUpb_Message_GetMsgdef(self);
+
+  if (!upb_MessageDef_FindByNameWithSize(msgdef, name, size, f, o)) {
+    if (exc_type) {
+      PyErr_Format(exc_type, "Protocol message %s has no \"%s\" field.",
+                   upb_MessageDef_Name(msgdef), name);
+    }
+    return false;
+  }
+
+  if (!o && !*f) {
+    if (exc_type) {
+      PyErr_Format(exc_type, "Expected a field name, but got oneof name %s.",
+                   name);
+    }
+    return false;
+  }
+
+  if (!f && !*o) {
+    if (exc_type) {
+      PyErr_Format(exc_type, "Expected a oneof name, but got field name %s.",
+                   name);
+    }
+    return false;
+  }
+
+  return true;
+}
+
+static bool PyUpb_Message_InitMessageMapEntry(PyObject* dst, PyObject* src) {
+  if (!src || !dst) return false;
+
+  PyObject* ok = PyObject_CallMethod(dst, "CopyFrom", "O", src);
+  if (!ok) return false;
+  Py_DECREF(ok);
+
+  return true;
+}
+
+int PyUpb_Message_InitMapAttributes(PyObject* map, PyObject* value,
+                                    const upb_FieldDef* f) {
+  const upb_MessageDef* entry_m = upb_FieldDef_MessageSubDef(f);
+  const upb_FieldDef* val_f = upb_MessageDef_Field(entry_m, 1);
+  PyObject* it = NULL;
+  PyObject* tmp = NULL;
+  int ret = -1;
+  if (upb_FieldDef_IsSubMessage(val_f)) {
+    it = PyObject_GetIter(value);
+    if (it == NULL) {
+      PyErr_Format(PyExc_TypeError, "Argument for field %s is not iterable",
+                   upb_FieldDef_FullName(f));
+      goto err;
+    }
+    PyObject* e;
+    while ((e = PyIter_Next(it)) != NULL) {
+      PyObject* src = PyObject_GetItem(value, e);
+      PyObject* dst = PyObject_GetItem(map, e);
+      Py_DECREF(e);
+      bool ok = PyUpb_Message_InitMessageMapEntry(dst, src);
+      Py_XDECREF(src);
+      Py_XDECREF(dst);
+      if (!ok) goto err;
+    }
+  } else {
+    tmp = PyObject_CallMethod(map, "update", "O", value);
+    if (!tmp) goto err;
+  }
+  ret = 0;
+
+err:
+  Py_XDECREF(it);
+  Py_XDECREF(tmp);
+  return ret;
+}
+
+void PyUpb_Message_EnsureReified(PyUpb_Message* self);
+
+static bool PyUpb_Message_InitMapAttribute(PyObject* _self, PyObject* name,
+                                           const upb_FieldDef* f,
+                                           PyObject* value) {
+  PyObject* map = PyUpb_Message_GetAttr(_self, name);
+  int ok = PyUpb_Message_InitMapAttributes(map, value, f);
+  Py_DECREF(map);
+  return ok >= 0;
+}
+
+static bool PyUpb_Message_InitRepeatedMessageAttribute(PyObject* _self,
+                                                       PyObject* repeated,
+                                                       PyObject* value,
+                                                       const upb_FieldDef* f) {
+  PyObject* it = PyObject_GetIter(value);
+  if (!it) {
+    PyErr_Format(PyExc_TypeError, "Argument for field %s is not iterable",
+                 upb_FieldDef_FullName(f));
+    return false;
+  }
+  PyObject* e = NULL;
+  PyObject* m = NULL;
+  while ((e = PyIter_Next(it)) != NULL) {
+    if (PyDict_Check(e)) {
+      m = PyUpb_RepeatedCompositeContainer_Add(repeated, NULL, e);
+      if (!m) goto err;
+    } else {
+      m = PyUpb_RepeatedCompositeContainer_Add(repeated, NULL, NULL);
+      if (!m) goto err;
+      PyObject* merged = PyUpb_Message_MergeFrom(m, e);
+      if (!merged) goto err;
+      Py_DECREF(merged);
+    }
+    Py_DECREF(e);
+    Py_DECREF(m);
+    m = NULL;
+  }
+
+err:
+  Py_XDECREF(it);
+  Py_XDECREF(e);
+  Py_XDECREF(m);
+  return !PyErr_Occurred();  // Check PyIter_Next() exit.
+}
+
+static bool PyUpb_Message_InitRepeatedAttribute(PyObject* _self, PyObject* name,
+                                                PyObject* value) {
+  PyUpb_Message* self = (void*)_self;
+  const upb_FieldDef* field;
+  if (!PyUpb_Message_LookupName(self, name, &field, NULL,
+                                PyExc_AttributeError)) {
+    return false;
+  }
+  bool ok = false;
+  PyObject* repeated = PyUpb_Message_GetFieldValue(_self, field);
+  PyObject* tmp = NULL;
+  if (!repeated) goto err;
+  if (upb_FieldDef_IsSubMessage(field)) {
+    if (!PyUpb_Message_InitRepeatedMessageAttribute(_self, repeated, value,
+                                                    field)) {
+      goto err;
+    }
+  } else {
+    tmp = PyUpb_RepeatedContainer_Extend(repeated, value);
+    if (!tmp) goto err;
+  }
+  ok = true;
+
+err:
+  Py_XDECREF(repeated);
+  Py_XDECREF(tmp);
+  return ok;
+}
+
+static bool PyUpb_Message_InitMessageAttribute(PyObject* _self, PyObject* name,
+                                               PyObject* value) {
+  PyObject* submsg = PyUpb_Message_GetAttr(_self, name);
+  if (!submsg) return -1;
+  assert(!PyErr_Occurred());
+  bool ok;
+  if (PyUpb_Message_TryCheck(value)) {
+    PyObject* tmp = PyUpb_Message_MergeFrom(submsg, value);
+    ok = tmp != NULL;
+    Py_XDECREF(tmp);
+  } else if (PyDict_Check(value)) {
+    assert(!PyErr_Occurred());
+    ok = PyUpb_Message_InitAttributes(submsg, NULL, value) >= 0;
+  } else {
+    const upb_MessageDef* m = PyUpb_Message_GetMsgdef(_self);
+    PyErr_Format(PyExc_TypeError, "Message must be initialized with a dict: %s",
+                 upb_MessageDef_FullName(m));
+    ok = false;
+  }
+  Py_DECREF(submsg);
+  return ok;
+}
+
+static bool PyUpb_Message_InitScalarAttribute(upb_Message* msg,
+                                              const upb_FieldDef* f,
+                                              PyObject* value,
+                                              upb_Arena* arena) {
+  upb_MessageValue msgval;
+  assert(!PyErr_Occurred());
+  if (!PyUpb_PyToUpb(value, f, &msgval, arena)) return false;
+  upb_Message_SetFieldByDef(msg, f, msgval, arena);
+  return true;
+}
+
+int PyUpb_Message_InitAttributes(PyObject* _self, PyObject* args,
+                                 PyObject* kwargs) {
+  assert(!PyErr_Occurred());
+
+  if (args != NULL && PyTuple_Size(args) != 0) {
+    PyErr_SetString(PyExc_TypeError, "No positional arguments allowed");
+    return -1;
+  }
+
+  if (kwargs == NULL) return 0;
+
+  PyUpb_Message* self = (void*)_self;
+  Py_ssize_t pos = 0;
+  PyObject* name;
+  PyObject* value;
+  PyUpb_Message_EnsureReified(self);
+  upb_Message* msg = PyUpb_Message_GetMsg(self);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+
+  while (PyDict_Next(kwargs, &pos, &name, &value)) {
+    assert(!PyErr_Occurred());
+    const upb_FieldDef* f;
+    assert(!PyErr_Occurred());
+    if (!PyUpb_Message_LookupName(self, name, &f, NULL, PyExc_ValueError)) {
+      return -1;
+    }
+
+    if (value == Py_None) continue;  // Ignored.
+
+    assert(!PyErr_Occurred());
+
+    if (upb_FieldDef_IsMap(f)) {
+      if (!PyUpb_Message_InitMapAttribute(_self, name, f, value)) return -1;
+    } else if (upb_FieldDef_IsRepeated(f)) {
+      if (!PyUpb_Message_InitRepeatedAttribute(_self, name, value)) return -1;
+    } else if (upb_FieldDef_IsSubMessage(f)) {
+      if (!PyUpb_Message_InitMessageAttribute(_self, name, value)) return -1;
+    } else {
+      if (!PyUpb_Message_InitScalarAttribute(msg, f, value, arena)) return -1;
+    }
+    if (PyErr_Occurred()) return -1;
+  }
+
+  if (PyErr_Occurred()) return -1;
+  return 0;
+}
+
+static int PyUpb_Message_Init(PyObject* _self, PyObject* args,
+                              PyObject* kwargs) {
+  if (args != NULL && PyTuple_Size(args) != 0) {
+    PyErr_SetString(PyExc_TypeError, "No positional arguments allowed");
+    return -1;
+  }
+
+  return PyUpb_Message_InitAttributes(_self, args, kwargs);
+}
+
+static PyObject* PyUpb_Message_NewStub(PyObject* parent, const upb_FieldDef* f,
+                                       PyObject* arena) {
+  const upb_MessageDef* sub_m = upb_FieldDef_MessageSubDef(f);
+  PyObject* cls = PyUpb_Descriptor_GetClass(sub_m);
+
+  PyUpb_Message* msg = (void*)PyType_GenericAlloc((PyTypeObject*)cls, 0);
+  msg->def = (uintptr_t)f | 1;
+  msg->arena = arena;
+  msg->ptr.parent = (PyUpb_Message*)parent;
+  msg->unset_subobj_map = NULL;
+  msg->ext_dict = NULL;
+  msg->version = 0;
+
+  Py_DECREF(cls);
+  Py_INCREF(parent);
+  Py_INCREF(arena);
+  return &msg->ob_base;
+}
+
+static bool PyUpb_Message_IsEmpty(const upb_Message* msg,
+                                  const upb_MessageDef* m,
+                                  const upb_DefPool* ext_pool) {
+  if (!msg) return true;
+
+  size_t iter = kUpb_Message_Begin;
+  const upb_FieldDef* f;
+  upb_MessageValue val;
+  if (upb_Message_Next(msg, m, ext_pool, &f, &val, &iter)) return false;
+
+  size_t len;
+  (void)upb_Message_GetUnknown(msg, &len);
+  return len == 0;
+}
+
+static bool PyUpb_Message_IsEqual(PyUpb_Message* m1, PyObject* _m2) {
+  PyUpb_Message* m2 = (void*)_m2;
+  if (m1 == m2) return true;
+  if (!PyObject_TypeCheck(_m2, m1->ob_base.ob_type)) {
+    return false;
+  }
+  const upb_MessageDef* m1_msgdef = _PyUpb_Message_GetMsgdef(m1);
+#ifndef NDEBUG
+  const upb_MessageDef* m2_msgdef = _PyUpb_Message_GetMsgdef(m2);
+  assert(m1_msgdef == m2_msgdef);
+#endif
+  const upb_Message* m1_msg = PyUpb_Message_GetIfReified((PyObject*)m1);
+  const upb_Message* m2_msg = PyUpb_Message_GetIfReified(_m2);
+  const upb_DefPool* symtab = upb_FileDef_Pool(upb_MessageDef_File(m1_msgdef));
+
+  const bool e1 = PyUpb_Message_IsEmpty(m1_msg, m1_msgdef, symtab);
+  const bool e2 = PyUpb_Message_IsEmpty(m2_msg, m1_msgdef, symtab);
+  if (e1 || e2) return e1 && e2;
+
+  return upb_Message_IsEqual(m1_msg, m2_msg, m1_msgdef);
+}
+
+static const upb_FieldDef* PyUpb_Message_InitAsMsg(PyUpb_Message* m,
+                                                   upb_Arena* arena) {
+  const upb_FieldDef* f = PyUpb_Message_GetFieldDef(m);
+  const upb_MessageDef* m2 = upb_FieldDef_MessageSubDef(f);
+  m->ptr.msg = upb_Message_New(upb_MessageDef_MiniTable(m2), arena);
+  m->def = (uintptr_t)m2;
+  PyUpb_ObjCache_Add(m->ptr.msg, &m->ob_base);
+  return f;
+}
+
+static void PyUpb_Message_SetField(PyUpb_Message* parent, const upb_FieldDef* f,
+                                   PyUpb_Message* child, upb_Arena* arena) {
+  upb_MessageValue msgval = {.msg_val = PyUpb_Message_GetMsg(child)};
+  upb_Message_SetFieldByDef(PyUpb_Message_GetMsg(parent), f, msgval, arena);
+  PyUpb_WeakMap_Delete(parent->unset_subobj_map, f);
+  // Releases a ref previously owned by child->ptr.parent of our child.
+  Py_DECREF(child);
+}
+
+/*
+ * PyUpb_Message_EnsureReified()
+ *
+ * This implements the "expando" behavior of Python protos:
+ *   foo = FooProto()
+ *
+ *   # The intermediate messages don't really exist, and won't be serialized.
+ *   x = foo.bar.bar.bar.bar.bar.baz
+ *
+ *   # Now all the intermediate objects are created.
+ *   foo.bar.bar.bar.bar.bar.baz = 5
+ *
+ * This function should be called before performing any mutation of a protobuf
+ * object.
+ *
+ * Post-condition:
+ *   PyUpb_Message_IsStub(self) is false
+ */
+void PyUpb_Message_EnsureReified(PyUpb_Message* self) {
+  if (!PyUpb_Message_IsStub(self)) return;
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+
+  // This is a non-present message. We need to create a real upb_Message for
+  // this object and every parent until we reach a present message.
+  PyUpb_Message* child = self;
+  PyUpb_Message* parent = self->ptr.parent;
+  const upb_FieldDef* child_f = PyUpb_Message_InitAsMsg(child, arena);
+  Py_INCREF(child);  // To avoid a special-case in PyUpb_Message_SetField().
+
+  do {
+    PyUpb_Message* next_parent = parent->ptr.parent;
+    const upb_FieldDef* parent_f = NULL;
+    if (PyUpb_Message_IsStub(parent)) {
+      parent_f = PyUpb_Message_InitAsMsg(parent, arena);
+    }
+    PyUpb_Message_SetField(parent, child_f, child, arena);
+    child = parent;
+    child_f = parent_f;
+    parent = next_parent;
+  } while (child_f);
+
+  // Releases ref previously owned by child->ptr.parent of our child.
+  Py_DECREF(child);
+  self->version++;
+}
+
+static void PyUpb_Message_SyncSubobjs(PyUpb_Message* self);
+
+/*
+ * PyUpb_Message_Reify()
+ *
+ * The message equivalent of PyUpb_*Container_Reify(), this transitions
+ * the wrapper from the unset state (owning a reference on self->ptr.parent) to
+ * the set state (having a non-owning pointer to self->ptr.msg).
+ */
+static void PyUpb_Message_Reify(PyUpb_Message* self, const upb_FieldDef* f,
+                                upb_Message* msg) {
+  assert(f == PyUpb_Message_GetFieldDef(self));
+  if (!msg) {
+    const upb_MessageDef* msgdef = PyUpb_Message_GetMsgdef((PyObject*)self);
+    const upb_MiniTable* layout = upb_MessageDef_MiniTable(msgdef);
+    msg = upb_Message_New(layout, PyUpb_Arena_Get(self->arena));
+  }
+  PyUpb_ObjCache_Add(msg, &self->ob_base);
+  Py_DECREF(&self->ptr.parent->ob_base);
+  self->ptr.msg = msg;  // Overwrites self->ptr.parent
+  self->def = (uintptr_t)upb_FieldDef_MessageSubDef(f);
+  PyUpb_Message_SyncSubobjs(self);
+}
+
+/*
+ * PyUpb_Message_SyncSubobjs()
+ *
+ * This operation must be invoked whenever the underlying upb_Message has been
+ * mutated directly in C.  This will attach any newly-present field data
+ * to previously returned stub wrapper objects.
+ *
+ * For example:
+ *   foo = FooMessage()
+ *   sub = foo.submsg  # Empty, unset sub-message
+ *
+ *   # SyncSubobjs() is required to connect our existing 'sub' wrapper to the
+ *   # newly created foo.submsg data in C.
+ *   foo.MergeFrom(FooMessage(submsg={}))
+ *
+ * This requires that all of the new sub-objects that have appeared are owned
+ * by `self`'s arena.
+ */
+static void PyUpb_Message_SyncSubobjs(PyUpb_Message* self) {
+  PyUpb_WeakMap* subobj_map = self->unset_subobj_map;
+  if (!subobj_map) return;
+
+  upb_Message* msg = PyUpb_Message_GetMsg(self);
+  intptr_t iter = PYUPB_WEAKMAP_BEGIN;
+  const void* key;
+  PyObject* obj;
+
+  // The last ref to this message could disappear during iteration.
+  // When we call PyUpb_*Container_Reify() below, the container will drop
+  // its ref on `self`.  If that was the last ref on self, the object will be
+  // deleted, and `subobj_map` along with it.  We need it to live until we are
+  // done iterating.
+  Py_INCREF(&self->ob_base);
+
+  while (PyUpb_WeakMap_Next(subobj_map, &key, &obj, &iter)) {
+    const upb_FieldDef* f = key;
+    if (upb_FieldDef_HasPresence(f) && !upb_Message_HasFieldByDef(msg, f))
+      continue;
+    upb_MessageValue msgval = upb_Message_GetFieldByDef(msg, f);
+    PyUpb_WeakMap_DeleteIter(subobj_map, &iter);
+    if (upb_FieldDef_IsMap(f)) {
+      if (!msgval.map_val) continue;
+      PyUpb_MapContainer_Reify(obj, (upb_Map*)msgval.map_val);
+    } else if (upb_FieldDef_IsRepeated(f)) {
+      if (!msgval.array_val) continue;
+      PyUpb_RepeatedContainer_Reify(obj, (upb_Array*)msgval.array_val);
+    } else {
+      PyUpb_Message* sub = (void*)obj;
+      assert(self == sub->ptr.parent);
+      PyUpb_Message_Reify(sub, f, (upb_Message*)msgval.msg_val);
+    }
+  }
+
+  Py_DECREF(&self->ob_base);
+
+  // TODO: present fields need to be iterated too if they can reach
+  // a WeakMap.
+}
+
+static PyObject* PyUpb_Message_ToString(PyUpb_Message* self) {
+  if (PyUpb_Message_IsStub(self)) {
+    return PyUnicode_FromStringAndSize(NULL, 0);
+  }
+  upb_Message* msg = PyUpb_Message_GetMsg(self);
+  const upb_MessageDef* msgdef = _PyUpb_Message_GetMsgdef(self);
+  const upb_DefPool* symtab = upb_FileDef_Pool(upb_MessageDef_File(msgdef));
+  char buf[1024];
+  int options = UPB_TXTENC_SKIPUNKNOWN;
+  size_t size = upb_TextEncode(msg, msgdef, symtab, options, buf, sizeof(buf));
+  if (size < sizeof(buf)) {
+    return PyUnicode_FromStringAndSize(buf, size);
+  } else {
+    char* buf2 = malloc(size + 1);
+    size_t size2 = upb_TextEncode(msg, msgdef, symtab, options, buf2, size + 1);
+    assert(size == size2);
+    PyObject* ret = PyUnicode_FromStringAndSize(buf2, size2);
+    free(buf2);
+    return ret;
+  }
+}
+
+static PyObject* PyUpb_Message_RichCompare(PyObject* _self, PyObject* other,
+                                           int opid) {
+  PyUpb_Message* self = (void*)_self;
+  if (opid != Py_EQ && opid != Py_NE) {
+    Py_INCREF(Py_NotImplemented);
+    return Py_NotImplemented;
+  }
+  if (!PyObject_TypeCheck(other, Py_TYPE(self))) {
+    Py_INCREF(Py_NotImplemented);
+    return Py_NotImplemented;
+  }
+  bool ret = PyUpb_Message_IsEqual(self, other);
+  if (opid == Py_NE) ret = !ret;
+  return PyBool_FromLong(ret);
+}
+
+void PyUpb_Message_CacheDelete(PyObject* _self, const upb_FieldDef* f) {
+  PyUpb_Message* self = (void*)_self;
+  PyUpb_WeakMap_Delete(self->unset_subobj_map, f);
+}
+
+void PyUpb_Message_SetConcreteSubobj(PyObject* _self, const upb_FieldDef* f,
+                                     upb_MessageValue subobj) {
+  PyUpb_Message* self = (void*)_self;
+  PyUpb_Message_EnsureReified(self);
+  PyUpb_Message_CacheDelete(_self, f);
+  upb_Message_SetFieldByDef(self->ptr.msg, f, subobj,
+                            PyUpb_Arena_Get(self->arena));
+}
+
+static void PyUpb_Message_Dealloc(PyObject* _self) {
+  PyUpb_Message* self = (void*)_self;
+
+  if (PyUpb_Message_IsStub(self)) {
+    PyUpb_Message_CacheDelete((PyObject*)self->ptr.parent,
+                              PyUpb_Message_GetFieldDef(self));
+    Py_DECREF(self->ptr.parent);
+  } else {
+    PyUpb_ObjCache_Delete(self->ptr.msg);
+  }
+
+  if (self->unset_subobj_map) {
+    PyUpb_WeakMap_Free(self->unset_subobj_map);
+  }
+
+  Py_DECREF(self->arena);
+
+  // We do not use PyUpb_Dealloc() here because Message is a base type and for
+  // base types there is a bug we have to work around in this case (see below).
+  PyTypeObject* tp = Py_TYPE(self);
+  freefunc tp_free = PyType_GetSlot(tp, Py_tp_free);
+  tp_free(self);
+
+  if (cpython_bits.python_version_hex >= 0x03080000) {
+    // Prior to Python 3.8 there is a bug where deallocating the type here would
+    // lead to a double-decref: https://bugs.python.org/issue37879
+    Py_DECREF(tp);
+  }
+}
+
+PyObject* PyUpb_Message_Get(upb_Message* u_msg, const upb_MessageDef* m,
+                            PyObject* arena) {
+  PyObject* ret = PyUpb_ObjCache_Get(u_msg);
+  if (ret) return ret;
+
+  PyObject* cls = PyUpb_Descriptor_GetClass(m);
+  // It is not safe to use PyObject_{,GC}_New() due to:
+  //    https://bugs.python.org/issue35810
+  PyUpb_Message* py_msg = (void*)PyType_GenericAlloc((PyTypeObject*)cls, 0);
+  py_msg->arena = arena;
+  py_msg->def = (uintptr_t)m;
+  py_msg->ptr.msg = u_msg;
+  py_msg->unset_subobj_map = NULL;
+  py_msg->ext_dict = NULL;
+  py_msg->version = 0;
+  ret = &py_msg->ob_base;
+  Py_DECREF(cls);
+  Py_INCREF(arena);
+  PyUpb_ObjCache_Add(u_msg, ret);
+  return ret;
+}
+
+/* PyUpb_Message_GetStub()
+ *
+ * Non-present messages return "stub" objects that point to their parent, but
+ * will materialize into real upb objects if they are mutated.
+ *
+ * Note: we do *not* create stubs for repeated/map fields unless the parent
+ * is a stub:
+ *
+ *    msg = TestMessage()
+ *    msg.submessage                # (A) Creates a stub
+ *    msg.repeated_foo              # (B) Does *not* create a stub
+ *    msg.submessage.repeated_bar   # (C) Creates a stub
+ *
+ * In case (B) we have some freedom: we could either create a stub, or create
+ * a reified object with underlying data.  It appears that either could work
+ * equally well, with no observable change to users.  There isn't a clear
+ * advantage to either choice.  We choose to follow the behavior of the
+ * pre-existing C++ behavior for consistency, but if it becomes apparent that
+ * there would be some benefit to reversing this decision, it should be totally
+ * within the realm of possibility.
+ */
+PyObject* PyUpb_Message_GetStub(PyUpb_Message* self,
+                                const upb_FieldDef* field) {
+  PyObject* _self = (void*)self;
+  if (!self->unset_subobj_map) {
+    self->unset_subobj_map = PyUpb_WeakMap_New();
+  }
+  PyObject* subobj = PyUpb_WeakMap_Get(self->unset_subobj_map, field);
+
+  if (subobj) return subobj;
+
+  if (upb_FieldDef_IsMap(field)) {
+    subobj = PyUpb_MapContainer_NewStub(_self, field, self->arena);
+  } else if (upb_FieldDef_IsRepeated(field)) {
+    subobj = PyUpb_RepeatedContainer_NewStub(_self, field, self->arena);
+  } else {
+    subobj = PyUpb_Message_NewStub(&self->ob_base, field, self->arena);
+  }
+  PyUpb_WeakMap_Add(self->unset_subobj_map, field, subobj);
+
+  assert(!PyErr_Occurred());
+  return subobj;
+}
+
+PyObject* PyUpb_Message_GetPresentWrapper(PyUpb_Message* self,
+                                          const upb_FieldDef* field) {
+  assert(!PyUpb_Message_IsStub(self));
+  upb_MutableMessageValue mutval =
+      upb_Message_Mutable(self->ptr.msg, field, PyUpb_Arena_Get(self->arena));
+  if (upb_FieldDef_IsMap(field)) {
+    return PyUpb_MapContainer_GetOrCreateWrapper(mutval.map, field,
+                                                 self->arena);
+  } else {
+    return PyUpb_RepeatedContainer_GetOrCreateWrapper(mutval.array, field,
+                                                      self->arena);
+  }
+}
+
+PyObject* PyUpb_Message_GetScalarValue(PyUpb_Message* self,
+                                       const upb_FieldDef* field) {
+  upb_MessageValue val;
+  if (PyUpb_Message_IsStub(self)) {
+    // Unset message always returns default values.
+    val = upb_FieldDef_Default(field);
+  } else {
+    val = upb_Message_GetFieldByDef(self->ptr.msg, field);
+  }
+  return PyUpb_UpbToPy(val, field, self->arena);
+}
+
+/*
+ * PyUpb_Message_GetFieldValue()
+ *
+ * Implements the equivalent of getattr(msg, field), once `field` has
+ * already been resolved to a `upb_FieldDef*`.
+ *
+ * This may involve constructing a wrapper object for the given field, or
+ * returning one that was previously constructed.  If the field is not actually
+ * set, the wrapper object will be an "unset" object that is not actually
+ * connected to any C data.
+ */
+PyObject* PyUpb_Message_GetFieldValue(PyObject* _self,
+                                      const upb_FieldDef* field) {
+  PyUpb_Message* self = (void*)_self;
+  assert(upb_FieldDef_ContainingType(field) == PyUpb_Message_GetMsgdef(_self));
+  bool submsg = upb_FieldDef_IsSubMessage(field);
+  bool seq = upb_FieldDef_IsRepeated(field);
+
+  if ((PyUpb_Message_IsStub(self) && (submsg || seq)) ||
+      (submsg && !seq && !upb_Message_HasFieldByDef(self->ptr.msg, field))) {
+    return PyUpb_Message_GetStub(self, field);
+  } else if (seq) {
+    return PyUpb_Message_GetPresentWrapper(self, field);
+  } else {
+    return PyUpb_Message_GetScalarValue(self, field);
+  }
+}
+
+int PyUpb_Message_SetFieldValue(PyObject* _self, const upb_FieldDef* field,
+                                PyObject* value, PyObject* exc) {
+  PyUpb_Message* self = (void*)_self;
+  assert(value);
+
+  if (upb_FieldDef_IsSubMessage(field) || upb_FieldDef_IsRepeated(field)) {
+    PyErr_Format(exc,
+                 "Assignment not allowed to message, map, or repeated "
+                 "field \"%s\" in protocol message object.",
+                 upb_FieldDef_Name(field));
+    return -1;
+  }
+
+  PyUpb_Message_EnsureReified(self);
+
+  upb_MessageValue val;
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  if (!PyUpb_PyToUpb(value, field, &val, arena)) {
+    return -1;
+  }
+
+  upb_Message_SetFieldByDef(self->ptr.msg, field, val, arena);
+  return 0;
+}
+
+int PyUpb_Message_GetVersion(PyObject* _self) {
+  PyUpb_Message* self = (void*)_self;
+  return self->version;
+}
+
+/*
+ * PyUpb_Message_GetAttr()
+ *
+ * Implements:
+ *   foo = msg.foo
+ *
+ * Attribute lookup must find both message fields and base class methods like
+ * msg.SerializeToString().
+ */
+__attribute__((flatten)) static PyObject* PyUpb_Message_GetAttr(
+    PyObject* _self, PyObject* attr) {
+  PyUpb_Message* self = (void*)_self;
+
+  // Lookup field by name.
+  const upb_FieldDef* field;
+  if (PyUpb_Message_LookupName(self, attr, &field, NULL, NULL)) {
+    return PyUpb_Message_GetFieldValue(_self, field);
+  }
+
+  // Check base class attributes.
+  assert(!PyErr_Occurred());
+  PyObject* ret = PyObject_GenericGetAttr(_self, attr);
+  if (ret) return ret;
+
+  // Swallow AttributeError if it occurred and try again on the metaclass
+  // to pick up class attributes.  But we have to special-case "Extensions"
+  // which affirmatively returns AttributeError when a message is not
+  // extendable.
+  const char* name;
+  if (PyErr_ExceptionMatches(PyExc_AttributeError) &&
+      (name = PyUpb_GetStrData(attr)) && strcmp(name, "Extensions") != 0) {
+    PyErr_Clear();
+    return PyUpb_MessageMeta_GetAttr((PyObject*)Py_TYPE(_self), attr);
+  }
+
+  return NULL;
+}
+
+/*
+ * PyUpb_Message_SetAttr()
+ *
+ * Implements:
+ *   msg.foo = foo
+ */
+static int PyUpb_Message_SetAttr(PyObject* _self, PyObject* attr,
+                                 PyObject* value) {
+  PyUpb_Message* self = (void*)_self;
+  const upb_FieldDef* field;
+  if (!PyUpb_Message_LookupName(self, attr, &field, NULL,
+                                PyExc_AttributeError)) {
+    return -1;
+  }
+
+  return PyUpb_Message_SetFieldValue(_self, field, value, PyExc_AttributeError);
+}
+
+static PyObject* PyUpb_Message_HasField(PyObject* _self, PyObject* arg) {
+  PyUpb_Message* self = (void*)_self;
+  const upb_FieldDef* field;
+  const upb_OneofDef* oneof;
+
+  if (!PyUpb_Message_LookupName(self, arg, &field, &oneof, PyExc_ValueError)) {
+    return NULL;
+  }
+
+  if (field && !upb_FieldDef_HasPresence(field)) {
+    PyErr_Format(PyExc_ValueError, "Field %s does not have presence.",
+                 upb_FieldDef_FullName(field));
+    return NULL;
+  }
+
+  if (PyUpb_Message_IsStub(self)) Py_RETURN_FALSE;
+
+  return PyBool_FromLong(field ? upb_Message_HasFieldByDef(self->ptr.msg, field)
+                               : upb_Message_WhichOneof(self->ptr.msg, oneof) !=
+                                     NULL);
+}
+
+static PyObject* PyUpb_Message_FindInitializationErrors(PyObject* _self,
+                                                        PyObject* arg);
+
+static PyObject* PyUpb_Message_IsInitializedAppendErrors(PyObject* _self,
+                                                         PyObject* errors) {
+  PyObject* list = PyUpb_Message_FindInitializationErrors(_self, NULL);
+  if (!list) return NULL;
+  bool ok = PyList_Size(list) == 0;
+  PyObject* ret = NULL;
+  PyObject* extend_result = NULL;
+  if (!ok) {
+    extend_result = PyObject_CallMethod(errors, "extend", "O", list);
+    if (!extend_result) goto done;
+  }
+  ret = PyBool_FromLong(ok);
+
+done:
+  Py_XDECREF(list);
+  Py_XDECREF(extend_result);
+  return ret;
+}
+
+static PyObject* PyUpb_Message_IsInitialized(PyObject* _self, PyObject* args) {
+  PyObject* errors = NULL;
+  if (!PyArg_ParseTuple(args, "|O", &errors)) {
+    return NULL;
+  }
+  if (errors) {
+    // We need to collect a list of unset required fields and append it to
+    // `errors`.
+    return PyUpb_Message_IsInitializedAppendErrors(_self, errors);
+  } else {
+    // We just need to return a boolean "true" or "false" for whether all
+    // required fields are set.
+    upb_Message* msg = PyUpb_Message_GetIfReified(_self);
+    const upb_MessageDef* m = PyUpb_Message_GetMsgdef(_self);
+    const upb_DefPool* symtab = upb_FileDef_Pool(upb_MessageDef_File(m));
+    bool initialized = !upb_util_HasUnsetRequired(msg, m, symtab, NULL);
+    return PyBool_FromLong(initialized);
+  }
+}
+
+static PyObject* PyUpb_Message_ListFieldsItemKey(PyObject* self,
+                                                 PyObject* val) {
+  assert(PyTuple_Check(val));
+  PyObject* field = PyTuple_GetItem(val, 0);
+  const upb_FieldDef* f = PyUpb_FieldDescriptor_GetDef(field);
+  return PyLong_FromLong(upb_FieldDef_Number(f));
+}
+
+static PyObject* PyUpb_Message_CheckCalledFromGeneratedFile(
+    PyObject* unused, PyObject* unused_arg) {
+  PyErr_SetString(
+      PyExc_TypeError,
+      "Descriptors cannot be created directly.\n"
+      "If this call came from a _pb2.py file, your generated code is out of "
+      "date and must be regenerated with protoc >= 3.19.0.\n"
+      "If you cannot immediately regenerate your protos, some other possible "
+      "workarounds are:\n"
+      " 1. Downgrade the protobuf package to 3.20.x or lower.\n"
+      " 2. Set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python (but this will "
+      "use pure-Python parsing and will be much slower).\n"
+      "\n"
+      "More information: "
+      "https://developers.google.com/protocol-buffers/docs/news/"
+      "2022-05-06#python-updates");
+  return NULL;
+}
+
+static bool PyUpb_Message_SortFieldList(PyObject* list) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  bool ok = false;
+  PyObject* args = PyTuple_New(0);
+  PyObject* kwargs = PyDict_New();
+  PyObject* method = PyObject_GetAttrString(list, "sort");
+  PyObject* call_result = NULL;
+  if (!args || !kwargs || !method) goto err;
+  if (PyDict_SetItemString(kwargs, "key", state->listfields_item_key) < 0) {
+    goto err;
+  }
+  call_result = PyObject_Call(method, args, kwargs);
+  if (!call_result) goto err;
+  ok = true;
+
+err:
+  Py_XDECREF(method);
+  Py_XDECREF(args);
+  Py_XDECREF(kwargs);
+  Py_XDECREF(call_result);
+  return ok;
+}
+
+static PyObject* PyUpb_Message_ListFields(PyObject* _self, PyObject* arg) {
+  PyObject* list = PyList_New(0);
+  upb_Message* msg = PyUpb_Message_GetIfReified(_self);
+  if (!msg) return list;
+
+  size_t iter1 = kUpb_Message_Begin;
+  const upb_MessageDef* m = PyUpb_Message_GetMsgdef(_self);
+  const upb_DefPool* symtab = upb_FileDef_Pool(upb_MessageDef_File(m));
+  const upb_FieldDef* f;
+  PyObject* field_desc = NULL;
+  PyObject* py_val = NULL;
+  PyObject* tuple = NULL;
+  upb_MessageValue val;
+  uint32_t last_field = 0;
+  bool in_order = true;
+  while (upb_Message_Next(msg, m, symtab, &f, &val, &iter1)) {
+    const uint32_t field_number = upb_FieldDef_Number(f);
+    if (field_number < last_field) in_order = false;
+    last_field = field_number;
+    PyObject* field_desc = PyUpb_FieldDescriptor_Get(f);
+    PyObject* py_val = PyUpb_Message_GetFieldValue(_self, f);
+    if (!field_desc || !py_val) goto err;
+    PyObject* tuple = Py_BuildValue("(NN)", field_desc, py_val);
+    field_desc = NULL;
+    py_val = NULL;
+    if (!tuple) goto err;
+    if (PyList_Append(list, tuple)) goto err;
+    Py_DECREF(tuple);
+    tuple = NULL;
+  }
+
+  // Users rely on fields being returned in field number order.
+  if (!in_order && !PyUpb_Message_SortFieldList(list)) goto err;
+
+  return list;
+
+err:
+  Py_XDECREF(field_desc);
+  Py_XDECREF(py_val);
+  Py_XDECREF(tuple);
+  Py_DECREF(list);
+  return NULL;
+}
+
+PyObject* PyUpb_Message_MergeFrom(PyObject* self, PyObject* arg) {
+  if (self->ob_type != arg->ob_type) {
+    PyErr_Format(PyExc_TypeError,
+                 "Parameter to MergeFrom() must be instance of same class: "
+                 "expected %S got %S.",
+                 Py_TYPE(self), Py_TYPE(arg));
+    return NULL;
+  }
+  // OPT: exit if src is empty.
+  PyObject* subargs = PyTuple_New(0);
+  PyObject* serialized =
+      PyUpb_Message_SerializePartialToString(arg, subargs, NULL);
+  Py_DECREF(subargs);
+  if (!serialized) return NULL;
+  PyObject* ret = PyUpb_Message_MergeFromString(self, serialized);
+  Py_DECREF(serialized);
+  Py_XDECREF(ret);
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_Message_Clear(PyUpb_Message* self);
+
+static PyObject* PyUpb_Message_CopyFrom(PyObject* _self, PyObject* arg) {
+  if (_self->ob_type != arg->ob_type) {
+    PyErr_Format(PyExc_TypeError,
+                 "Parameter to CopyFrom() must be instance of same class: "
+                 "expected %S got %S.",
+                 Py_TYPE(_self), Py_TYPE(arg));
+    return NULL;
+  }
+  if (_self == arg) {
+    Py_RETURN_NONE;
+  }
+  PyUpb_Message* self = (void*)_self;
+  PyUpb_Message* other = (void*)arg;
+  PyUpb_Message_EnsureReified(self);
+
+  const upb_Message* other_msg = PyUpb_Message_GetIfReified((PyObject*)other);
+  if (other_msg) {
+    upb_Message_DeepCopy(
+        self->ptr.msg, other_msg,
+        upb_MessageDef_MiniTable((const upb_MessageDef*)other->def),
+        PyUpb_Arena_Get(self->arena));
+  } else {
+    PyObject* tmp = PyUpb_Message_Clear(self);
+    Py_DECREF(tmp);
+  }
+  PyUpb_Message_SyncSubobjs(self);
+
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_Message_SetInParent(PyObject* _self, PyObject* arg) {
+  PyUpb_Message* self = (void*)_self;
+  PyUpb_Message_EnsureReified(self);
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_Message_UnknownFields(PyObject* _self, PyObject* arg) {
+  // TODO: re-enable when unknown fields are added.
+  // return PyUpb_UnknownFields_New(_self);
+  PyErr_SetString(PyExc_NotImplementedError, "unknown field accessor");
+  return NULL;
+}
+
+PyObject* PyUpb_Message_MergeFromString(PyObject* _self, PyObject* arg) {
+  PyUpb_Message* self = (void*)_self;
+  char* buf;
+  Py_ssize_t size;
+  PyObject* bytes = NULL;
+
+  if (PyMemoryView_Check(arg)) {
+    bytes = PyBytes_FromObject(arg);
+    // Cannot fail when passed something of the correct type.
+    int err = PyBytes_AsStringAndSize(bytes, &buf, &size);
+    (void)err;
+    assert(err >= 0);
+  } else if (PyBytes_AsStringAndSize(arg, &buf, &size) < 0) {
+    return NULL;
+  }
+
+  PyUpb_Message_EnsureReified(self);
+  const upb_MessageDef* msgdef = _PyUpb_Message_GetMsgdef(self);
+  const upb_FileDef* file = upb_MessageDef_File(msgdef);
+  const upb_ExtensionRegistry* extreg =
+      upb_DefPool_ExtensionRegistry(upb_FileDef_Pool(file));
+  const upb_MiniTable* layout = upb_MessageDef_MiniTable(msgdef);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  int options =
+      upb_DecodeOptions_MaxDepth(state->allow_oversize_protos ? UINT16_MAX : 0);
+  upb_DecodeStatus status =
+      upb_Decode(buf, size, self->ptr.msg, layout, extreg, options, arena);
+  Py_XDECREF(bytes);
+  if (status != kUpb_DecodeStatus_Ok) {
+    PyErr_Format(state->decode_error_class, "Error parsing message");
+    return NULL;
+  }
+  PyUpb_Message_SyncSubobjs(self);
+  return PyLong_FromSsize_t(size);
+}
+
+static PyObject* PyUpb_Message_ParseFromString(PyObject* self, PyObject* arg) {
+  PyObject* tmp = PyUpb_Message_Clear((PyUpb_Message*)self);
+  Py_DECREF(tmp);
+  return PyUpb_Message_MergeFromString(self, arg);
+}
+
+static PyObject* PyUpb_Message_ByteSize(PyObject* self, PyObject* args) {
+  // TODO: At the
+  // moment upb does not have a "byte size" function, so we just serialize to
+  // string and get the size of the string.
+  PyObject* subargs = PyTuple_New(0);
+  PyObject* serialized = PyUpb_Message_SerializeToString(self, subargs, NULL);
+  Py_DECREF(subargs);
+  if (!serialized) return NULL;
+  size_t size = PyBytes_Size(serialized);
+  Py_DECREF(serialized);
+  return PyLong_FromSize_t(size);
+}
+
+static PyObject* PyUpb_Message_Clear(PyUpb_Message* self) {
+  PyUpb_Message_EnsureReified(self);
+  const upb_MessageDef* msgdef = _PyUpb_Message_GetMsgdef(self);
+  PyUpb_WeakMap* subobj_map = self->unset_subobj_map;
+
+  if (subobj_map) {
+    upb_Message* msg = PyUpb_Message_GetMsg(self);
+    (void)msg;  // Suppress unused warning when asserts are disabled.
+    intptr_t iter = PYUPB_WEAKMAP_BEGIN;
+    const void* key;
+    PyObject* obj;
+
+    while (PyUpb_WeakMap_Next(subobj_map, &key, &obj, &iter)) {
+      const upb_FieldDef* f = key;
+      PyUpb_WeakMap_DeleteIter(subobj_map, &iter);
+      if (upb_FieldDef_IsMap(f)) {
+        assert(upb_Message_GetFieldByDef(msg, f).map_val == NULL);
+        PyUpb_MapContainer_Reify(obj, NULL);
+      } else if (upb_FieldDef_IsRepeated(f)) {
+        assert(upb_Message_GetFieldByDef(msg, f).array_val == NULL);
+        PyUpb_RepeatedContainer_Reify(obj, NULL);
+      } else {
+        assert(!upb_Message_HasFieldByDef(msg, f));
+        PyUpb_Message* sub = (void*)obj;
+        assert(self == sub->ptr.parent);
+        PyUpb_Message_Reify(sub, f, NULL);
+      }
+    }
+  }
+
+  upb_Message_ClearByDef(self->ptr.msg, msgdef);
+  Py_RETURN_NONE;
+}
+
+void PyUpb_Message_DoClearField(PyObject* _self, const upb_FieldDef* f) {
+  PyUpb_Message* self = (void*)_self;
+  PyUpb_Message_EnsureReified((PyUpb_Message*)self);
+
+  // We must ensure that any stub object is reified so its parent no longer
+  // points to us.
+  PyObject* sub = self->unset_subobj_map
+                      ? PyUpb_WeakMap_Get(self->unset_subobj_map, f)
+                      : NULL;
+
+  if (upb_FieldDef_IsMap(f)) {
+    // For maps we additionally have to invalidate any iterators.  So we need
+    // to get an object even if it's reified.
+    if (!sub) {
+      sub = PyUpb_Message_GetFieldValue(_self, f);
+    }
+    PyUpb_MapContainer_EnsureReified(sub);
+    PyUpb_MapContainer_Invalidate(sub);
+  } else if (upb_FieldDef_IsRepeated(f)) {
+    if (sub) {
+      PyUpb_RepeatedContainer_EnsureReified(sub);
+    }
+  } else if (upb_FieldDef_IsSubMessage(f)) {
+    if (sub) {
+      PyUpb_Message_EnsureReified((PyUpb_Message*)sub);
+    }
+  }
+
+  Py_XDECREF(sub);
+  upb_Message_ClearFieldByDef(self->ptr.msg, f);
+}
+
+static PyObject* PyUpb_Message_ClearExtension(PyObject* _self, PyObject* arg) {
+  PyUpb_Message* self = (void*)_self;
+  PyUpb_Message_EnsureReified(self);
+  const upb_FieldDef* f = PyUpb_Message_GetExtensionDef(_self, arg);
+  if (!f) return NULL;
+  PyUpb_Message_DoClearField(_self, f);
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_Message_ClearField(PyObject* _self, PyObject* arg) {
+  PyUpb_Message* self = (void*)_self;
+
+  // We always need EnsureReified() here (even for an unset message) to
+  // preserve behavior like:
+  //   msg = FooMessage()
+  //   msg.foo.Clear()
+  //   assert msg.HasField("foo")
+  PyUpb_Message_EnsureReified(self);
+
+  const upb_FieldDef* f;
+  const upb_OneofDef* o;
+  if (!PyUpb_Message_LookupName(self, arg, &f, &o, PyExc_ValueError)) {
+    return NULL;
+  }
+
+  if (o) f = upb_Message_WhichOneof(self->ptr.msg, o);
+  if (f) PyUpb_Message_DoClearField(_self, f);
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_Message_DiscardUnknownFields(PyUpb_Message* self,
+                                                    PyObject* arg) {
+  PyUpb_Message_EnsureReified(self);
+  const upb_MessageDef* msgdef = _PyUpb_Message_GetMsgdef(self);
+  upb_Message_DiscardUnknown(self->ptr.msg, msgdef, 64);
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_Message_FindInitializationErrors(PyObject* _self,
+                                                        PyObject* arg) {
+  PyUpb_Message* self = (void*)_self;
+  upb_Message* msg = PyUpb_Message_GetIfReified(_self);
+  const upb_MessageDef* msgdef = _PyUpb_Message_GetMsgdef(self);
+  const upb_DefPool* ext_pool = upb_FileDef_Pool(upb_MessageDef_File(msgdef));
+  upb_FieldPathEntry* fields_base;
+  PyObject* ret = PyList_New(0);
+  if (upb_util_HasUnsetRequired(msg, msgdef, ext_pool, &fields_base)) {
+    upb_FieldPathEntry* fields = fields_base;
+    char* buf = NULL;
+    size_t size = 0;
+    assert(fields->field);
+    while (fields->field) {
+      upb_FieldPathEntry* field = fields;
+      size_t need = upb_FieldPath_ToText(&fields, buf, size);
+      if (need >= size) {
+        fields = field;
+        size = size ? size * 2 : 16;
+        while (size <= need) size *= 2;
+        buf = realloc(buf, size);
+        need = upb_FieldPath_ToText(&fields, buf, size);
+        assert(size > need);
+      }
+      PyObject* str = PyUnicode_FromString(buf);
+      PyList_Append(ret, str);
+      Py_DECREF(str);
+    }
+    free(buf);
+    free(fields_base);
+  }
+  return ret;
+}
+
+static PyObject* PyUpb_Message_FromString(PyObject* cls, PyObject* serialized) {
+  PyObject* ret = NULL;
+  PyObject* length = NULL;
+
+  ret = PyObject_CallObject(cls, NULL);
+  if (ret == NULL) goto err;
+  length = PyUpb_Message_MergeFromString(ret, serialized);
+  if (length == NULL) goto err;
+
+done:
+  Py_XDECREF(length);
+  return ret;
+
+err:
+  Py_XDECREF(ret);
+  ret = NULL;
+  goto done;
+}
+
+const upb_FieldDef* PyUpb_Message_GetExtensionDef(PyObject* _self,
+                                                  PyObject* key) {
+  const upb_FieldDef* f = PyUpb_FieldDescriptor_GetDef(key);
+  if (!f) {
+    PyErr_Clear();
+    PyErr_Format(PyExc_KeyError, "Object %R is not a field descriptor\n", key);
+    return NULL;
+  }
+  if (!upb_FieldDef_IsExtension(f)) {
+    PyErr_Format(PyExc_KeyError, "Field %s is not an extension\n",
+                 upb_FieldDef_FullName(f));
+    return NULL;
+  }
+  const upb_MessageDef* msgdef = PyUpb_Message_GetMsgdef(_self);
+  if (upb_FieldDef_ContainingType(f) != msgdef) {
+    PyErr_Format(PyExc_KeyError, "Extension doesn't match (%s vs %s)",
+                 upb_MessageDef_FullName(msgdef), upb_FieldDef_FullName(f));
+    return NULL;
+  }
+  return f;
+}
+
+static PyObject* PyUpb_Message_HasExtension(PyObject* _self,
+                                            PyObject* ext_desc) {
+  upb_Message* msg = PyUpb_Message_GetIfReified(_self);
+  const upb_FieldDef* f = PyUpb_Message_GetExtensionDef(_self, ext_desc);
+  if (!f) return NULL;
+  if (upb_FieldDef_IsRepeated(f)) {
+    PyErr_SetString(PyExc_KeyError,
+                    "Field is repeated. A singular method is required.");
+    return NULL;
+  }
+  if (!msg) Py_RETURN_FALSE;
+  return PyBool_FromLong(upb_Message_HasFieldByDef(msg, f));
+}
+
+void PyUpb_Message_ReportInitializationErrors(const upb_MessageDef* msgdef,
+                                              PyObject* errors, PyObject* exc) {
+  PyObject* comma = PyUnicode_FromString(",");
+  PyObject* missing_fields = NULL;
+  if (!comma) goto done;
+  missing_fields = PyUnicode_Join(comma, errors);
+  if (!missing_fields) goto done;
+  PyErr_Format(exc, "Message %s is missing required fields: %U",
+               upb_MessageDef_FullName(msgdef), missing_fields);
+done:
+  Py_XDECREF(comma);
+  Py_XDECREF(missing_fields);
+  Py_DECREF(errors);
+}
+
+PyObject* PyUpb_Message_SerializeInternal(PyObject* _self, PyObject* args,
+                                          PyObject* kwargs,
+                                          bool check_required) {
+  PyUpb_Message* self = (void*)_self;
+  if (!PyUpb_Message_Verify((PyObject*)self)) return NULL;
+  static const char* kwlist[] = {"deterministic", NULL};
+  int deterministic = 0;
+  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|p", (char**)(kwlist),
+                                   &deterministic)) {
+    return NULL;
+  }
+
+  const upb_MessageDef* msgdef = _PyUpb_Message_GetMsgdef(self);
+  if (PyUpb_Message_IsStub(self)) {
+    // Nothing to serialize, but we do have to check whether the message is
+    // initialized.
+    PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+    PyObject* errors = PyUpb_Message_FindInitializationErrors(_self, NULL);
+    if (!errors) return NULL;
+    if (PyList_Size(errors) == 0) {
+      Py_DECREF(errors);
+      return PyBytes_FromStringAndSize(NULL, 0);
+    }
+    PyUpb_Message_ReportInitializationErrors(msgdef, errors,
+                                             state->encode_error_class);
+    return NULL;
+  }
+
+  upb_Arena* arena = upb_Arena_New();
+  const upb_MiniTable* layout = upb_MessageDef_MiniTable(msgdef);
+  size_t size = 0;
+  // Python does not currently have any effective limit on serialization depth.
+  int options = upb_EncodeOptions_MaxDepth(UINT16_MAX);
+  if (check_required) options |= kUpb_EncodeOption_CheckRequired;
+  if (deterministic) options |= kUpb_EncodeOption_Deterministic;
+  char* pb;
+  upb_EncodeStatus status =
+      upb_Encode(self->ptr.msg, layout, options, arena, &pb, &size);
+  PyObject* ret = NULL;
+
+  if (status != kUpb_EncodeStatus_Ok) {
+    PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+    PyObject* errors = PyUpb_Message_FindInitializationErrors(_self, NULL);
+    if (PyList_Size(errors) != 0) {
+      PyUpb_Message_ReportInitializationErrors(msgdef, errors,
+                                               state->encode_error_class);
+    } else {
+      PyErr_Format(state->encode_error_class, "Failed to serialize proto");
+    }
+    goto done;
+  }
+
+  ret = PyBytes_FromStringAndSize(pb, size);
+
+done:
+  upb_Arena_Free(arena);
+  return ret;
+}
+
+PyObject* PyUpb_Message_SerializeToString(PyObject* _self, PyObject* args,
+                                          PyObject* kwargs) {
+  return PyUpb_Message_SerializeInternal(_self, args, kwargs, true);
+}
+
+PyObject* PyUpb_Message_SerializePartialToString(PyObject* _self,
+                                                 PyObject* args,
+                                                 PyObject* kwargs) {
+  return PyUpb_Message_SerializeInternal(_self, args, kwargs, false);
+}
+
+static PyObject* PyUpb_Message_WhichOneof(PyObject* _self, PyObject* name) {
+  PyUpb_Message* self = (void*)_self;
+  const upb_OneofDef* o;
+  if (!PyUpb_Message_LookupName(self, name, NULL, &o, PyExc_ValueError)) {
+    return NULL;
+  }
+  upb_Message* msg = PyUpb_Message_GetIfReified(_self);
+  if (!msg) Py_RETURN_NONE;
+  const upb_FieldDef* f = upb_Message_WhichOneof(msg, o);
+  if (!f) Py_RETURN_NONE;
+  return PyUnicode_FromString(upb_FieldDef_Name(f));
+}
+
+PyObject* DeepCopy(PyObject* _self, PyObject* arg) {
+  PyUpb_Message* self = (void*)_self;
+  const upb_MessageDef* def = PyUpb_Message_GetMsgdef(_self);
+
+  PyObject* arena = PyUpb_Arena_New();
+  upb_Message* clone = upb_Message_DeepClone(
+      self->ptr.msg, upb_MessageDef_MiniTable(def), PyUpb_Arena_Get(arena));
+  PyObject* ret = PyUpb_Message_Get(clone, def, arena);
+  Py_DECREF(arena);
+
+  return ret;
+}
+
+void PyUpb_Message_ClearExtensionDict(PyObject* _self) {
+  PyUpb_Message* self = (void*)_self;
+  assert(self->ext_dict);
+  self->ext_dict = NULL;
+}
+
+static PyObject* PyUpb_Message_GetExtensionDict(PyObject* _self,
+                                                void* closure) {
+  PyUpb_Message* self = (void*)_self;
+  if (self->ext_dict) {
+    Py_INCREF(self->ext_dict);
+    return self->ext_dict;
+  }
+
+  const upb_MessageDef* m = _PyUpb_Message_GetMsgdef(self);
+  if (upb_MessageDef_ExtensionRangeCount(m) == 0) {
+    PyErr_SetNone(PyExc_AttributeError);
+    return NULL;
+  }
+
+  self->ext_dict = PyUpb_ExtensionDict_New(_self);
+  return self->ext_dict;
+}
+
+static PyGetSetDef PyUpb_Message_Getters[] = {
+    {"Extensions", PyUpb_Message_GetExtensionDict, NULL, "Extension dict"},
+    {NULL}};
+
+static PyMethodDef PyUpb_Message_Methods[] = {
+    {"__deepcopy__", (PyCFunction)DeepCopy, METH_VARARGS,
+     "Makes a deep copy of the class."},
+    // TODO
+    //{ "__unicode__", (PyCFunction)ToUnicode, METH_NOARGS,
+    //  "Outputs a unicode representation of the message." },
+    {"ByteSize", (PyCFunction)PyUpb_Message_ByteSize, METH_NOARGS,
+     "Returns the size of the message in bytes."},
+    {"Clear", (PyCFunction)PyUpb_Message_Clear, METH_NOARGS,
+     "Clears the message."},
+    {"ClearExtension", PyUpb_Message_ClearExtension, METH_O,
+     "Clears a message field."},
+    {"ClearField", PyUpb_Message_ClearField, METH_O, "Clears a message field."},
+    {"CopyFrom", PyUpb_Message_CopyFrom, METH_O,
+     "Copies a protocol message into the current message."},
+    {"DiscardUnknownFields", (PyCFunction)PyUpb_Message_DiscardUnknownFields,
+     METH_NOARGS, "Discards the unknown fields."},
+    {"FindInitializationErrors", PyUpb_Message_FindInitializationErrors,
+     METH_NOARGS, "Finds unset required fields."},
+    {"FromString", PyUpb_Message_FromString, METH_O | METH_CLASS,
+     "Creates new method instance from given serialized data."},
+    {"HasExtension", PyUpb_Message_HasExtension, METH_O,
+     "Checks if a message field is set."},
+    {"HasField", PyUpb_Message_HasField, METH_O,
+     "Checks if a message field is set."},
+    {"IsInitialized", PyUpb_Message_IsInitialized, METH_VARARGS,
+     "Checks if all required fields of a protocol message are set."},
+    {"ListFields", PyUpb_Message_ListFields, METH_NOARGS,
+     "Lists all set fields of a message."},
+    {"MergeFrom", PyUpb_Message_MergeFrom, METH_O,
+     "Merges a protocol message into the current message."},
+    {"MergeFromString", PyUpb_Message_MergeFromString, METH_O,
+     "Merges a serialized message into the current message."},
+    {"ParseFromString", PyUpb_Message_ParseFromString, METH_O,
+     "Parses a serialized message into the current message."},
+    {"SerializePartialToString",
+     (PyCFunction)PyUpb_Message_SerializePartialToString,
+     METH_VARARGS | METH_KEYWORDS,
+     "Serializes the message to a string, even if it isn't initialized."},
+    {"SerializeToString", (PyCFunction)PyUpb_Message_SerializeToString,
+     METH_VARARGS | METH_KEYWORDS,
+     "Serializes the message to a string, only for initialized messages."},
+    {"SetInParent", (PyCFunction)PyUpb_Message_SetInParent, METH_NOARGS,
+     "Sets the has bit of the given field in its parent message."},
+    {"UnknownFields", (PyCFunction)PyUpb_Message_UnknownFields, METH_NOARGS,
+     "Parse unknown field set"},
+    {"WhichOneof", PyUpb_Message_WhichOneof, METH_O,
+     "Returns the name of the field set inside a oneof, "
+     "or None if no field is set."},
+    {"_ListFieldsItemKey", PyUpb_Message_ListFieldsItemKey,
+     METH_O | METH_STATIC,
+     "Compares ListFields() list entries by field number"},
+    {"_CheckCalledFromGeneratedFile",
+     PyUpb_Message_CheckCalledFromGeneratedFile, METH_NOARGS | METH_STATIC,
+     "Raises TypeError if the caller is not in a _pb2.py file."},
+    {NULL, NULL}};
+
+static PyType_Slot PyUpb_Message_Slots[] = {
+    {Py_tp_dealloc, PyUpb_Message_Dealloc},
+    {Py_tp_doc, "A ProtocolMessage"},
+    {Py_tp_getattro, PyUpb_Message_GetAttr},
+    {Py_tp_getset, PyUpb_Message_Getters},
+    {Py_tp_hash, PyObject_HashNotImplemented},
+    {Py_tp_methods, PyUpb_Message_Methods},
+    {Py_tp_new, PyUpb_Message_New},
+    {Py_tp_str, PyUpb_Message_ToString},
+    {Py_tp_repr, PyUpb_Message_ToString},
+    {Py_tp_richcompare, PyUpb_Message_RichCompare},
+    {Py_tp_setattro, PyUpb_Message_SetAttr},
+    {Py_tp_init, PyUpb_Message_Init},
+    {0, NULL}};
+
+PyType_Spec PyUpb_Message_Spec = {
+    PYUPB_MODULE_NAME ".Message",              // tp_name
+    sizeof(PyUpb_Message),                     // tp_basicsize
+    0,                                         // tp_itemsize
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,  // tp_flags
+    PyUpb_Message_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// MessageMeta
+// -----------------------------------------------------------------------------
+
+// MessageMeta is the metaclass for message objects.  The generated code uses it
+// to construct message classes, ie.
+//
+// FooMessage = _message.MessageMeta('FooMessage', (_message.Message), {...})
+//
+// (This is not quite true: at the moment the Python library subclasses
+// MessageMeta, and uses that subclass as the metaclass.  There is a TODO below
+// to simplify this, so that the illustration above is indeed accurate).
+
+typedef struct {
+  const upb_MiniTable* layout;
+  PyObject* py_message_descriptor;
+} PyUpb_MessageMeta;
+
+// The PyUpb_MessageMeta struct is trailing data tacked onto the end of
+// MessageMeta instances.  This means that we get our instances of this struct
+// by adding the appropriate number of bytes.
+static PyUpb_MessageMeta* PyUpb_GetMessageMeta(PyObject* cls) {
+#ifndef NDEBUG
+  PyUpb_ModuleState* state = PyUpb_ModuleState_MaybeGet();
+  assert(!state || cls->ob_type == state->message_meta_type);
+#endif
+  return (PyUpb_MessageMeta*)((char*)cls + cpython_bits.type_basicsize);
+}
+
+static const upb_MessageDef* PyUpb_MessageMeta_GetMsgdef(PyObject* cls) {
+  PyUpb_MessageMeta* self = PyUpb_GetMessageMeta(cls);
+  return PyUpb_Descriptor_GetDef(self->py_message_descriptor);
+}
+
+PyObject* PyUpb_MessageMeta_DoCreateClass(PyObject* py_descriptor,
+                                          const char* name, PyObject* dict) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyTypeObject* descriptor_type = state->descriptor_types[kPyUpb_Descriptor];
+  if (!PyObject_TypeCheck(py_descriptor, descriptor_type)) {
+    return PyErr_Format(PyExc_TypeError, "Expected a message Descriptor");
+  }
+
+  const upb_MessageDef* msgdef = PyUpb_Descriptor_GetDef(py_descriptor);
+  assert(msgdef);
+  assert(!PyUpb_ObjCache_Get(upb_MessageDef_MiniTable(msgdef)));
+
+  PyObject* slots = PyTuple_New(0);
+  if (!slots) return NULL;
+  int status = PyDict_SetItemString(dict, "__slots__", slots);
+  Py_DECREF(slots);
+  if (status < 0) return NULL;
+
+  // Bases are either:
+  //    (Message, Message)            # for regular messages
+  //    (Message, Message, WktBase)   # For well-known types
+  PyObject* wkt_bases = PyUpb_GetWktBases(state);
+  PyObject* wkt_base =
+      PyDict_GetItemString(wkt_bases, upb_MessageDef_FullName(msgdef));
+  PyObject* args;
+  if (wkt_base == NULL) {
+    args = Py_BuildValue("s(OO)O", name, state->cmessage_type,
+                         state->message_class, dict);
+  } else {
+    args = Py_BuildValue("s(OOO)O", name, state->cmessage_type,
+                         state->message_class, wkt_base, dict);
+  }
+
+  PyObject* ret = cpython_bits.type_new(state->message_meta_type, args, NULL);
+  Py_DECREF(args);
+  if (!ret) return NULL;
+
+  PyUpb_MessageMeta* meta = PyUpb_GetMessageMeta(ret);
+  meta->py_message_descriptor = py_descriptor;
+  meta->layout = upb_MessageDef_MiniTable(msgdef);
+  Py_INCREF(meta->py_message_descriptor);
+  PyUpb_Descriptor_SetClass(py_descriptor, ret);
+
+  PyUpb_ObjCache_Add(meta->layout, ret);
+
+  return ret;
+}
+
+static PyObject* PyUpb_MessageMeta_New(PyTypeObject* type, PyObject* args,
+                                       PyObject* kwargs) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  static const char* kwlist[] = {"name", "bases", "dict", 0};
+  PyObject *bases, *dict;
+  const char* name;
+
+  // Check arguments: (name, bases, dict)
+  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO!O!:type", (char**)kwlist,
+                                   &name, &PyTuple_Type, &bases, &PyDict_Type,
+                                   &dict)) {
+    return NULL;
+  }
+
+  // Check bases: only (), or (message.Message,) are allowed
+  Py_ssize_t size = PyTuple_Size(bases);
+  if (!(size == 0 ||
+        (size == 1 && PyTuple_GetItem(bases, 0) == state->message_class))) {
+    PyErr_Format(PyExc_TypeError,
+                 "A Message class can only inherit from Message, not %S",
+                 bases);
+    return NULL;
+  }
+
+  // Check dict['DESCRIPTOR']
+  PyObject* py_descriptor = PyDict_GetItemString(dict, "DESCRIPTOR");
+  if (py_descriptor == NULL) {
+    PyErr_SetString(PyExc_TypeError, "Message class has no DESCRIPTOR");
+    return NULL;
+  }
+
+  const upb_MessageDef* m = PyUpb_Descriptor_GetDef(py_descriptor);
+  PyObject* ret = PyUpb_ObjCache_Get(upb_MessageDef_MiniTable(m));
+  if (ret) return ret;
+  return PyUpb_MessageMeta_DoCreateClass(py_descriptor, name, dict);
+}
+
+static void PyUpb_MessageMeta_Dealloc(PyObject* self) {
+  PyUpb_MessageMeta* meta = PyUpb_GetMessageMeta(self);
+  PyUpb_ObjCache_Delete(meta->layout);
+  Py_DECREF(meta->py_message_descriptor);
+  PyTypeObject* tp = Py_TYPE(self);
+  cpython_bits.type_dealloc(self);
+  Py_DECREF(tp);
+}
+
+void PyUpb_MessageMeta_AddFieldNumber(PyObject* self, const upb_FieldDef* f) {
+  PyObject* name =
+      PyUnicode_FromFormat("%s_FIELD_NUMBER", upb_FieldDef_Name(f));
+  PyObject* upper = PyObject_CallMethod(name, "upper", "");
+  PyObject_SetAttr(self, upper, PyLong_FromLong(upb_FieldDef_Number(f)));
+  Py_DECREF(name);
+  Py_DECREF(upper);
+}
+
+static PyObject* PyUpb_MessageMeta_GetDynamicAttr(PyObject* self,
+                                                  PyObject* name) {
+  const char* name_buf = PyUpb_GetStrData(name);
+  if (!name_buf) return NULL;
+  const upb_MessageDef* msgdef = PyUpb_MessageMeta_GetMsgdef(self);
+  const upb_FileDef* filedef = upb_MessageDef_File(msgdef);
+  const upb_DefPool* symtab = upb_FileDef_Pool(filedef);
+
+  PyObject* py_key =
+      PyBytes_FromFormat("%s.%s", upb_MessageDef_FullName(msgdef), name_buf);
+  const char* key = PyUpb_GetStrData(py_key);
+  PyObject* ret = NULL;
+  const upb_MessageDef* nested = upb_DefPool_FindMessageByName(symtab, key);
+  const upb_EnumDef* enumdef;
+  const upb_EnumValueDef* enumval;
+  const upb_FieldDef* ext;
+
+  if (nested) {
+    ret = PyUpb_Descriptor_GetClass(nested);
+  } else if ((enumdef = upb_DefPool_FindEnumByName(symtab, key))) {
+    PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+    PyObject* klass = state->enum_type_wrapper_class;
+    ret = PyUpb_EnumDescriptor_Get(enumdef);
+    ret = PyObject_CallFunctionObjArgs(klass, ret, NULL);
+  } else if ((enumval = upb_DefPool_FindEnumByNameval(symtab, key))) {
+    ret = PyLong_FromLong(upb_EnumValueDef_Number(enumval));
+  } else if ((ext = upb_DefPool_FindExtensionByName(symtab, key))) {
+    ret = PyUpb_FieldDescriptor_Get(ext);
+  }
+
+  Py_DECREF(py_key);
+
+  const char* suffix = "_FIELD_NUMBER";
+  size_t n = strlen(name_buf);
+  size_t suffix_n = strlen(suffix);
+  if (n > suffix_n && memcmp(suffix, name_buf + n - suffix_n, suffix_n) == 0) {
+    // We can't look up field names dynamically, because the <NAME>_FIELD_NUMBER
+    // naming scheme upper-cases the field name and is therefore non-reversible.
+    // So we just add all field numbers.
+    int n = upb_MessageDef_FieldCount(msgdef);
+    for (int i = 0; i < n; i++) {
+      PyUpb_MessageMeta_AddFieldNumber(self, upb_MessageDef_Field(msgdef, i));
+    }
+    n = upb_MessageDef_NestedExtensionCount(msgdef);
+    for (int i = 0; i < n; i++) {
+      PyUpb_MessageMeta_AddFieldNumber(
+          self, upb_MessageDef_NestedExtension(msgdef, i));
+    }
+    ret = PyObject_GenericGetAttr(self, name);
+  }
+
+  return ret;
+}
+
+static PyObject* PyUpb_MessageMeta_GetAttr(PyObject* self, PyObject* name) {
+  // We want to first delegate to the type's tp_dict to retrieve any attributes
+  // that were previously calculated and cached in the type's dict.
+  PyObject* ret = cpython_bits.type_getattro(self, name);
+  if (ret) return ret;
+
+  // We did not find a cached attribute. Try to calculate the attribute
+  // dynamically, using the descriptor as an argument.
+  PyErr_Clear();
+  ret = PyUpb_MessageMeta_GetDynamicAttr(self, name);
+
+  if (ret) {
+    PyObject_SetAttr(self, name, ret);
+    PyErr_Clear();
+    return ret;
+  }
+
+  PyErr_SetObject(PyExc_AttributeError, name);
+  return NULL;
+}
+
+static int PyUpb_MessageMeta_Traverse(PyObject* self, visitproc visit,
+                                      void* arg) {
+  PyUpb_MessageMeta* meta = PyUpb_GetMessageMeta(self);
+  Py_VISIT(meta->py_message_descriptor);
+  return cpython_bits.type_traverse(self, visit, arg);
+}
+
+static int PyUpb_MessageMeta_Clear(PyObject* self, visitproc visit, void* arg) {
+  return cpython_bits.type_clear(self);
+}
+
+static PyType_Slot PyUpb_MessageMeta_Slots[] = {
+    {Py_tp_new, PyUpb_MessageMeta_New},
+    {Py_tp_dealloc, PyUpb_MessageMeta_Dealloc},
+    {Py_tp_getattro, PyUpb_MessageMeta_GetAttr},
+    {Py_tp_traverse, PyUpb_MessageMeta_Traverse},
+    {Py_tp_clear, PyUpb_MessageMeta_Clear},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_MessageMeta_Spec = {
+    PYUPB_MODULE_NAME ".MessageMeta",  // tp_name
+    0,  // To be filled in by size of base     // tp_basicsize
+    0,  // tp_itemsize
+    // TODO: remove BASETYPE, Python should just use MessageMeta
+    // directly instead of subclassing it.
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,  // tp_flags
+    PyUpb_MessageMeta_Slots,
+};
+
+static PyObject* PyUpb_MessageMeta_CreateType(void) {
+  PyObject* bases = Py_BuildValue("(O)", &PyType_Type);
+  if (!bases) return NULL;
+  PyUpb_MessageMeta_Spec.basicsize =
+      cpython_bits.type_basicsize + sizeof(PyUpb_MessageMeta);
+  PyObject* type = PyType_FromSpecWithBases(&PyUpb_MessageMeta_Spec, bases);
+  Py_DECREF(bases);
+  return type;
+}
+
+bool PyUpb_InitMessage(PyObject* m) {
+  if (!PyUpb_CPythonBits_Init(&cpython_bits)) return false;
+  PyObject* message_meta_type = PyUpb_MessageMeta_CreateType();
+
+  PyUpb_ModuleState* state = PyUpb_ModuleState_GetFromModule(m);
+  state->cmessage_type = PyUpb_AddClass(m, &PyUpb_Message_Spec);
+  state->message_meta_type = (PyTypeObject*)message_meta_type;
+
+  if (!state->cmessage_type || !state->message_meta_type) return false;
+  if (PyModule_AddObject(m, "MessageMeta", message_meta_type)) return false;
+  state->listfields_item_key = PyObject_GetAttrString(
+      (PyObject*)state->cmessage_type, "_ListFieldsItemKey");
+
+  PyObject* mod =
+      PyImport_ImportModule(PYUPB_PROTOBUF_PUBLIC_PACKAGE ".message");
+  if (mod == NULL) return false;
+
+  state->encode_error_class = PyObject_GetAttrString(mod, "EncodeError");
+  state->decode_error_class = PyObject_GetAttrString(mod, "DecodeError");
+  state->message_class = PyObject_GetAttrString(mod, "Message");
+  Py_DECREF(mod);
+
+  PyObject* enum_type_wrapper = PyImport_ImportModule(
+      PYUPB_PROTOBUF_INTERNAL_PACKAGE ".enum_type_wrapper");
+  if (enum_type_wrapper == NULL) return false;
+
+  state->enum_type_wrapper_class =
+      PyObject_GetAttrString(enum_type_wrapper, "EnumTypeWrapper");
+  Py_DECREF(enum_type_wrapper);
+
+  if (!state->encode_error_class || !state->decode_error_class ||
+      !state->message_class || !state->listfields_item_key ||
+      !state->enum_type_wrapper_class) {
+    return false;
+  }
+
+  return true;
+}
diff --git a/python/message.h b/python/message.h
new file mode 100644
index 0000000..d497f61
--- /dev/null
+++ b/python/message.h
@@ -0,0 +1,110 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYPB_MESSAGE_H__
+#define PYPB_MESSAGE_H__
+
+#include <stdbool.h>
+
+#include "python/protobuf.h"
+#include "upb/reflection/message.h"
+
+// Removes the wrapper object for this field from the unset subobject cache.
+void PyUpb_Message_CacheDelete(PyObject* _self, const upb_FieldDef* f);
+
+// Sets the field value for `f` to `subobj`, evicting the wrapper object from
+// the "unset subobject" cache now that real data exists for it.  The caller
+// must also update the wrapper associated with `f` to point to `subobj` also.
+void PyUpb_Message_SetConcreteSubobj(PyObject* _self, const upb_FieldDef* f,
+                                     upb_MessageValue subobj);
+
+// Gets a Python wrapper object for message `u_msg` of type `m`, returning a
+// cached wrapper if one was previously created.  If a new object is created,
+// it will reference `arena`, which must own `u_msg`.
+PyObject* PyUpb_Message_Get(upb_Message* u_msg, const upb_MessageDef* m,
+                            PyObject* arena);
+
+// Verifies that a Python object is a message.  Sets a TypeError exception and
+// returns false on failure.
+bool PyUpb_Message_Verify(PyObject* self);
+
+// Gets the upb_Message* for this message object if the message is reified.
+// Otherwise returns NULL.
+upb_Message* PyUpb_Message_GetIfReified(PyObject* _self);
+
+// Returns the `upb_MessageDef` for a given Message.
+const upb_MessageDef* PyUpb_Message_GetMsgdef(PyObject* self);
+
+// Functions that match the corresponding methods on the message object.
+PyObject* PyUpb_Message_MergeFrom(PyObject* self, PyObject* arg);
+PyObject* PyUpb_Message_MergeFromString(PyObject* self, PyObject* arg);
+PyObject* PyUpb_Message_SerializeToString(PyObject* self, PyObject* args,
+                                          PyObject* kwargs);
+PyObject* PyUpb_Message_SerializePartialToString(PyObject* self, PyObject* args,
+                                                 PyObject* kwargs);
+
+// Sets fields of the message according to the attribuges in `kwargs`.
+int PyUpb_Message_InitAttributes(PyObject* _self, PyObject* args,
+                                 PyObject* kwargs);
+
+// Checks that `key` is a field descriptor for an extension type, and that the
+// extendee is this message.  Otherwise returns NULL and sets a KeyError.
+const upb_FieldDef* PyUpb_Message_GetExtensionDef(PyObject* _self,
+                                                  PyObject* key);
+
+// Clears the given field in this message.
+void PyUpb_Message_DoClearField(PyObject* _self, const upb_FieldDef* f);
+
+// Clears the ExtensionDict from the message.  The message must have an
+// ExtensionDict set.
+void PyUpb_Message_ClearExtensionDict(PyObject* _self);
+
+// Implements the equivalent of getattr(msg, field), once `field` has
+// already been resolved to a `upb_FieldDef*`.
+PyObject* PyUpb_Message_GetFieldValue(PyObject* _self,
+                                      const upb_FieldDef* field);
+
+// Implements the equivalent of setattr(msg, field, value), once `field` has
+// already been resolved to a `upb_FieldDef*`.
+int PyUpb_Message_SetFieldValue(PyObject* _self, const upb_FieldDef* field,
+                                PyObject* value, PyObject* exc);
+
+// Creates message meta class.
+PyObject* PyUpb_MessageMeta_DoCreateClass(PyObject* py_descriptor,
+                                          const char* name, PyObject* dict);
+
+// Returns the version associated with this message.  The version will be
+// incremented when the message changes.
+int PyUpb_Message_GetVersion(PyObject* _self);
+
+// Module-level init.
+bool PyUpb_InitMessage(PyObject* m);
+
+#endif  // PYPB_MESSAGE_H__
diff --git a/python/minimal_test.py b/python/minimal_test.py
new file mode 100644
index 0000000..e1690d1
--- /dev/null
+++ b/python/minimal_test.py
@@ -0,0 +1,187 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A bare-bones unit test that doesn't load any generated code."""
+
+
+import unittest
+from google.protobuf.pyext import _message
+from google3.net.proto2.python.internal import api_implementation
+from google.protobuf import unittest_pb2
+from google.protobuf import map_unittest_pb2
+from google.protobuf import descriptor_pool
+from google.protobuf import text_format
+from google.protobuf import message_factory
+from google.protobuf import message
+from google3.net.proto2.python.internal import factory_test1_pb2
+from google3.net.proto2.python.internal import factory_test2_pb2
+from google3.net.proto2.python.internal import more_extensions_pb2
+from google.protobuf import descriptor_pb2
+
+class TestMessageExtension(unittest.TestCase):
+
+    def test_descriptor_pool(self):
+        serialized_desc = b'\n\ntest.proto\"\x0e\n\x02M1*\x08\x08\x01\x10\x80\x80\x80\x80\x02:\x15\n\x08test_ext\x12\x03.M1\x18\x01 \x01(\x05'
+        pool = _message.DescriptorPool()
+        file_desc = pool.AddSerializedFile(serialized_desc)
+        self.assertEqual("test.proto", file_desc.name)
+        ext_desc = pool.FindExtensionByName("test_ext")
+        self.assertEqual(1, ext_desc.number)
+
+        # Test object cache: repeatedly retrieving the same descriptor
+        # should result in the same object
+        self.assertIs(ext_desc, pool.FindExtensionByName("test_ext"))
+
+
+    def test_lib_is_upb(self):
+        # Ensure we are not pulling in a different protobuf library on the
+        # system.
+        print(_message._IS_UPB)
+        self.assertTrue(_message._IS_UPB)
+        self.assertEqual(api_implementation.Type(), "cpp")
+
+    def test_repeated_field_slice_delete(self):
+        def test_slice(start, end, step):
+            vals = list(range(20))
+            message = unittest_pb2.TestAllTypes(repeated_int32=vals)
+            del vals[start:end:step]
+            del message.repeated_int32[start:end:step]
+            self.assertEqual(vals, list(message.repeated_int32))
+        test_slice(3, 11, 1)
+        test_slice(3, 11, 2)
+        test_slice(3, 11, 3)
+        test_slice(11, 3, -1)
+        test_slice(11, 3, -2)
+        test_slice(11, 3, -3)
+        test_slice(10, 25, 4)
+    
+    def testExtensionsErrors(self):
+        msg = unittest_pb2.TestAllTypes()
+        self.assertRaises(AttributeError, getattr, msg, 'Extensions')
+    
+    def testClearStubMapField(self):
+        msg = map_unittest_pb2.TestMapSubmessage()
+        int32_map = msg.test_map.map_int32_int32
+        msg.test_map.ClearField("map_int32_int32")
+        int32_map[123] = 456
+        self.assertEqual(0, msg.test_map.ByteSize())
+
+    def testClearReifiedMapField(self):
+        msg = map_unittest_pb2.TestMap()
+        int32_map = msg.map_int32_int32
+        int32_map[123] = 456
+        msg.ClearField("map_int32_int32")
+        int32_map[111] = 222
+        self.assertEqual(0, msg.ByteSize())
+
+    def testClearStubRepeatedField(self):
+        msg = unittest_pb2.NestedTestAllTypes()
+        int32_array = msg.payload.repeated_int32
+        msg.payload.ClearField("repeated_int32")
+        int32_array.append(123)
+        self.assertEqual(0, msg.payload.ByteSize())
+
+    def testClearReifiedRepeatdField(self):
+        msg = unittest_pb2.TestAllTypes()
+        int32_array = msg.repeated_int32
+        int32_array.append(123)
+        self.assertNotEqual(0, msg.ByteSize())
+        msg.ClearField("repeated_int32")
+        int32_array.append(123)
+        self.assertEqual(0, msg.ByteSize())
+
+    def testFloatPrinting(self):
+        message = unittest_pb2.TestAllTypes()
+        message.optional_float = -0.0
+        self.assertEqual(str(message), 'optional_float: -0\n')
+
+class OversizeProtosTest(unittest.TestCase):
+  def setUp(self):
+    msg = unittest_pb2.NestedTestAllTypes()
+    m = msg
+    for i in range(101):
+      m = m.child
+    m.Clear()
+    self.p_serialized = msg.SerializeToString()
+    
+  def testAssertOversizeProto(self):
+    from google.protobuf.pyext._message import SetAllowOversizeProtos
+    SetAllowOversizeProtos(False)
+    q = unittest_pb2.NestedTestAllTypes()
+    with self.assertRaises(message.DecodeError):
+      q.ParseFromString(self.p_serialized)
+      print(q)
+  
+  def testSucceedOversizeProto(self):
+    from google.protobuf.pyext._message import SetAllowOversizeProtos
+    SetAllowOversizeProtos(True)
+    q = unittest_pb2.NestedTestAllTypes()
+    q.ParseFromString(self.p_serialized)
+
+  def testExtensionIter(self):
+    extendee_proto = more_extensions_pb2.ExtendedMessage()
+
+    extension_int32 = more_extensions_pb2.optional_int_extension
+    extendee_proto.Extensions[extension_int32] = 23
+
+    extension_repeated = more_extensions_pb2.repeated_int_extension
+    extendee_proto.Extensions[extension_repeated].append(11)
+
+    extension_msg = more_extensions_pb2.optional_message_extension
+    extendee_proto.Extensions[extension_msg].foreign_message_int = 56
+
+    # Set some normal fields.
+    extendee_proto.optional_int32 = 1
+    extendee_proto.repeated_string.append('hi')
+
+    expected = {
+        extension_int32: True,
+        extension_msg: True,
+        extension_repeated: True
+    }
+    count = 0
+    for item in extendee_proto.Extensions:
+      del expected[item]
+      self.assertIn(item, extendee_proto.Extensions)
+      count += 1
+    self.assertEqual(count, 3)
+    self.assertEqual(len(expected), 0)
+  
+  def testIsInitializedStub(self):
+    proto = unittest_pb2.TestRequiredForeign()
+    self.assertTrue(proto.IsInitialized())
+    self.assertFalse(proto.optional_message.IsInitialized())
+    errors = []
+    self.assertFalse(proto.optional_message.IsInitialized(errors))
+    self.assertEqual(['a', 'b', 'c'], errors)
+    self.assertRaises(message.EncodeError, proto.optional_message.SerializeToString)
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/BUILD b/python/pb_unit_tests/BUILD
new file mode 100644
index 0000000..56ee250
--- /dev/null
+++ b/python/pb_unit_tests/BUILD
@@ -0,0 +1,84 @@
+# Copyright (c) 2009-2021, Google LLC
+# All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+load(":pyproto_test_wrapper.bzl", "pyproto_test_wrapper")
+# begin:github_only
+load("@pip_deps//:requirements.bzl", "requirement")
+# end:github_only
+
+# begin:google_only
+# package(default_applicable_licenses = ["//upb:license"])
+# end:google_only
+
+licenses(["notice"])
+
+pyproto_test_wrapper(name = "descriptor_database_test")
+
+pyproto_test_wrapper(name = "descriptor_pool_test")
+
+pyproto_test_wrapper(name = "descriptor_test")
+
+# begin:github_only
+pyproto_test_wrapper(name = "generator_test")
+# end:github_only
+
+pyproto_test_wrapper(name = "json_format_test")
+
+pyproto_test_wrapper(name = "keywords_test")
+
+pyproto_test_wrapper(name = "message_factory_test")
+
+# begin:github_only
+# This target has different dependencies and fails when using the wrapper
+# TODO: Move this to using pyproto_test_wrapper
+py_test(
+    name = "numpy_test",
+    srcs = ["numpy_test_wrapper.py"],
+    main = "numpy_test_wrapper.py",
+    deps = [
+      requirement("numpy"),
+      "//python/google/protobuf/internal/numpy:numpy_test",
+      "//python:_message",
+    ],
+    target_compatible_with = select({
+      "@system_python//:supported": [],
+      "//conditions:default": ["@platforms//:incompatible"],
+    }),
+)
+# end:github_only
+
+# begin:google_only
+# pyproto_test_wrapper(name = "numpy_test")
+# end:google_only
+
+pyproto_test_wrapper(name = "proto_builder_test")
+
+pyproto_test_wrapper(name = "service_reflection_test")
+
+pyproto_test_wrapper(name = "symbol_database_test")
+
+pyproto_test_wrapper(name = "text_encoding_test")
+
+pyproto_test_wrapper(name = "message_test")
+
+pyproto_test_wrapper(name = "reflection_test")
+
+pyproto_test_wrapper(name = "text_format_test")
+
+pyproto_test_wrapper(name = "unknown_fields_test")
+
+pyproto_test_wrapper(name = "well_known_types_test")
+
+pyproto_test_wrapper(name = "wire_format_test")
+
+filegroup(
+    name = "test_files",
+    srcs = glob(["*.py"]),
+    visibility = [
+        "//python/dist:__pkg__",  # Scheuklappen: keep
+    ],
+)
diff --git a/python/pb_unit_tests/README.md b/python/pb_unit_tests/README.md
new file mode 100644
index 0000000..669f067
--- /dev/null
+++ b/python/pb_unit_tests/README.md
@@ -0,0 +1,11 @@
+
+# Protobuf Unit Tests
+
+This directory contains wrappers around the Python unit tests defined in
+the protobuf repo.  Python+upb is intended to be a drop-in replacement for
+protobuf Python, so we should be able to pass the same set of unit tests.
+
+Our wrappers contain exclusion lists for tests we know we are not currently
+passing.  Ideally these exclusion lists will become empty once Python+upb is
+fully implemented.  However there may be a few edge cases that we decide
+are not worth matching with perfect parity.
diff --git a/python/pb_unit_tests/descriptor_database_test_wrapper.py b/python/pb_unit_tests/descriptor_database_test_wrapper.py
new file mode 100644
index 0000000..2e6081f
--- /dev/null
+++ b/python/pb_unit_tests/descriptor_database_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.descriptor_database_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/descriptor_pool_test_wrapper.py b/python/pb_unit_tests/descriptor_pool_test_wrapper.py
new file mode 100644
index 0000000..1c4f282
--- /dev/null
+++ b/python/pb_unit_tests/descriptor_pool_test_wrapper.py
@@ -0,0 +1,45 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+from google.protobuf.internal.descriptor_pool_test import *
+
+SecondaryDescriptorFromDescriptorDB.testErrorCollector.__unittest_expecting_failure__ = True
+
+# begin:github_only
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
+# end:github_only
+
+# begin:google_only
+# from absl import app
+# if __name__ == '__main__':
+#   app.run(lambda argv: unittest.main(verbosity=2))
+# end:google_only
diff --git a/python/pb_unit_tests/descriptor_test_wrapper.py b/python/pb_unit_tests/descriptor_test_wrapper.py
new file mode 100644
index 0000000..11f47ad
--- /dev/null
+++ b/python/pb_unit_tests/descriptor_test_wrapper.py
@@ -0,0 +1,46 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.descriptor_test import *
+import unittest
+
+# These fail because they attempt to add fields with conflicting JSON names.
+# We don't want to support this going forward.
+MakeDescriptorTest.testCamelcaseName.__unittest_expecting_failure__ = True
+MakeDescriptorTest.testJsonName.__unittest_expecting_failure__ = True
+
+# We pass this test, but the error message is slightly different.
+# Our error message is better.
+NewDescriptorTest.testImmutableCppDescriptor.__unittest_expecting_failure__ = True
+
+DescriptorTest.testGetDebugString.__unittest_expecting_failure__ = True
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/generator_test_wrapper.py b/python/pb_unit_tests/generator_test_wrapper.py
new file mode 100644
index 0000000..9ffc27f
--- /dev/null
+++ b/python/pb_unit_tests/generator_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.generator_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/json_format_test_wrapper.py b/python/pb_unit_tests/json_format_test_wrapper.py
new file mode 100644
index 0000000..27d855c
--- /dev/null
+++ b/python/pb_unit_tests/json_format_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.json_format_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/keywords_test_wrapper.py b/python/pb_unit_tests/keywords_test_wrapper.py
new file mode 100644
index 0000000..d940178
--- /dev/null
+++ b/python/pb_unit_tests/keywords_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.keywords_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/message_factory_test_wrapper.py b/python/pb_unit_tests/message_factory_test_wrapper.py
new file mode 100644
index 0000000..4e3a7ba
--- /dev/null
+++ b/python/pb_unit_tests/message_factory_test_wrapper.py
@@ -0,0 +1,37 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.message_factory_test import *
+import unittest
+
+MessageFactoryTest.testDuplicateExtensionNumber.__unittest_expecting_failure__ = True
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/message_test_wrapper.py b/python/pb_unit_tests/message_test_wrapper.py
new file mode 100644
index 0000000..fcac3a3
--- /dev/null
+++ b/python/pb_unit_tests/message_test_wrapper.py
@@ -0,0 +1,55 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.message_test import *
+import unittest
+
+MessageTest.testExtendFloatWithNothing_proto2.__unittest_skip__ = True
+MessageTest.testExtendFloatWithNothing_proto3.__unittest_skip__ = True
+MessageTest.testExtendInt32WithNothing_proto2.__unittest_skip__ = True
+MessageTest.testExtendInt32WithNothing_proto3.__unittest_skip__ = True
+MessageTest.testExtendStringWithNothing_proto2.__unittest_skip__ = True
+MessageTest.testExtendStringWithNothing_proto3.__unittest_skip__ = True
+
+# Python/C++ customizes the C++ TextFormat to always print trailing ".0" for
+# floats. upb doesn't do this, it matches C++ TextFormat.
+MessageTest.testFloatPrinting_proto2.__unittest_expecting_failure__ = True
+MessageTest.testFloatPrinting_proto3.__unittest_expecting_failure__ = True
+
+# For these tests we are throwing the correct error, only the text of the error
+# message is a mismatch.  For technical reasons around the limited API, matching
+# the existing error message exactly is not feasible.
+Proto3Test.testCopyFromBadType.__unittest_expecting_failure__ = True
+Proto3Test.testMergeFromBadType.__unittest_expecting_failure__ = True
+
+Proto2Test.test_documentation.__unittest_expecting_failure__ = True
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/numpy_test_wrapper.py b/python/pb_unit_tests/numpy_test_wrapper.py
new file mode 100644
index 0000000..62089e9
--- /dev/null
+++ b/python/pb_unit_tests/numpy_test_wrapper.py
@@ -0,0 +1,36 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from google.protobuf.internal.numpy.numpy_test import *
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/proto_builder_test_wrapper.py b/python/pb_unit_tests/proto_builder_test_wrapper.py
new file mode 100644
index 0000000..468d13e
--- /dev/null
+++ b/python/pb_unit_tests/proto_builder_test_wrapper.py
@@ -0,0 +1,37 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.proto_builder_test import *
+import unittest
+
+ProtoBuilderTest.testMakeLargeProtoClass.__unittest_expecting_failure__ = True
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/pyproto_test_wrapper.bzl b/python/pb_unit_tests/pyproto_test_wrapper.bzl
new file mode 100644
index 0000000..5691e88
--- /dev/null
+++ b/python/pb_unit_tests/pyproto_test_wrapper.bzl
@@ -0,0 +1,46 @@
+# begin:github_only
+
+def pyproto_test_wrapper(name, deps = []):
+    src = name + "_wrapper.py"
+    native.py_test(
+        name = name,
+        srcs = [src],
+        legacy_create_init = False,
+        main = src,
+        data = ["//src/google/protobuf:testdata"],
+        deps = [
+            "//python:_message",
+            "//:python_common_test_protos",
+            "//:python_specific_test_protos",
+            "//:python_test_srcs",
+            "//:python_srcs",
+        ] + deps,
+        target_compatible_with = select({
+            "@system_python//:supported": [],
+            "//conditions:default": ["@platforms//:incompatible"],
+        }),
+    )
+
+# end:github_only
+
+# begin:google_only
+#
+# load("//third_party/bazel_rules/rules_python/python:py_test.bzl", "py_test")
+#
+# def pyproto_test_wrapper(name):
+#     src = name + "_wrapper.py"
+#     py_test(
+#         name = name,
+#         srcs = [src],
+#         main = src,
+#         deps = [
+#             "//third_party/py/google/protobuf/internal:" + name + "_for_deps",
+#             "//net/proto2/python/public:use_upb_protos",
+#         ],
+#         target_compatible_with = select({
+#             "@platforms//os:windows": ["@platforms//:incompatible"],
+#             "//conditions:default": [],
+#         }),
+#     )
+#
+# end:google_only
diff --git a/python/pb_unit_tests/reflection_test_wrapper.py b/python/pb_unit_tests/reflection_test_wrapper.py
new file mode 100644
index 0000000..7572f7c
--- /dev/null
+++ b/python/pb_unit_tests/reflection_test_wrapper.py
@@ -0,0 +1,53 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.reflection_test import *
+import unittest
+
+# These tests depend on a specific iteration order for extensions, which is not
+# reasonable to guarantee.
+Proto2ReflectionTest.testExtensionIter.__unittest_expecting_failure__ = True
+
+# These tests depend on a specific serialization order for extensions, which is
+# not reasonable to guarantee.
+SerializationTest.testCanonicalSerializationOrder.__unittest_expecting_failure__ = True
+SerializationTest.testCanonicalSerializationOrderSameAsCpp.__unittest_expecting_failure__ = True
+
+# This test relies on the internal implementation using Python descriptors.
+# This is an implementation detail that users should not depend on.
+SerializationTest.testFieldDataDescriptor.__unittest_expecting_failure__ = True
+
+SerializationTest.testFieldProperties.__unittest_expecting_failure__ = True
+
+# TODO Python Docker image on MacOS failing.
+ClassAPITest.testParsingNestedClass.__unittest_skip__ = True
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/service_reflection_test_wrapper.py b/python/pb_unit_tests/service_reflection_test_wrapper.py
new file mode 100644
index 0000000..bc0345c
--- /dev/null
+++ b/python/pb_unit_tests/service_reflection_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.service_reflection_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/symbol_database_test_wrapper.py b/python/pb_unit_tests/symbol_database_test_wrapper.py
new file mode 100644
index 0000000..16ea965
--- /dev/null
+++ b/python/pb_unit_tests/symbol_database_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.symbol_database_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/text_encoding_test_wrapper.py b/python/pb_unit_tests/text_encoding_test_wrapper.py
new file mode 100644
index 0000000..3eb8153
--- /dev/null
+++ b/python/pb_unit_tests/text_encoding_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.text_encoding_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/text_format_test_wrapper.py b/python/pb_unit_tests/text_format_test_wrapper.py
new file mode 100644
index 0000000..535561d
--- /dev/null
+++ b/python/pb_unit_tests/text_format_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.text_format_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/unknown_fields_test_wrapper.py b/python/pb_unit_tests/unknown_fields_test_wrapper.py
new file mode 100644
index 0000000..1807f6d
--- /dev/null
+++ b/python/pb_unit_tests/unknown_fields_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.unknown_fields_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/well_known_types_test_wrapper.py b/python/pb_unit_tests/well_known_types_test_wrapper.py
new file mode 100644
index 0000000..5006332
--- /dev/null
+++ b/python/pb_unit_tests/well_known_types_test_wrapper.py
@@ -0,0 +1,36 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.well_known_types_test import *
+import os
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/pb_unit_tests/wire_format_test_wrapper.py b/python/pb_unit_tests/wire_format_test_wrapper.py
new file mode 100644
index 0000000..3b13a2b
--- /dev/null
+++ b/python/pb_unit_tests/wire_format_test_wrapper.py
@@ -0,0 +1,35 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2023 Google LLC.  All rights reserved.
+# https://developers.google.com/protocol-buffers/
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google LLC nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.protobuf.internal.wire_format_test import *
+import unittest
+
+if __name__ == '__main__':
+  unittest.main(verbosity=2)
diff --git a/python/protobuf.c b/python/protobuf.c
new file mode 100644
index 0000000..324b1ed
--- /dev/null
+++ b/python/protobuf.c
@@ -0,0 +1,431 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/protobuf.h"
+
+#include "python/descriptor.h"
+#include "python/descriptor_containers.h"
+#include "python/descriptor_pool.h"
+#include "python/extension_dict.h"
+#include "python/map.h"
+#include "python/message.h"
+#include "python/repeated.h"
+#include "python/unknown_fields.h"
+
+static upb_Arena* PyUpb_NewArena(void);
+
+static void PyUpb_ModuleDealloc(void* module) {
+  PyUpb_ModuleState* s = PyModule_GetState(module);
+  PyUpb_WeakMap_Free(s->obj_cache);
+  if (s->c_descriptor_symtab) {
+    upb_DefPool_Free(s->c_descriptor_symtab);
+  }
+}
+
+PyObject* PyUpb_SetAllowOversizeProtos(PyObject* m, PyObject* arg) {
+  if (!arg || !PyBool_Check(arg)) {
+    PyErr_SetString(PyExc_TypeError,
+                    "Argument to SetAllowOversizeProtos must be boolean");
+    return NULL;
+  }
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  state->allow_oversize_protos = PyObject_IsTrue(arg);
+  Py_INCREF(arg);
+  return arg;
+}
+
+static PyMethodDef PyUpb_ModuleMethods[] = {
+    {"SetAllowOversizeProtos", PyUpb_SetAllowOversizeProtos, METH_O,
+     "Enable/disable oversize proto parsing."},
+    {NULL, NULL}};
+
+static struct PyModuleDef module_def = {PyModuleDef_HEAD_INIT,
+                                        PYUPB_MODULE_NAME,
+                                        "Protobuf Module",
+                                        sizeof(PyUpb_ModuleState),
+                                        PyUpb_ModuleMethods,  // m_methods
+                                        NULL,                 // m_slots
+                                        NULL,                 // m_traverse
+                                        NULL,                 // m_clear
+                                        PyUpb_ModuleDealloc};
+
+// -----------------------------------------------------------------------------
+// ModuleState
+// -----------------------------------------------------------------------------
+
+PyUpb_ModuleState* PyUpb_ModuleState_MaybeGet(void) {
+  PyObject* module = PyState_FindModule(&module_def);
+  return module ? PyModule_GetState(module) : NULL;
+}
+
+PyUpb_ModuleState* PyUpb_ModuleState_GetFromModule(PyObject* module) {
+  PyUpb_ModuleState* state = PyModule_GetState(module);
+  assert(state);
+  assert(PyModule_GetDef(module) == &module_def);
+  return state;
+}
+
+PyUpb_ModuleState* PyUpb_ModuleState_Get(void) {
+  PyObject* module = PyState_FindModule(&module_def);
+  assert(module);
+  return PyUpb_ModuleState_GetFromModule(module);
+}
+
+PyObject* PyUpb_GetWktBases(PyUpb_ModuleState* state) {
+  if (!state->wkt_bases) {
+    PyObject* wkt_module = PyImport_ImportModule(PYUPB_PROTOBUF_INTERNAL_PACKAGE
+                                                 ".well_known_types");
+
+    if (wkt_module == NULL) {
+      return false;
+    }
+
+    state->wkt_bases = PyObject_GetAttrString(wkt_module, "WKTBASES");
+    PyObject* m = PyState_FindModule(&module_def);
+    // Reparent ownership to m.
+    PyModule_AddObject(m, "__internal_wktbases", state->wkt_bases);
+    Py_DECREF(wkt_module);
+  }
+
+  return state->wkt_bases;
+}
+
+// -----------------------------------------------------------------------------
+// WeakMap
+// -----------------------------------------------------------------------------
+
+struct PyUpb_WeakMap {
+  upb_inttable table;
+  upb_Arena* arena;
+};
+
+PyUpb_WeakMap* PyUpb_WeakMap_New(void) {
+  upb_Arena* arena = PyUpb_NewArena();
+  PyUpb_WeakMap* map = upb_Arena_Malloc(arena, sizeof(*map));
+  map->arena = arena;
+  upb_inttable_init(&map->table, map->arena);
+  return map;
+}
+
+void PyUpb_WeakMap_Free(PyUpb_WeakMap* map) { upb_Arena_Free(map->arena); }
+
+// To give better entropy in the table key, we shift away low bits that are
+// always zero.
+static const int PyUpb_PtrShift = (sizeof(void*) == 4) ? 2 : 3;
+
+uintptr_t PyUpb_WeakMap_GetKey(const void* key) {
+  uintptr_t n = (uintptr_t)key;
+  assert((n & ((1 << PyUpb_PtrShift) - 1)) == 0);
+  return n >> PyUpb_PtrShift;
+}
+
+void PyUpb_WeakMap_Add(PyUpb_WeakMap* map, const void* key, PyObject* py_obj) {
+  upb_inttable_insert(&map->table, PyUpb_WeakMap_GetKey(key),
+                      upb_value_ptr(py_obj), map->arena);
+}
+
+void PyUpb_WeakMap_Delete(PyUpb_WeakMap* map, const void* key) {
+  upb_value val;
+  bool removed =
+      upb_inttable_remove(&map->table, PyUpb_WeakMap_GetKey(key), &val);
+  (void)removed;
+  assert(removed);
+}
+
+void PyUpb_WeakMap_TryDelete(PyUpb_WeakMap* map, const void* key) {
+  upb_inttable_remove(&map->table, PyUpb_WeakMap_GetKey(key), NULL);
+}
+
+PyObject* PyUpb_WeakMap_Get(PyUpb_WeakMap* map, const void* key) {
+  upb_value val;
+  if (upb_inttable_lookup(&map->table, PyUpb_WeakMap_GetKey(key), &val)) {
+    PyObject* ret = upb_value_getptr(val);
+    Py_INCREF(ret);
+    return ret;
+  } else {
+    return NULL;
+  }
+}
+
+bool PyUpb_WeakMap_Next(PyUpb_WeakMap* map, const void** key, PyObject** obj,
+                        intptr_t* iter) {
+  uintptr_t u_key;
+  upb_value val;
+  if (!upb_inttable_next(&map->table, &u_key, &val, iter)) return false;
+  *key = (void*)(u_key << PyUpb_PtrShift);
+  *obj = upb_value_getptr(val);
+  return true;
+}
+
+void PyUpb_WeakMap_DeleteIter(PyUpb_WeakMap* map, intptr_t* iter) {
+  upb_inttable_removeiter(&map->table, iter);
+}
+
+// -----------------------------------------------------------------------------
+// ObjCache
+// -----------------------------------------------------------------------------
+
+PyUpb_WeakMap* PyUpb_ObjCache_Instance(void) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  return state->obj_cache;
+}
+
+void PyUpb_ObjCache_Add(const void* key, PyObject* py_obj) {
+  PyUpb_WeakMap_Add(PyUpb_ObjCache_Instance(), key, py_obj);
+}
+
+void PyUpb_ObjCache_Delete(const void* key) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_MaybeGet();
+  if (!state) {
+    // During the shutdown sequence, our object's Dealloc() methods can be
+    // called *after* our module Dealloc() method has been called.  At that
+    // point our state will be NULL and there is nothing to delete out of the
+    // map.
+    return;
+  }
+  PyUpb_WeakMap_Delete(state->obj_cache, key);
+}
+
+PyObject* PyUpb_ObjCache_Get(const void* key) {
+  return PyUpb_WeakMap_Get(PyUpb_ObjCache_Instance(), key);
+}
+
+// -----------------------------------------------------------------------------
+// Arena
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  upb_Arena* arena;
+} PyUpb_Arena;
+
+// begin:google_only
+// static upb_alloc* global_alloc = &upb_alloc_global;
+// end:google_only
+
+// begin:github_only
+#ifdef __GLIBC__
+#include <malloc.h>  // malloc_trim()
+#endif
+
+// A special allocator that calls malloc_trim() periodically to release
+// memory to the OS.  Without this call, we appear to leak memory, at least
+// as measured in RSS.
+//
+// We opt not to use this instead of PyMalloc (which would also solve the
+// problem) because the latter requires the GIL to be held.  This would make
+// our messages unsafe to share with other languages that could free at
+// unpredictable
+// times.
+static void* upb_trim_allocfunc(upb_alloc* alloc, void* ptr, size_t oldsize,
+                                size_t size) {
+  (void)alloc;
+  (void)oldsize;
+  if (size == 0) {
+    free(ptr);
+#ifdef __GLIBC__
+    static int count = 0;
+    if (++count == 10000) {
+      malloc_trim(0);
+      count = 0;
+    }
+#endif
+    return NULL;
+  } else {
+    return realloc(ptr, size);
+  }
+}
+static upb_alloc trim_alloc = {&upb_trim_allocfunc};
+static const upb_alloc* global_alloc = &trim_alloc;
+// end:github_only
+
+static upb_Arena* PyUpb_NewArena(void) {
+  return upb_Arena_Init(NULL, 0, global_alloc);
+}
+
+PyObject* PyUpb_Arena_New(void) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  PyUpb_Arena* arena = (void*)PyType_GenericAlloc(state->arena_type, 0);
+  arena->arena = PyUpb_NewArena();
+  return &arena->ob_base;
+}
+
+static void PyUpb_Arena_Dealloc(PyObject* self) {
+  upb_Arena_Free(PyUpb_Arena_Get(self));
+  PyUpb_Dealloc(self);
+}
+
+upb_Arena* PyUpb_Arena_Get(PyObject* arena) {
+  return ((PyUpb_Arena*)arena)->arena;
+}
+
+static PyType_Slot PyUpb_Arena_Slots[] = {
+    {Py_tp_dealloc, PyUpb_Arena_Dealloc},
+    {0, NULL},
+};
+
+static PyType_Spec PyUpb_Arena_Spec = {
+    PYUPB_MODULE_NAME ".Arena",
+    sizeof(PyUpb_Arena),
+    0,  // itemsize
+    Py_TPFLAGS_DEFAULT,
+    PyUpb_Arena_Slots,
+};
+
+static bool PyUpb_InitArena(PyObject* m) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_GetFromModule(m);
+  state->arena_type = PyUpb_AddClass(m, &PyUpb_Arena_Spec);
+  return state->arena_type;
+}
+
+// -----------------------------------------------------------------------------
+// Utilities
+// -----------------------------------------------------------------------------
+
+PyTypeObject* AddObject(PyObject* m, const char* name, PyType_Spec* spec) {
+  PyObject* type = PyType_FromSpec(spec);
+  return type && PyModule_AddObject(m, name, type) == 0 ? (PyTypeObject*)type
+                                                        : NULL;
+}
+
+static const char* PyUpb_GetClassName(PyType_Spec* spec) {
+  // spec->name contains a fully-qualified name, like:
+  //   google.protobuf.pyext._message.FooBar
+  //
+  // Find the rightmost '.' to get "FooBar".
+  const char* name = strrchr(spec->name, '.');
+  assert(name);
+  return name + 1;
+}
+
+PyTypeObject* PyUpb_AddClass(PyObject* m, PyType_Spec* spec) {
+  PyObject* type = PyType_FromSpec(spec);
+  const char* name = PyUpb_GetClassName(spec);
+  if (PyModule_AddObject(m, name, type) < 0) {
+    Py_XDECREF(type);
+    return NULL;
+  }
+  return (PyTypeObject*)type;
+}
+
+PyTypeObject* PyUpb_AddClassWithBases(PyObject* m, PyType_Spec* spec,
+                                      PyObject* bases) {
+  PyObject* type = PyType_FromSpecWithBases(spec, bases);
+  const char* name = PyUpb_GetClassName(spec);
+  if (PyModule_AddObject(m, name, type) < 0) {
+    Py_XDECREF(type);
+    return NULL;
+  }
+  return (PyTypeObject*)type;
+}
+
+const char* PyUpb_GetStrData(PyObject* obj) {
+  if (PyUnicode_Check(obj)) {
+    return PyUnicode_AsUTF8AndSize(obj, NULL);
+  } else if (PyBytes_Check(obj)) {
+    return PyBytes_AsString(obj);
+  } else {
+    return NULL;
+  }
+}
+
+const char* PyUpb_VerifyStrData(PyObject* obj) {
+  const char* ret = PyUpb_GetStrData(obj);
+  if (ret) return ret;
+  PyErr_Format(PyExc_TypeError, "Expected string: %S", obj);
+  return NULL;
+}
+
+PyObject* PyUpb_Forbidden_New(PyObject* cls, PyObject* args, PyObject* kwds) {
+  PyObject* name = PyObject_GetAttrString(cls, "__name__");
+  PyErr_Format(PyExc_RuntimeError,
+               "Objects of type %U may not be created directly.", name);
+  Py_XDECREF(name);
+  return NULL;
+}
+
+bool PyUpb_IndexToRange(PyObject* index, Py_ssize_t size, Py_ssize_t* i,
+                        Py_ssize_t* count, Py_ssize_t* step) {
+  assert(i && count && step);
+  if (PySlice_Check(index)) {
+    Py_ssize_t start, stop;
+    if (PySlice_Unpack(index, &start, &stop, step) < 0) return false;
+    *count = PySlice_AdjustIndices(size, &start, &stop, *step);
+    *i = start;
+  } else {
+    *i = PyNumber_AsSsize_t(index, PyExc_IndexError);
+
+    if (*i == -1 && PyErr_Occurred()) {
+      PyErr_SetString(PyExc_TypeError, "list indices must be integers");
+      return false;
+    }
+
+    if (*i < 0) *i += size;
+    *step = 0;
+    *count = 1;
+
+    if (*i < 0 || size <= *i) {
+      PyErr_Format(PyExc_IndexError, "list index out of range");
+      return false;
+    }
+  }
+  return true;
+}
+
+// -----------------------------------------------------------------------------
+// Module Entry Point
+// -----------------------------------------------------------------------------
+
+__attribute__((visibility("default"))) PyMODINIT_FUNC PyInit__message(void) {
+  PyObject* m = PyModule_Create(&module_def);
+  if (!m) return NULL;
+
+  PyUpb_ModuleState* state = PyUpb_ModuleState_GetFromModule(m);
+
+  state->allow_oversize_protos = false;
+  state->wkt_bases = NULL;
+  state->obj_cache = PyUpb_WeakMap_New();
+  state->c_descriptor_symtab = NULL;
+
+  if (!PyUpb_InitDescriptorContainers(m) || !PyUpb_InitDescriptorPool(m) ||
+      !PyUpb_InitDescriptor(m) || !PyUpb_InitArena(m) ||
+      !PyUpb_InitExtensionDict(m) || !PyUpb_Map_Init(m) ||
+      !PyUpb_InitMessage(m) || !PyUpb_Repeated_Init(m) ||
+      !PyUpb_UnknownFields_Init(m)) {
+    Py_DECREF(m);
+    return NULL;
+  }
+
+  // Temporary: an cookie we can use in the tests to ensure we are testing upb
+  // and not another protobuf library on the system.
+  PyModule_AddIntConstant(m, "_IS_UPB", 1);
+
+  return m;
+}
diff --git a/python/protobuf.h b/python/protobuf.h
new file mode 100644
index 0000000..e9839be
--- /dev/null
+++ b/python/protobuf.h
@@ -0,0 +1,240 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_PROTOBUF_H__
+#define PYUPB_PROTOBUF_H__
+
+#include <stdbool.h>
+
+#include "python/descriptor.h"
+#include "python/python_api.h"
+#include "upb/hash/int_table.h"
+
+// begin:github_only
+#define PYUPB_PROTOBUF_PUBLIC_PACKAGE "google.protobuf"
+#define PYUPB_PROTOBUF_INTERNAL_PACKAGE "google.protobuf.internal"
+#define PYUPB_DESCRIPTOR_PROTO_PACKAGE "google.protobuf"
+#define PYUPB_MODULE_NAME "google._upb._message"
+// end:github_only
+
+// begin:google_only
+// #define PYUPB_PROTOBUF_PUBLIC_PACKAGE "google3.net.google.protobuf.python.public"
+// #define PYUPB_PROTOBUF_INTERNAL_PACKAGE "google3.net.google.protobuf.python.internal"
+// #define PYUPB_DESCRIPTOR_PROTO_PACKAGE "proto2"
+// #define PYUPB_MODULE_NAME "google3.third_party.upb.python._message"
+// end:google_only
+
+#define PYUPB_DESCRIPTOR_MODULE "google.protobuf.descriptor_pb2"
+#define PYUPB_RETURN_OOM return PyErr_SetNone(PyExc_MemoryError), NULL
+
+struct PyUpb_WeakMap;
+typedef struct PyUpb_WeakMap PyUpb_WeakMap;
+
+// -----------------------------------------------------------------------------
+// ModuleState
+// -----------------------------------------------------------------------------
+
+// We store all "global" state in this struct instead of using (C) global
+// variables. This makes this extension compatible with sub-interpreters.
+
+typedef struct {
+  // From descriptor.c
+  PyTypeObject* descriptor_types[kPyUpb_Descriptor_Count];
+
+  // From descriptor_containers.c
+  PyTypeObject* by_name_map_type;
+  PyTypeObject* by_name_iterator_type;
+  PyTypeObject* by_number_map_type;
+  PyTypeObject* by_number_iterator_type;
+  PyTypeObject* generic_sequence_type;
+
+  // From descriptor_pool.c
+  PyObject* default_pool;
+
+  // From descriptor_pool.c
+  PyTypeObject* descriptor_pool_type;
+  upb_DefPool* c_descriptor_symtab;
+
+  // From extension_dict.c
+  PyTypeObject* extension_dict_type;
+  PyTypeObject* extension_iterator_type;
+
+  // From map.c
+  PyTypeObject* map_iterator_type;
+  PyTypeObject* message_map_container_type;
+  PyTypeObject* scalar_map_container_type;
+
+  // From message.c
+  PyObject* decode_error_class;
+  PyObject* descriptor_string;
+  PyObject* encode_error_class;
+  PyObject* enum_type_wrapper_class;
+  PyObject* message_class;
+  PyTypeObject* cmessage_type;
+  PyTypeObject* message_meta_type;
+  PyObject* listfields_item_key;
+
+  // From protobuf.c
+  bool allow_oversize_protos;
+  PyObject* wkt_bases;
+  PyTypeObject* arena_type;
+  PyUpb_WeakMap* obj_cache;
+
+  // From repeated.c
+  PyTypeObject* repeated_composite_container_type;
+  PyTypeObject* repeated_scalar_container_type;
+
+  // From unknown_fields.c
+  PyTypeObject* unknown_fields_type;
+  PyObject* unknown_field_type;
+} PyUpb_ModuleState;
+
+// Returns the global state object from the current interpreter. The current
+// interpreter is looked up from thread-local state.
+PyUpb_ModuleState* PyUpb_ModuleState_Get(void);
+PyUpb_ModuleState* PyUpb_ModuleState_GetFromModule(PyObject* module);
+
+// Returns NULL if module state is not yet available (during startup).
+// Any use of the module state during startup needs to be passed explicitly.
+PyUpb_ModuleState* PyUpb_ModuleState_MaybeGet(void);
+
+// Returns:
+//   from google.protobuf.internal.well_known_types import WKTBASES
+//
+// This has to be imported lazily rather than at module load time, because
+// otherwise it would cause a circular import.
+PyObject* PyUpb_GetWktBases(PyUpb_ModuleState* state);
+
+// -----------------------------------------------------------------------------
+// WeakMap
+// -----------------------------------------------------------------------------
+
+// A WeakMap maps C pointers to the corresponding Python wrapper object. We
+// want a consistent Python wrapper object for each C object, both to save
+// memory and to provide object stability (ie. x is x).
+//
+// Each wrapped object should add itself to the map when it is constructed and
+// remove itself from the map when it is destroyed. The map is weak so it does
+// not take references to the cached objects.
+
+PyUpb_WeakMap* PyUpb_WeakMap_New(void);
+void PyUpb_WeakMap_Free(PyUpb_WeakMap* map);
+
+// Adds the given object to the map, indexed by the given key.
+void PyUpb_WeakMap_Add(PyUpb_WeakMap* map, const void* key, PyObject* py_obj);
+
+// Removes the given key from the cache. It must exist in the cache currently.
+void PyUpb_WeakMap_Delete(PyUpb_WeakMap* map, const void* key);
+void PyUpb_WeakMap_TryDelete(PyUpb_WeakMap* map, const void* key);
+
+// Returns a new reference to an object if it exists, otherwise returns NULL.
+PyObject* PyUpb_WeakMap_Get(PyUpb_WeakMap* map, const void* key);
+
+#define PYUPB_WEAKMAP_BEGIN UPB_INTTABLE_BEGIN
+
+// Iteration over the weak map, eg.
+//
+// intptr_t it = PYUPB_WEAKMAP_BEGIN;
+// while (PyUpb_WeakMap_Next(map, &key, &obj, &it)) {
+//   // ...
+// }
+//
+// Note that the callee does not own a ref on the returned `obj`.
+bool PyUpb_WeakMap_Next(PyUpb_WeakMap* map, const void** key, PyObject** obj,
+                        intptr_t* iter);
+void PyUpb_WeakMap_DeleteIter(PyUpb_WeakMap* map, intptr_t* iter);
+
+// -----------------------------------------------------------------------------
+// ObjCache
+// -----------------------------------------------------------------------------
+
+// The object cache is a global WeakMap for mapping upb objects to the
+// corresponding wrapper.
+void PyUpb_ObjCache_Add(const void* key, PyObject* py_obj);
+void PyUpb_ObjCache_Delete(const void* key);
+PyObject* PyUpb_ObjCache_Get(const void* key);  // returns NULL if not present.
+PyUpb_WeakMap* PyUpb_ObjCache_Instance(void);
+
+// -----------------------------------------------------------------------------
+// Arena
+// -----------------------------------------------------------------------------
+
+PyObject* PyUpb_Arena_New(void);
+upb_Arena* PyUpb_Arena_Get(PyObject* arena);
+
+// -----------------------------------------------------------------------------
+// Utilities
+// -----------------------------------------------------------------------------
+
+PyTypeObject* AddObject(PyObject* m, const char* name, PyType_Spec* spec);
+
+// Creates a Python type from `spec` and adds it to the given module `m`.
+PyTypeObject* PyUpb_AddClass(PyObject* m, PyType_Spec* spec);
+
+// Like PyUpb_AddClass(), but allows you to specify a tuple of base classes
+// in `bases`.
+PyTypeObject* PyUpb_AddClassWithBases(PyObject* m, PyType_Spec* spec,
+                                      PyObject* bases);
+
+// A function that implements the tp_new slot for types that we do not allow
+// users to create directly. This will immediately fail with an error message.
+PyObject* PyUpb_Forbidden_New(PyObject* cls, PyObject* args, PyObject* kwds);
+
+// Our standard dealloc func. It follows the guidance defined in:
+//   https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_dealloc
+// However it tests Py_TPFLAGS_HEAPTYPE dynamically so that a single dealloc
+// function can work for any type.
+static inline void PyUpb_Dealloc(void* self) {
+  PyTypeObject* tp = Py_TYPE(self);
+  assert(PyType_GetFlags(tp) & Py_TPFLAGS_HEAPTYPE);
+  freefunc tp_free = (freefunc)PyType_GetSlot(tp, Py_tp_free);
+  tp_free(self);
+  Py_DECREF(tp);
+}
+
+// Equivalent to the Py_NewRef() function introduced in Python 3.10.  If/when we
+// drop support for Python <3.10, we can remove this function and replace all
+// callers with Py_NewRef().
+static inline PyObject* PyUpb_NewRef(PyObject* obj) {
+  Py_INCREF(obj);
+  return obj;
+}
+
+const char* PyUpb_GetStrData(PyObject* obj);
+const char* PyUpb_VerifyStrData(PyObject* obj);
+
+// For an expression like:
+//    foo[index]
+//
+// Converts `index` to an effective i/count/step, for a repeated field
+// or descriptor sequence of size 'size'.
+bool PyUpb_IndexToRange(PyObject* index, Py_ssize_t size, Py_ssize_t* i,
+                        Py_ssize_t* count, Py_ssize_t* step);
+#endif  // PYUPB_PROTOBUF_H__
diff --git a/python/py_extension.bzl b/python/py_extension.bzl
new file mode 100644
index 0000000..7b918bc
--- /dev/null
+++ b/python/py_extension.bzl
@@ -0,0 +1,60 @@
+"""Macro to support py_extension """
+
+load("@bazel_skylib//lib:selects.bzl", "selects")
+
+def py_extension(name, srcs, copts, deps = [], **kwargs):
+    """Creates a C++ library to extend python
+
+    Args:
+      name: Name of the target
+      srcs: List of source files to create the target
+      copts: List of C++ compile options to use
+      deps: Libraries that the target depends on
+    """
+
+    native.cc_binary(
+        name = name + "_binary",
+        srcs = srcs,
+        copts = copts + ["-fvisibility=hidden"],
+        linkopts = selects.with_or({
+            (
+                "//python/dist:osx_x86_64",
+                "//python/dist:osx_aarch64",
+            ): ["-undefined", "dynamic_lookup"],
+            "//python/dist:windows_x86_32": ["-static-libgcc"],
+            "//conditions:default": [],
+        }),
+        linkshared = True,
+        linkstatic = True,
+        deps = deps + select({
+            "//python:limited_api_3.7": ["@python-3.7.0//:python_headers"],
+            "//python:full_api_3.7_win32": ["@nuget_python_i686_3.7.0//:python_full_api"],
+            "//python:full_api_3.7_win64": ["@nuget_python_x86-64_3.7.0//:python_full_api"],
+            "//python:full_api_3.8_win32": ["@nuget_python_i686_3.8.0//:python_full_api"],
+            "//python:full_api_3.8_win64": ["@nuget_python_x86-64_3.8.0//:python_full_api"],
+            "//python:full_api_3.9_win32": ["@nuget_python_i686_3.9.0//:python_full_api"],
+            "//python:full_api_3.9_win64": ["@nuget_python_x86-64_3.9.0//:python_full_api"],
+            "//python:limited_api_3.10_win32": ["@nuget_python_i686_3.10.0//:python_limited_api"],
+            "//python:limited_api_3.10_win64": ["@nuget_python_x86-64_3.10.0//:python_limited_api"],
+            "//conditions:default": ["@system_python//:python_headers"],
+        }),
+        **kwargs
+    )
+
+    EXT_SUFFIX = ".abi3.so"
+    output_file = "google/_upb/" + name + EXT_SUFFIX
+
+    native.genrule(
+        name = "copy" + name,
+        srcs = [":" + name + "_binary"],
+        outs = [output_file],
+        cmd = "cp $< $@",
+        visibility = ["//python:__subpackages__"],
+    )
+
+    native.py_library(
+        name = name,
+        data = [output_file],
+        imports = ["."],
+        visibility = ["//python:__subpackages__"],
+    )
diff --git a/python/python_api.h b/python/python_api.h
new file mode 100644
index 0000000..fae7df2
--- /dev/null
+++ b/python/python_api.h
@@ -0,0 +1,64 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_PYTHON_H__
+#define PYUPB_PYTHON_H__
+
+// We restrict ourselves to the limited API, so that a single build can be
+// ABI-compatible with a wide range of Python versions.
+//
+// The build system will define Py_LIMITED_API as appropriate (see BUILD). We
+// only want to define it for our distribution packages, since we can do some
+// extra assertions when Py_LIMITED_API is not defined.  Also Py_LIMITED_API is
+// incompatible with Py_DEBUG.
+
+// #define Py_LIMITED_API <val>  // Defined by build system when appropriate.
+
+#include "Python.h"
+
+// Ideally we could restrict ourselves to the limited API of 3.7, but this is
+// a very important function that was not officially added to the limited API
+// until 3.10.  Without this function, there is no way of getting data from a
+// Python `str` object without a copy.
+//
+// While this function was not *officially* added to the limited API until
+// Python 3.10, In practice it has been stable since Python 3.1.
+//   https://bugs.python.org/issue41784
+//
+// On Linux/ELF and macOS/Mach-O, we can get away with using this function with
+// the limited API prior to 3.10.
+
+#if (defined(__linux__) || defined(__APPLE__)) && defined(Py_LIMITED_API) && \
+    Py_LIMITED_API < 0x03100000
+PyAPI_FUNC(const char*)
+    PyUnicode_AsUTF8AndSize(PyObject* unicode, Py_ssize_t* size);
+#endif
+
+#endif  // PYUPB_PYTHON_H__
diff --git a/python/repeated.c b/python/repeated.c
new file mode 100644
index 0000000..abb34e8
--- /dev/null
+++ b/python/repeated.c
@@ -0,0 +1,800 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/repeated.h"
+
+#include "python/convert.h"
+#include "python/message.h"
+#include "python/protobuf.h"
+
+static PyObject* PyUpb_RepeatedCompositeContainer_Append(PyObject* _self,
+                                                         PyObject* value);
+static PyObject* PyUpb_RepeatedScalarContainer_Append(PyObject* _self,
+                                                      PyObject* value);
+
+// Wrapper for a repeated field.
+typedef struct {
+  PyObject_HEAD;
+  PyObject* arena;
+  // The field descriptor (PyObject*).
+  // The low bit indicates whether the container is reified (see ptr below).
+  //   - low bit set: repeated field is a stub (no underlying data).
+  //   - low bit clear: repeated field is reified (points to upb_Array).
+  uintptr_t field;
+  union {
+    PyObject* parent;  // stub: owning pointer to parent message.
+    upb_Array* arr;    // reified: the data for this array.
+  } ptr;
+} PyUpb_RepeatedContainer;
+
+static bool PyUpb_RepeatedContainer_IsStub(PyUpb_RepeatedContainer* self) {
+  return self->field & 1;
+}
+
+static PyObject* PyUpb_RepeatedContainer_GetFieldDescriptor(
+    PyUpb_RepeatedContainer* self) {
+  return (PyObject*)(self->field & ~(uintptr_t)1);
+}
+
+static const upb_FieldDef* PyUpb_RepeatedContainer_GetField(
+    PyUpb_RepeatedContainer* self) {
+  return PyUpb_FieldDescriptor_GetDef(
+      PyUpb_RepeatedContainer_GetFieldDescriptor(self));
+}
+
+// If the repeated field is reified, returns it.  Otherwise, returns NULL.
+// If NULL is returned, the object is empty and has no underlying data.
+static upb_Array* PyUpb_RepeatedContainer_GetIfReified(
+    PyUpb_RepeatedContainer* self) {
+  return PyUpb_RepeatedContainer_IsStub(self) ? NULL : self->ptr.arr;
+}
+
+void PyUpb_RepeatedContainer_Reify(PyObject* _self, upb_Array* arr) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  assert(PyUpb_RepeatedContainer_IsStub(self));
+  if (!arr) {
+    const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+    upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+    arr = upb_Array_New(arena, upb_FieldDef_CType(f));
+  }
+  PyUpb_ObjCache_Add(arr, &self->ob_base);
+  Py_DECREF(self->ptr.parent);
+  self->ptr.arr = arr;  // Overwrites self->ptr.parent.
+  self->field &= ~(uintptr_t)1;
+  assert(!PyUpb_RepeatedContainer_IsStub(self));
+}
+
+upb_Array* PyUpb_RepeatedContainer_EnsureReified(PyObject* _self) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_GetIfReified(self);
+  if (arr) return arr;  // Already writable.
+
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  arr = upb_Array_New(arena, upb_FieldDef_CType(f));
+  PyUpb_Message_SetConcreteSubobj(self->ptr.parent, f,
+                                  (upb_MessageValue){.array_val = arr});
+  PyUpb_RepeatedContainer_Reify((PyObject*)self, arr);
+  return arr;
+}
+
+static void PyUpb_RepeatedContainer_Dealloc(PyObject* _self) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  Py_DECREF(self->arena);
+  if (PyUpb_RepeatedContainer_IsStub(self)) {
+    PyUpb_Message_CacheDelete(self->ptr.parent,
+                              PyUpb_RepeatedContainer_GetField(self));
+    Py_DECREF(self->ptr.parent);
+  } else {
+    PyUpb_ObjCache_Delete(self->ptr.arr);
+  }
+  Py_DECREF(PyUpb_RepeatedContainer_GetFieldDescriptor(self));
+  PyUpb_Dealloc(self);
+}
+
+static PyTypeObject* PyUpb_RepeatedContainer_GetClass(const upb_FieldDef* f) {
+  assert(upb_FieldDef_IsRepeated(f) && !upb_FieldDef_IsMap(f));
+  PyUpb_ModuleState* state = PyUpb_ModuleState_Get();
+  return upb_FieldDef_IsSubMessage(f) ? state->repeated_composite_container_type
+                                      : state->repeated_scalar_container_type;
+}
+
+static Py_ssize_t PyUpb_RepeatedContainer_Length(PyObject* self) {
+  upb_Array* arr =
+      PyUpb_RepeatedContainer_GetIfReified((PyUpb_RepeatedContainer*)self);
+  return arr ? upb_Array_Size(arr) : 0;
+}
+
+PyObject* PyUpb_RepeatedContainer_NewStub(PyObject* parent,
+                                          const upb_FieldDef* f,
+                                          PyObject* arena) {
+  // We only create stubs when the parent is reified, by convention.  However
+  // this is not an invariant: the parent could become reified at any time.
+  assert(PyUpb_Message_GetIfReified(parent) == NULL);
+  PyTypeObject* cls = PyUpb_RepeatedContainer_GetClass(f);
+  PyUpb_RepeatedContainer* repeated = (void*)PyType_GenericAlloc(cls, 0);
+  repeated->arena = arena;
+  repeated->field = (uintptr_t)PyUpb_FieldDescriptor_Get(f) | 1;
+  repeated->ptr.parent = parent;
+  Py_INCREF(arena);
+  Py_INCREF(parent);
+  return &repeated->ob_base;
+}
+
+PyObject* PyUpb_RepeatedContainer_GetOrCreateWrapper(upb_Array* arr,
+                                                     const upb_FieldDef* f,
+                                                     PyObject* arena) {
+  PyObject* ret = PyUpb_ObjCache_Get(arr);
+  if (ret) return ret;
+
+  PyTypeObject* cls = PyUpb_RepeatedContainer_GetClass(f);
+  PyUpb_RepeatedContainer* repeated = (void*)PyType_GenericAlloc(cls, 0);
+  repeated->arena = arena;
+  repeated->field = (uintptr_t)PyUpb_FieldDescriptor_Get(f);
+  repeated->ptr.arr = arr;
+  ret = &repeated->ob_base;
+  Py_INCREF(arena);
+  PyUpb_ObjCache_Add(arr, ret);
+  return ret;
+}
+
+static PyObject* PyUpb_RepeatedContainer_MergeFrom(PyObject* _self,
+                                                   PyObject* args);
+
+PyObject* PyUpb_RepeatedContainer_DeepCopy(PyObject* _self, PyObject* value) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  PyUpb_RepeatedContainer* clone =
+      (void*)PyType_GenericAlloc(Py_TYPE(_self), 0);
+  if (clone == NULL) return NULL;
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  clone->arena = PyUpb_Arena_New();
+  clone->field = (uintptr_t)PyUpb_FieldDescriptor_Get(f);
+  clone->ptr.arr =
+      upb_Array_New(PyUpb_Arena_Get(clone->arena), upb_FieldDef_CType(f));
+  PyUpb_ObjCache_Add(clone->ptr.arr, (PyObject*)clone);
+  PyObject* result = PyUpb_RepeatedContainer_MergeFrom((PyObject*)clone, _self);
+  if (!result) {
+    Py_DECREF(clone);
+    return NULL;
+  }
+  Py_DECREF(result);
+  return (PyObject*)clone;
+}
+
+PyObject* PyUpb_RepeatedContainer_Extend(PyObject* _self, PyObject* value) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  size_t start_size = upb_Array_Size(arr);
+  PyObject* it = PyObject_GetIter(value);
+  if (!it) {
+    PyErr_SetString(PyExc_TypeError, "Value must be iterable");
+    return NULL;
+  }
+
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  bool submsg = upb_FieldDef_IsSubMessage(f);
+  PyObject* e;
+
+  while ((e = PyIter_Next(it))) {
+    PyObject* ret;
+    if (submsg) {
+      ret = PyUpb_RepeatedCompositeContainer_Append(_self, e);
+    } else {
+      ret = PyUpb_RepeatedScalarContainer_Append(_self, e);
+    }
+    Py_XDECREF(ret);
+    Py_DECREF(e);
+  }
+
+  Py_DECREF(it);
+
+  if (PyErr_Occurred()) {
+    upb_Array_Resize(arr, start_size, NULL);
+    return NULL;
+  }
+
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_RepeatedContainer_Item(PyObject* _self,
+                                              Py_ssize_t index) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_GetIfReified(self);
+  Py_ssize_t size = arr ? upb_Array_Size(arr) : 0;
+  if (index < 0 || index >= size) {
+    PyErr_Format(PyExc_IndexError, "list index (%zd) out of range", index);
+    return NULL;
+  }
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  return PyUpb_UpbToPy(upb_Array_Get(arr, index), f, self->arena);
+}
+
+PyObject* PyUpb_RepeatedContainer_ToList(PyObject* _self) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_GetIfReified(self);
+  if (!arr) return PyList_New(0);
+
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  size_t n = upb_Array_Size(arr);
+  PyObject* list = PyList_New(n);
+  for (size_t i = 0; i < n; i++) {
+    PyObject* val = PyUpb_UpbToPy(upb_Array_Get(arr, i), f, self->arena);
+    if (!val) {
+      Py_DECREF(list);
+      return NULL;
+    }
+    PyList_SetItem(list, i, val);
+  }
+  return list;
+}
+
+static PyObject* PyUpb_RepeatedContainer_Repr(PyObject* _self) {
+  PyObject* list = PyUpb_RepeatedContainer_ToList(_self);
+  if (!list) return NULL;
+  assert(!PyErr_Occurred());
+  PyObject* repr = PyObject_Repr(list);
+  Py_DECREF(list);
+  return repr;
+}
+
+static PyObject* PyUpb_RepeatedContainer_RichCompare(PyObject* _self,
+                                                     PyObject* _other,
+                                                     int opid) {
+  if (opid != Py_EQ && opid != Py_NE) {
+    Py_INCREF(Py_NotImplemented);
+    return Py_NotImplemented;
+  }
+  PyObject* list1 = PyUpb_RepeatedContainer_ToList(_self);
+  PyObject* list2 = _other;
+  PyObject* del = NULL;
+  if (PyObject_TypeCheck(_other, _self->ob_type)) {
+    del = list2 = PyUpb_RepeatedContainer_ToList(_other);
+  }
+  PyObject* ret = PyObject_RichCompare(list1, list2, opid);
+  Py_DECREF(list1);
+  Py_XDECREF(del);
+  return ret;
+}
+
+static PyObject* PyUpb_RepeatedContainer_Subscript(PyObject* _self,
+                                                   PyObject* key) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_GetIfReified(self);
+  Py_ssize_t size = arr ? upb_Array_Size(arr) : 0;
+  Py_ssize_t idx, count, step;
+  if (!PyUpb_IndexToRange(key, size, &idx, &count, &step)) return NULL;
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  if (step == 0) {
+    return PyUpb_UpbToPy(upb_Array_Get(arr, idx), f, self->arena);
+  } else {
+    PyObject* list = PyList_New(count);
+    for (Py_ssize_t i = 0; i < count; i++, idx += step) {
+      upb_MessageValue msgval = upb_Array_Get(self->ptr.arr, idx);
+      PyObject* item = PyUpb_UpbToPy(msgval, f, self->arena);
+      if (!item) {
+        Py_DECREF(list);
+        return NULL;
+      }
+      PyList_SetItem(list, i, item);
+    }
+    return list;
+  }
+}
+
+static int PyUpb_RepeatedContainer_SetSubscript(
+    PyUpb_RepeatedContainer* self, upb_Array* arr, const upb_FieldDef* f,
+    Py_ssize_t idx, Py_ssize_t count, Py_ssize_t step, PyObject* value) {
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  if (upb_FieldDef_IsSubMessage(f)) {
+    PyErr_SetString(PyExc_TypeError, "does not support assignment");
+    return -1;
+  }
+
+  if (step == 0) {
+    // Set single value.
+    upb_MessageValue msgval;
+    if (!PyUpb_PyToUpb(value, f, &msgval, arena)) return -1;
+    upb_Array_Set(arr, idx, msgval);
+    return 0;
+  }
+
+  // Set range.
+  PyObject* seq =
+      PySequence_Fast(value, "must assign iterable to extended slice");
+  PyObject* item = NULL;
+  int ret = -1;
+  if (!seq) goto err;
+  Py_ssize_t seq_size = PySequence_Size(seq);
+  if (seq_size != count) {
+    if (step == 1) {
+      // We must shift the tail elements (either right or left).
+      size_t tail = upb_Array_Size(arr) - (idx + count);
+      upb_Array_Resize(arr, idx + seq_size + tail, arena);
+      upb_Array_Move(arr, idx + seq_size, idx + count, tail);
+      count = seq_size;
+    } else {
+      PyErr_Format(PyExc_ValueError,
+                   "attempt to assign sequence of  %zd to extended slice "
+                   "of size %zd",
+                   seq_size, count);
+      goto err;
+    }
+  }
+  for (Py_ssize_t i = 0; i < count; i++, idx += step) {
+    upb_MessageValue msgval;
+    item = PySequence_GetItem(seq, i);
+    if (!item) goto err;
+    // XXX: if this fails we can leave the list partially mutated.
+    if (!PyUpb_PyToUpb(item, f, &msgval, arena)) goto err;
+    Py_DECREF(item);
+    item = NULL;
+    upb_Array_Set(arr, idx, msgval);
+  }
+  ret = 0;
+
+err:
+  Py_XDECREF(seq);
+  Py_XDECREF(item);
+  return ret;
+}
+
+static int PyUpb_RepeatedContainer_DeleteSubscript(upb_Array* arr,
+                                                   Py_ssize_t idx,
+                                                   Py_ssize_t count,
+                                                   Py_ssize_t step) {
+  // Normalize direction: deletion is order-independent.
+  Py_ssize_t start = idx;
+  if (step < 0) {
+    Py_ssize_t end = start + step * (count - 1);
+    start = end;
+    step = -step;
+  }
+
+  size_t dst = start;
+  size_t src;
+  if (step > 1) {
+    // Move elements between steps:
+    //
+    //        src
+    //         |
+    // |------X---X---X---X------------------------------|
+    //        |
+    //       dst           <-------- tail -------------->
+    src = start + 1;
+    for (Py_ssize_t i = 1; i < count; i++, dst += step - 1, src += step) {
+      upb_Array_Move(arr, dst, src, step);
+    }
+  } else {
+    src = start + count;
+  }
+
+  // Move tail.
+  size_t tail = upb_Array_Size(arr) - src;
+  size_t new_size = dst + tail;
+  assert(new_size == upb_Array_Size(arr) - count);
+  upb_Array_Move(arr, dst, src, tail);
+  upb_Array_Resize(arr, new_size, NULL);
+  return 0;
+}
+
+static int PyUpb_RepeatedContainer_AssignSubscript(PyObject* _self,
+                                                   PyObject* key,
+                                                   PyObject* value) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  Py_ssize_t size = arr ? upb_Array_Size(arr) : 0;
+  Py_ssize_t idx, count, step;
+  if (!PyUpb_IndexToRange(key, size, &idx, &count, &step)) return -1;
+  if (value) {
+    return PyUpb_RepeatedContainer_SetSubscript(self, arr, f, idx, count, step,
+                                                value);
+  } else {
+    return PyUpb_RepeatedContainer_DeleteSubscript(arr, idx, count, step);
+  }
+}
+
+static PyObject* PyUpb_RepeatedContainer_Pop(PyObject* _self, PyObject* args) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  Py_ssize_t index = -1;
+  if (!PyArg_ParseTuple(args, "|n", &index)) return NULL;
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  size_t size = upb_Array_Size(arr);
+  if (index < 0) index += size;
+  if (index >= size) index = size - 1;
+  PyObject* ret = PyUpb_RepeatedContainer_Item(_self, index);
+  if (!ret) return NULL;
+  upb_Array_Delete(self->ptr.arr, index, 1);
+  return ret;
+}
+
+static PyObject* PyUpb_RepeatedContainer_Remove(PyObject* _self,
+                                                PyObject* value) {
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  Py_ssize_t match_index = -1;
+  Py_ssize_t n = PyUpb_RepeatedContainer_Length(_self);
+  for (Py_ssize_t i = 0; i < n; ++i) {
+    PyObject* elem = PyUpb_RepeatedContainer_Item(_self, i);
+    if (!elem) return NULL;
+    int eq = PyObject_RichCompareBool(elem, value, Py_EQ);
+    Py_DECREF(elem);
+    if (eq) {
+      match_index = i;
+      break;
+    }
+  }
+  if (match_index == -1) {
+    PyErr_SetString(PyExc_ValueError, "remove(x): x not in container");
+    return NULL;
+  }
+  if (PyUpb_RepeatedContainer_DeleteSubscript(arr, match_index, 1, 1) < 0) {
+    return NULL;
+  }
+  Py_RETURN_NONE;
+}
+
+// A helper function used only for Sort().
+static bool PyUpb_RepeatedContainer_Assign(PyObject* _self, PyObject* list) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  Py_ssize_t size = PyList_Size(list);
+  bool submsg = upb_FieldDef_IsSubMessage(f);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  for (Py_ssize_t i = 0; i < size; ++i) {
+    PyObject* obj = PyList_GetItem(list, i);
+    upb_MessageValue msgval;
+    if (submsg) {
+      msgval.msg_val = PyUpb_Message_GetIfReified(obj);
+      assert(msgval.msg_val);
+    } else {
+      if (!PyUpb_PyToUpb(obj, f, &msgval, arena)) return false;
+    }
+    upb_Array_Set(arr, i, msgval);
+  }
+  return true;
+}
+
+static PyObject* PyUpb_RepeatedContainer_Sort(PyObject* pself, PyObject* args,
+                                              PyObject* kwds) {
+  // Support the old sort_function argument for backwards
+  // compatibility.
+  if (kwds != NULL) {
+    PyObject* sort_func = PyDict_GetItemString(kwds, "sort_function");
+    if (sort_func != NULL) {
+      // Must set before deleting as sort_func is a borrowed reference
+      // and kwds might be the only thing keeping it alive.
+      if (PyDict_SetItemString(kwds, "cmp", sort_func) == -1) return NULL;
+      if (PyDict_DelItemString(kwds, "sort_function") == -1) return NULL;
+    }
+  }
+
+  PyObject* ret = NULL;
+  PyObject* full_slice = NULL;
+  PyObject* list = NULL;
+  PyObject* m = NULL;
+  PyObject* res = NULL;
+  if ((full_slice = PySlice_New(NULL, NULL, NULL)) &&
+      (list = PyUpb_RepeatedContainer_Subscript(pself, full_slice)) &&
+      (m = PyObject_GetAttrString(list, "sort")) &&
+      (res = PyObject_Call(m, args, kwds)) &&
+      PyUpb_RepeatedContainer_Assign(pself, list)) {
+    Py_INCREF(Py_None);
+    ret = Py_None;
+  }
+
+  Py_XDECREF(full_slice);
+  Py_XDECREF(list);
+  Py_XDECREF(m);
+  Py_XDECREF(res);
+  return ret;
+}
+
+static PyObject* PyUpb_RepeatedContainer_Reverse(PyObject* _self) {
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  size_t n = upb_Array_Size(arr);
+  size_t half = n / 2;  // Rounds down.
+  for (size_t i = 0; i < half; i++) {
+    size_t i2 = n - i - 1;
+    upb_MessageValue val1 = upb_Array_Get(arr, i);
+    upb_MessageValue val2 = upb_Array_Get(arr, i2);
+    upb_Array_Set(arr, i, val2);
+    upb_Array_Set(arr, i2, val1);
+  }
+  Py_RETURN_NONE;
+}
+
+static PyObject* PyUpb_RepeatedContainer_MergeFrom(PyObject* _self,
+                                                   PyObject* args) {
+  return PyUpb_RepeatedContainer_Extend(_self, args);
+}
+
+// -----------------------------------------------------------------------------
+// RepeatedCompositeContainer
+// -----------------------------------------------------------------------------
+
+static PyObject* PyUpb_RepeatedCompositeContainer_AppendNew(PyObject* _self) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  if (!arr) return NULL;
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  const upb_MessageDef* m = upb_FieldDef_MessageSubDef(f);
+  const upb_MiniTable* layout = upb_MessageDef_MiniTable(m);
+  upb_Message* msg = upb_Message_New(layout, arena);
+  upb_MessageValue msgval = {.msg_val = msg};
+  upb_Array_Append(arr, msgval, arena);
+  return PyUpb_Message_Get(msg, m, self->arena);
+}
+
+PyObject* PyUpb_RepeatedCompositeContainer_Add(PyObject* _self, PyObject* args,
+                                               PyObject* kwargs) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  PyObject* py_msg = PyUpb_RepeatedCompositeContainer_AppendNew(_self);
+  if (!py_msg) return NULL;
+  if (PyUpb_Message_InitAttributes(py_msg, args, kwargs) < 0) {
+    Py_DECREF(py_msg);
+    upb_Array_Delete(self->ptr.arr, upb_Array_Size(self->ptr.arr) - 1, 1);
+    return NULL;
+  }
+  return py_msg;
+}
+
+static PyObject* PyUpb_RepeatedCompositeContainer_Append(PyObject* _self,
+                                                         PyObject* value) {
+  if (!PyUpb_Message_Verify(value)) return NULL;
+  PyObject* py_msg = PyUpb_RepeatedCompositeContainer_AppendNew(_self);
+  if (!py_msg) return NULL;
+  PyObject* none = PyUpb_Message_MergeFrom(py_msg, value);
+  if (!none) {
+    Py_DECREF(py_msg);
+    return NULL;
+  }
+  Py_DECREF(none);
+  return py_msg;
+}
+
+static PyObject* PyUpb_RepeatedContainer_Insert(PyObject* _self,
+                                                PyObject* args) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  Py_ssize_t index;
+  PyObject* value;
+  if (!PyArg_ParseTuple(args, "nO", &index, &value)) return NULL;
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  if (!arr) return NULL;
+
+  // Normalize index.
+  Py_ssize_t size = upb_Array_Size(arr);
+  if (index < 0) index += size;
+  if (index < 0) index = 0;
+  if (index > size) index = size;
+
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  upb_MessageValue msgval;
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  if (upb_FieldDef_IsSubMessage(f)) {
+    // Create message.
+    const upb_MessageDef* m = upb_FieldDef_MessageSubDef(f);
+    const upb_MiniTable* layout = upb_MessageDef_MiniTable(m);
+    upb_Message* msg = upb_Message_New(layout, arena);
+    PyObject* py_msg = PyUpb_Message_Get(msg, m, self->arena);
+    PyObject* ret = PyUpb_Message_MergeFrom(py_msg, value);
+    Py_DECREF(py_msg);
+    if (!ret) return NULL;
+    Py_DECREF(ret);
+    msgval.msg_val = msg;
+  } else {
+    if (!PyUpb_PyToUpb(value, f, &msgval, arena)) return NULL;
+  }
+
+  upb_Array_Insert(arr, index, 1, arena);
+  upb_Array_Set(arr, index, msgval);
+
+  Py_RETURN_NONE;
+}
+
+static PyMethodDef PyUpb_RepeatedCompositeContainer_Methods[] = {
+    {"__deepcopy__", PyUpb_RepeatedContainer_DeepCopy, METH_VARARGS,
+     "Makes a deep copy of the class."},
+    {"add", (PyCFunction)PyUpb_RepeatedCompositeContainer_Add,
+     METH_VARARGS | METH_KEYWORDS, "Adds an object to the repeated container."},
+    {"append", PyUpb_RepeatedCompositeContainer_Append, METH_O,
+     "Appends a message to the end of the repeated container."},
+    {"insert", PyUpb_RepeatedContainer_Insert, METH_VARARGS,
+     "Inserts a message before the specified index."},
+    {"extend", PyUpb_RepeatedContainer_Extend, METH_O,
+     "Adds objects to the repeated container."},
+    {"pop", PyUpb_RepeatedContainer_Pop, METH_VARARGS,
+     "Removes an object from the repeated container and returns it."},
+    {"remove", PyUpb_RepeatedContainer_Remove, METH_O,
+     "Removes an object from the repeated container."},
+    {"sort", (PyCFunction)PyUpb_RepeatedContainer_Sort,
+     METH_VARARGS | METH_KEYWORDS, "Sorts the repeated container."},
+    {"reverse", (PyCFunction)PyUpb_RepeatedContainer_Reverse, METH_NOARGS,
+     "Reverses elements order of the repeated container."},
+    {"MergeFrom", PyUpb_RepeatedContainer_MergeFrom, METH_O,
+     "Adds objects to the repeated container."},
+    {NULL, NULL}};
+
+static PyType_Slot PyUpb_RepeatedCompositeContainer_Slots[] = {
+    {Py_tp_dealloc, PyUpb_RepeatedContainer_Dealloc},
+    {Py_tp_methods, PyUpb_RepeatedCompositeContainer_Methods},
+    {Py_sq_length, PyUpb_RepeatedContainer_Length},
+    {Py_sq_item, PyUpb_RepeatedContainer_Item},
+    {Py_mp_length, PyUpb_RepeatedContainer_Length},
+    {Py_tp_repr, PyUpb_RepeatedContainer_Repr},
+    {Py_mp_subscript, PyUpb_RepeatedContainer_Subscript},
+    {Py_mp_ass_subscript, PyUpb_RepeatedContainer_AssignSubscript},
+    {Py_tp_new, PyUpb_Forbidden_New},
+    {Py_tp_richcompare, PyUpb_RepeatedContainer_RichCompare},
+    {Py_tp_hash, PyObject_HashNotImplemented},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_RepeatedCompositeContainer_Spec = {
+    PYUPB_MODULE_NAME ".RepeatedCompositeContainer",
+    sizeof(PyUpb_RepeatedContainer),
+    0,  // tp_itemsize
+    Py_TPFLAGS_DEFAULT,
+    PyUpb_RepeatedCompositeContainer_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// RepeatedScalarContainer
+// -----------------------------------------------------------------------------
+
+static PyObject* PyUpb_RepeatedScalarContainer_Append(PyObject* _self,
+                                                      PyObject* value) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_EnsureReified(_self);
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  upb_MessageValue msgval;
+  if (!PyUpb_PyToUpb(value, f, &msgval, arena)) {
+    return NULL;
+  }
+  upb_Array_Append(arr, msgval, arena);
+  Py_RETURN_NONE;
+}
+
+static int PyUpb_RepeatedScalarContainer_AssignItem(PyObject* _self,
+                                                    Py_ssize_t index,
+                                                    PyObject* item) {
+  PyUpb_RepeatedContainer* self = (PyUpb_RepeatedContainer*)_self;
+  upb_Array* arr = PyUpb_RepeatedContainer_GetIfReified(self);
+  Py_ssize_t size = arr ? upb_Array_Size(arr) : 0;
+  if (index < 0 || index >= size) {
+    PyErr_Format(PyExc_IndexError, "list index (%zd) out of range", index);
+    return -1;
+  }
+  const upb_FieldDef* f = PyUpb_RepeatedContainer_GetField(self);
+  upb_MessageValue msgval;
+  upb_Arena* arena = PyUpb_Arena_Get(self->arena);
+  if (!PyUpb_PyToUpb(item, f, &msgval, arena)) {
+    return -1;
+  }
+  upb_Array_Set(self->ptr.arr, index, msgval);
+  return 0;
+}
+
+static PyObject* PyUpb_RepeatedScalarContainer_Reduce(PyObject* unused_self,
+                                                      PyObject* unused_other) {
+  PyObject* pickle_module = PyImport_ImportModule("pickle");
+  if (!pickle_module) return NULL;
+  PyObject* pickle_error = PyObject_GetAttrString(pickle_module, "PickleError");
+  Py_DECREF(pickle_module);
+  if (!pickle_error) return NULL;
+  PyErr_Format(pickle_error,
+               "can't pickle repeated message fields, convert to list first");
+  Py_DECREF(pickle_error);
+  return NULL;
+}
+
+static PyMethodDef PyUpb_RepeatedScalarContainer_Methods[] = {
+    {"__deepcopy__", PyUpb_RepeatedContainer_DeepCopy, METH_VARARGS,
+     "Makes a deep copy of the class."},
+    {"__reduce__", PyUpb_RepeatedScalarContainer_Reduce, METH_NOARGS,
+     "Outputs picklable representation of the repeated field."},
+    {"append", PyUpb_RepeatedScalarContainer_Append, METH_O,
+     "Appends an object to the repeated container."},
+    {"extend", PyUpb_RepeatedContainer_Extend, METH_O,
+     "Appends objects to the repeated container."},
+    {"insert", PyUpb_RepeatedContainer_Insert, METH_VARARGS,
+     "Inserts an object at the specified position in the container."},
+    {"pop", PyUpb_RepeatedContainer_Pop, METH_VARARGS,
+     "Removes an object from the repeated container and returns it."},
+    {"remove", PyUpb_RepeatedContainer_Remove, METH_O,
+     "Removes an object from the repeated container."},
+    {"sort", (PyCFunction)PyUpb_RepeatedContainer_Sort,
+     METH_VARARGS | METH_KEYWORDS, "Sorts the repeated container."},
+    {"reverse", (PyCFunction)PyUpb_RepeatedContainer_Reverse, METH_NOARGS,
+     "Reverses elements order of the repeated container."},
+    {"MergeFrom", PyUpb_RepeatedContainer_MergeFrom, METH_O,
+     "Merges a repeated container into the current container."},
+    {NULL, NULL}};
+
+static PyType_Slot PyUpb_RepeatedScalarContainer_Slots[] = {
+    {Py_tp_dealloc, PyUpb_RepeatedContainer_Dealloc},
+    {Py_tp_methods, PyUpb_RepeatedScalarContainer_Methods},
+    {Py_tp_new, PyUpb_Forbidden_New},
+    {Py_tp_repr, PyUpb_RepeatedContainer_Repr},
+    {Py_sq_length, PyUpb_RepeatedContainer_Length},
+    {Py_sq_item, PyUpb_RepeatedContainer_Item},
+    {Py_sq_ass_item, PyUpb_RepeatedScalarContainer_AssignItem},
+    {Py_mp_length, PyUpb_RepeatedContainer_Length},
+    {Py_mp_subscript, PyUpb_RepeatedContainer_Subscript},
+    {Py_mp_ass_subscript, PyUpb_RepeatedContainer_AssignSubscript},
+    {Py_tp_richcompare, PyUpb_RepeatedContainer_RichCompare},
+    {Py_tp_hash, PyObject_HashNotImplemented},
+    {0, NULL}};
+
+static PyType_Spec PyUpb_RepeatedScalarContainer_Spec = {
+    PYUPB_MODULE_NAME ".RepeatedScalarContainer",
+    sizeof(PyUpb_RepeatedContainer),
+    0,  // tp_itemsize
+    Py_TPFLAGS_DEFAULT,
+    PyUpb_RepeatedScalarContainer_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// Top Level
+// -----------------------------------------------------------------------------
+
+static bool PyUpb_Repeated_RegisterAsSequence(PyUpb_ModuleState* state) {
+  PyObject* collections = NULL;
+  PyObject* seq = NULL;
+  PyObject* ret1 = NULL;
+  PyObject* ret2 = NULL;
+  PyTypeObject* type1 = state->repeated_scalar_container_type;
+  PyTypeObject* type2 = state->repeated_composite_container_type;
+
+  bool ok = (collections = PyImport_ImportModule("collections.abc")) &&
+            (seq = PyObject_GetAttrString(collections, "MutableSequence")) &&
+            (ret1 = PyObject_CallMethod(seq, "register", "O", type1)) &&
+            (ret2 = PyObject_CallMethod(seq, "register", "O", type2));
+
+  Py_XDECREF(collections);
+  Py_XDECREF(seq);
+  Py_XDECREF(ret1);
+  Py_XDECREF(ret2);
+  return ok;
+}
+
+bool PyUpb_Repeated_Init(PyObject* m) {
+  PyUpb_ModuleState* state = PyUpb_ModuleState_GetFromModule(m);
+
+  state->repeated_composite_container_type =
+      PyUpb_AddClass(m, &PyUpb_RepeatedCompositeContainer_Spec);
+  state->repeated_scalar_container_type =
+      PyUpb_AddClass(m, &PyUpb_RepeatedScalarContainer_Spec);
+
+  return state->repeated_composite_container_type &&
+         state->repeated_scalar_container_type &&
+         PyUpb_Repeated_RegisterAsSequence(state);
+}
diff --git a/python/repeated.h b/python/repeated.h
new file mode 100644
index 0000000..54670e7
--- /dev/null
+++ b/python/repeated.h
@@ -0,0 +1,72 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_REPEATED_H__
+#define PYUPB_REPEATED_H__
+
+#include <stdbool.h>
+
+#include "python/python_api.h"
+#include "upb/reflection/def.h"
+
+// Creates a new repeated field stub for field `f` of message object `parent`.
+// Precondition: `parent` must be a stub.
+PyObject* PyUpb_RepeatedContainer_NewStub(PyObject* parent,
+                                          const upb_FieldDef* f,
+                                          PyObject* arena);
+
+// Returns a repeated field object wrapping `arr`, of field type `f`, which
+// must be on `arena`.  If an existing wrapper object exists, it will be
+// returned, otherwise a new object will be created.  The caller always owns a
+// ref on the returned value.
+PyObject* PyUpb_RepeatedContainer_GetOrCreateWrapper(upb_Array* arr,
+                                                     const upb_FieldDef* f,
+                                                     PyObject* arena);
+
+// Reifies a repeated field stub to point to the concrete data in `arr`.
+// If `arr` is NULL, an appropriate empty array will be constructed.
+void PyUpb_RepeatedContainer_Reify(PyObject* self, upb_Array* arr);
+
+// Reifies this repeated object if it is not already reified.
+upb_Array* PyUpb_RepeatedContainer_EnsureReified(PyObject* self);
+
+// Implements repeated_field.extend(iterable).  `_self` must be a repeated
+// field (either repeated composite or repeated scalar).
+PyObject* PyUpb_RepeatedContainer_Extend(PyObject* _self, PyObject* value);
+
+// Implements repeated_field.add(initial_values).  `_self` must be a repeated
+// composite field.
+PyObject* PyUpb_RepeatedCompositeContainer_Add(PyObject* _self, PyObject* args,
+                                               PyObject* kwargs);
+
+// Module-level init.
+bool PyUpb_Repeated_Init(PyObject* m);
+
+#endif  // PYUPB_REPEATED_H__
diff --git a/python/requirements.txt b/python/requirements.txt
new file mode 100644
index 0000000..ad71bf2
--- /dev/null
+++ b/python/requirements.txt
@@ -0,0 +1 @@
+numpy<=1.24.4
diff --git a/python/unknown_fields.c b/python/unknown_fields.c
new file mode 100644
index 0000000..f228f23
--- /dev/null
+++ b/python/unknown_fields.c
@@ -0,0 +1,358 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "python/unknown_fields.h"
+
+#include "python/message.h"
+#include "python/protobuf.h"
+#include "upb/wire/eps_copy_input_stream.h"
+#include "upb/wire/reader.h"
+#include "upb/wire/types.h"
+
+// -----------------------------------------------------------------------------
+// UnknownFieldSet
+// -----------------------------------------------------------------------------
+
+typedef struct {
+  PyObject_HEAD;
+  PyObject* fields;
+} PyUpb_UnknownFieldSet;
+
+static void PyUpb_UnknownFieldSet_Dealloc(PyObject* _self) {
+  PyUpb_UnknownFieldSet* self = (PyUpb_UnknownFieldSet*)_self;
+  Py_XDECREF(self->fields);
+  PyUpb_Dealloc(self);
+}
+
+PyUpb_UnknownFieldSet* PyUpb_UnknownFieldSet_NewBare(void) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  PyUpb_UnknownFieldSet* self =
+      (void*)PyType_GenericAlloc(s->unknown_fields_type, 0);
+  return self;
+}
+
+// For MessageSet the established behavior is for UnknownFieldSet to interpret
+// the MessageSet wire format:
+//    message MessageSet {
+//      repeated group Item = 1 {
+//        required int32 type_id = 2;
+//        required bytes message = 3;
+//      }
+//    }
+//
+// And create unknown fields like:
+//   UnknownField(type_id, WIRE_TYPE_DELIMITED, message)
+//
+// For any unknown fields that are unexpected per the wire format defined above,
+// we drop them on the floor.
+
+enum {
+  kUpb_MessageSet_StartItemTag = (1 << 3) | kUpb_WireType_StartGroup,
+  kUpb_MessageSet_EndItemTag = (1 << 3) | kUpb_WireType_EndGroup,
+  kUpb_MessageSet_TypeIdTag = (2 << 3) | kUpb_WireType_Varint,
+  kUpb_MessageSet_MessageTag = (3 << 3) | kUpb_WireType_Delimited,
+};
+
+static const char* PyUpb_UnknownFieldSet_BuildMessageSetItem(
+    PyUpb_UnknownFieldSet* self, upb_EpsCopyInputStream* stream,
+    const char* ptr) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  int type_id = 0;
+  PyObject* msg = NULL;
+  while (!upb_EpsCopyInputStream_IsDone(stream, &ptr)) {
+    uint32_t tag;
+    ptr = upb_WireReader_ReadTag(ptr, &tag);
+    if (!ptr) goto err;
+    switch (tag) {
+      case kUpb_MessageSet_EndItemTag:
+        goto done;
+      case kUpb_MessageSet_TypeIdTag: {
+        uint64_t tmp;
+        ptr = upb_WireReader_ReadVarint(ptr, &tmp);
+        if (!ptr) goto err;
+        if (!type_id) type_id = tmp;
+        break;
+      }
+      case kUpb_MessageSet_MessageTag: {
+        int size;
+        ptr = upb_WireReader_ReadSize(ptr, &size);
+        if (!upb_EpsCopyInputStream_CheckDataSizeAvailable(stream, ptr, size)) {
+          goto err;
+        }
+        const char* str = ptr;
+        ptr = upb_EpsCopyInputStream_ReadStringAliased(stream, &str, size);
+        if (!msg) {
+          msg = PyBytes_FromStringAndSize(str, size);
+          if (!msg) goto err;
+        } else {
+          // already saw a message here so deliberately skipping the duplicate
+        }
+        break;
+      }
+      default:
+        ptr = upb_WireReader_SkipValue(ptr, tag, stream);
+        if (!ptr) goto err;
+    }
+  }
+
+done:
+  if (type_id && msg) {
+    PyObject* field = PyObject_CallFunction(
+        s->unknown_field_type, "iiO", type_id, kUpb_WireType_Delimited, msg);
+    if (!field) goto err;
+    PyList_Append(self->fields, field);
+    Py_DECREF(field);
+  }
+  Py_XDECREF(msg);
+  return ptr;
+
+err:
+  Py_XDECREF(msg);
+  return NULL;
+}
+
+static const char* PyUpb_UnknownFieldSet_BuildMessageSet(
+    PyUpb_UnknownFieldSet* self, upb_EpsCopyInputStream* stream,
+    const char* ptr) {
+  self->fields = PyList_New(0);
+  while (!upb_EpsCopyInputStream_IsDone(stream, &ptr)) {
+    uint32_t tag;
+    ptr = upb_WireReader_ReadTag(ptr, &tag);
+    if (!ptr) goto err;
+    if (tag == kUpb_MessageSet_StartItemTag) {
+      ptr = PyUpb_UnknownFieldSet_BuildMessageSetItem(self, stream, ptr);
+    } else {
+      ptr = upb_WireReader_SkipValue(ptr, tag, stream);
+    }
+    if (!ptr) goto err;
+  }
+  if (upb_EpsCopyInputStream_IsError(stream)) goto err;
+  return ptr;
+
+err:
+  Py_DECREF(self->fields);
+  self->fields = NULL;
+  return NULL;
+}
+
+static const char* PyUpb_UnknownFieldSet_Build(PyUpb_UnknownFieldSet* self,
+                                               upb_EpsCopyInputStream* stream,
+                                               const char* ptr,
+                                               int group_number);
+
+static const char* PyUpb_UnknownFieldSet_BuildValue(
+    PyUpb_UnknownFieldSet* self, upb_EpsCopyInputStream* stream,
+    const char* ptr, int field_number, int wire_type, int group_number,
+    PyObject** data) {
+  switch (wire_type) {
+    case kUpb_WireType_Varint: {
+      uint64_t val;
+      ptr = upb_WireReader_ReadVarint(ptr, &val);
+      if (!ptr) return NULL;
+      *data = PyLong_FromUnsignedLongLong(val);
+      return ptr;
+    }
+    case kUpb_WireType_64Bit: {
+      uint64_t val;
+      ptr = upb_WireReader_ReadFixed64(ptr, &val);
+      *data = PyLong_FromUnsignedLongLong(val);
+      return ptr;
+    }
+    case kUpb_WireType_32Bit: {
+      uint32_t val;
+      ptr = upb_WireReader_ReadFixed32(ptr, &val);
+      *data = PyLong_FromUnsignedLongLong(val);
+      return ptr;
+    }
+    case kUpb_WireType_Delimited: {
+      int size;
+      ptr = upb_WireReader_ReadSize(ptr, &size);
+      if (!upb_EpsCopyInputStream_CheckDataSizeAvailable(stream, ptr, size)) {
+        return NULL;
+      }
+      const char* str = ptr;
+      ptr = upb_EpsCopyInputStream_ReadStringAliased(stream, &str, size);
+      *data = PyBytes_FromStringAndSize(str, size);
+      return ptr;
+    }
+    case kUpb_WireType_StartGroup: {
+      PyUpb_UnknownFieldSet* sub = PyUpb_UnknownFieldSet_NewBare();
+      if (!sub) return NULL;
+      *data = &sub->ob_base;
+      return PyUpb_UnknownFieldSet_Build(sub, stream, ptr, field_number);
+    }
+    default:
+      assert(0);
+      *data = NULL;
+      return NULL;
+  }
+}
+
+// For non-MessageSet we just build the unknown fields exactly as they exist on
+// the wire.
+static const char* PyUpb_UnknownFieldSet_Build(PyUpb_UnknownFieldSet* self,
+                                               upb_EpsCopyInputStream* stream,
+                                               const char* ptr,
+                                               int group_number) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_Get();
+  self->fields = PyList_New(0);
+  while (!upb_EpsCopyInputStream_IsDone(stream, &ptr)) {
+    uint32_t tag;
+    ptr = upb_WireReader_ReadTag(ptr, &tag);
+    if (!ptr) goto err;
+    PyObject* data = NULL;
+    int field_number = upb_WireReader_GetFieldNumber(tag);
+    int wire_type = upb_WireReader_GetWireType(tag);
+    if (wire_type == kUpb_WireType_EndGroup) {
+      if (field_number != group_number) return NULL;
+      return ptr;
+    }
+    ptr = PyUpb_UnknownFieldSet_BuildValue(self, stream, ptr, field_number,
+                                           wire_type, group_number, &data);
+    if (!ptr) {
+      Py_XDECREF(data);
+      goto err;
+    }
+    assert(data);
+    PyObject* field = PyObject_CallFunction(s->unknown_field_type, "iiN",
+                                            field_number, wire_type, data);
+    PyList_Append(self->fields, field);
+    Py_DECREF(field);
+  }
+  if (upb_EpsCopyInputStream_IsError(stream)) goto err;
+  return ptr;
+
+err:
+  Py_DECREF(self->fields);
+  self->fields = NULL;
+  return NULL;
+}
+
+static PyObject* PyUpb_UnknownFieldSet_New(PyTypeObject* type, PyObject* args,
+                                           PyObject* kwargs) {
+  char* kwlist[] = {"message", 0};
+  PyObject* py_msg = NULL;
+
+  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &py_msg)) {
+    return NULL;
+  }
+
+  if (!PyUpb_Message_Verify(py_msg)) return NULL;
+  PyUpb_UnknownFieldSet* self = PyUpb_UnknownFieldSet_NewBare();
+  upb_Message* msg = PyUpb_Message_GetIfReified(py_msg);
+  if (!msg) return &self->ob_base;
+
+  size_t size;
+  const char* ptr = upb_Message_GetUnknown(msg, &size);
+  if (size == 0) return &self->ob_base;
+
+  upb_EpsCopyInputStream stream;
+  upb_EpsCopyInputStream_Init(&stream, &ptr, size, true);
+  const upb_MessageDef* msgdef = PyUpb_Message_GetMsgdef(py_msg);
+
+  bool ok;
+  if (upb_MessageDef_IsMessageSet(msgdef)) {
+    ok = PyUpb_UnknownFieldSet_BuildMessageSet(self, &stream, ptr) != NULL;
+  } else {
+    ok = PyUpb_UnknownFieldSet_Build(self, &stream, ptr, -1) != NULL;
+  }
+
+  if (!ok) {
+    Py_DECREF(&self->ob_base);
+    return NULL;
+  }
+
+  return &self->ob_base;
+}
+
+static Py_ssize_t PyUpb_UnknownFieldSet_Length(PyObject* _self) {
+  PyUpb_UnknownFieldSet* self = (PyUpb_UnknownFieldSet*)_self;
+  return self->fields ? PyObject_Length(self->fields) : 0;
+}
+
+static PyObject* PyUpb_UnknownFieldSet_GetItem(PyObject* _self,
+                                               Py_ssize_t index) {
+  PyUpb_UnknownFieldSet* self = (PyUpb_UnknownFieldSet*)_self;
+  if (!self->fields) {
+    PyErr_Format(PyExc_IndexError, "list index (%zd) out of range", index);
+    return NULL;
+  }
+  PyObject* ret = PyList_GetItem(self->fields, index);
+  if (ret) Py_INCREF(ret);
+  return ret;
+}
+
+static PyType_Slot PyUpb_UnknownFieldSet_Slots[] = {
+    {Py_tp_new, &PyUpb_UnknownFieldSet_New},
+    {Py_tp_dealloc, &PyUpb_UnknownFieldSet_Dealloc},
+    {Py_sq_length, PyUpb_UnknownFieldSet_Length},
+    {Py_sq_item, PyUpb_UnknownFieldSet_GetItem},
+    {Py_tp_hash, PyObject_HashNotImplemented},
+    {0, NULL},
+};
+
+static PyType_Spec PyUpb_UnknownFieldSet_Spec = {
+    PYUPB_MODULE_NAME ".UnknownFieldSet",  // tp_name
+    sizeof(PyUpb_UnknownFieldSet),         // tp_basicsize
+    0,                                     // tp_itemsize
+    Py_TPFLAGS_DEFAULT,                    // tp_flags
+    PyUpb_UnknownFieldSet_Slots,
+};
+
+// -----------------------------------------------------------------------------
+// Top Level
+// -----------------------------------------------------------------------------
+
+PyObject* PyUpb_UnknownFieldSet_CreateNamedTuple(void) {
+  PyObject* mod = NULL;
+  PyObject* namedtuple = NULL;
+  PyObject* ret = NULL;
+
+  mod = PyImport_ImportModule("collections");
+  if (!mod) goto done;
+  namedtuple = PyObject_GetAttrString(mod, "namedtuple");
+  if (!namedtuple) goto done;
+  ret = PyObject_CallFunction(namedtuple, "s[sss]", "PyUnknownField",
+                              "field_number", "wire_type", "data");
+
+done:
+  Py_XDECREF(mod);
+  Py_XDECREF(namedtuple);
+  return ret;
+}
+
+bool PyUpb_UnknownFields_Init(PyObject* m) {
+  PyUpb_ModuleState* s = PyUpb_ModuleState_GetFromModule(m);
+
+  s->unknown_fields_type = PyUpb_AddClass(m, &PyUpb_UnknownFieldSet_Spec);
+  s->unknown_field_type = PyUpb_UnknownFieldSet_CreateNamedTuple();
+
+  return s->unknown_fields_type && s->unknown_field_type;
+}
diff --git a/python/unknown_fields.h b/python/unknown_fields.h
new file mode 100644
index 0000000..85ea40c
--- /dev/null
+++ b/python/unknown_fields.h
@@ -0,0 +1,42 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2023 Google LLC.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef PYUPB_UNKNOWN_FIELDS_H__
+#define PYUPB_UNKNOWN_FIELDS_H__
+
+#include <stdbool.h>
+
+#include "python/python_api.h"
+
+PyObject* PyUpb_UnknownFields_New(PyObject* msg);
+
+bool PyUpb_UnknownFields_Init(PyObject* m);
+
+#endif  // PYUPB_UNKNOWN_FIELDS_H__
diff --git a/python/version_script.lds b/python/version_script.lds
new file mode 100644
index 0000000..7cb8300
--- /dev/null
+++ b/python/version_script.lds
@@ -0,0 +1,6 @@
+message {
+  global:
+    PyInit__message;
+  local:
+    *;
+};