Where is zip?

Download a copy of CPython’s source:

% git clone https://github.com/python/cpython

Where is zip implemented?

% cd cpython
% grep -Irn "def zip"
... bunch of stuff about zipfile...

% grep -Irn \"zip 
...
./Python/bltinmodule.c:3313:                            "zip() argument %zd is shorter than argument%s%zd",
./Python/bltinmodule.c:3323:                                "zip() argument %zd is longer than argument%s%zd",
./Python/bltinmodule.c:3369:"zip(*iterables, strict=False)\n\
./Python/bltinmodule.c:3385:    "zip",                              /* tp_name */
./Python/bltinmodule.c:3617:    SETBUILTIN("zip",                   &PyZip_Type);
...                 

Looks promising!

This looks like a good start point — defining it along with the other builtins inside _PyBuiltin_Init in Python/bltinmodule.c:

PyObject *
_PyBuiltin_Init(PyInterpreterState *interp)
{
...
    SETBUILTIN("zip",                   &PyZip_Type);

where _PyBuiltin_Init gets called in Python/pylifecycle.c here:

static PyStatus
pycore_init_builtins(PyThreadState *tstate)
{
    PyInterpreterState *interp = tstate->interp;

    PyObject *bimod = _PyBuiltin_Init(interp);

where pycore_init_builtins called in Python/pylifecycle.c here:

static PyStatus
pycore_interp_init(PyThreadState *tstate)
{
...
    status = pycore_init_builtins(tstate);

where pycore_interp_init gets called in Python/pylifecycle.c here:

static PyStatus
new_interpreter(PyThreadState **tstate_p,
                const PyInterpreterConfig *config, long whence)
{
...
    status = pycore_interp_init(tstate);
    if (_PyStatus_EXCEPTION(status)) {
        goto error;
    }

where new_interpreter gets called in Python/pylifecycle.c here:

PyStatus
Py_NewInterpreterFromConfig(PyThreadState **tstate_p,
                            const PyInterpreterConfig *config)
{
    long whence = _PyInterpreterState_WHENCE_CAPI;
    return new_interpreter(tstate_p, config, whence);
}

Okay, getting back to “zip”, here’s where PyZip_Type is defined in Python/bltinmodule.c:

PyTypeObject PyZip_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "zip",                              /* tp_name */
    sizeof(zipobject),                  /* tp_basicsize */
    0,                                  /* tp_itemsize */
    /* methods */
    zip_dealloc,                        /* tp_dealloc */
    0,                                  /* tp_vectorcall_offset */
    0,                                  /* tp_getattr */
    0,                                  /* tp_setattr */
    0,                                  /* tp_as_async */
    0,                                  /* tp_repr */
    0,                                  /* tp_as_number */
    0,                                  /* tp_as_sequence */
    0,                                  /* tp_as_mapping */
    0,                                  /* tp_hash */
    0,                                  /* tp_call */
    0,                                  /* tp_str */
    PyObject_GenericGetAttr,            /* tp_getattro */
    0,                                  /* tp_setattro */
    0,                                  /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
        Py_TPFLAGS_BASETYPE,            /* tp_flags */
    zip_doc,                            /* tp_doc */
    zip_traverse,                       /* tp_traverse */
    0,                                  /* tp_clear */
    0,                                  /* tp_richcompare */
    0,                                  /* tp_weaklistoffset */
    PyObject_SelfIter,                  /* tp_iter */
    zip_next,                           /* tp_iternext */
    zip_methods,                        /* tp_methods */
    0,                                  /* tp_members */
    0,                                  /* tp_getset */
    0,                                  /* tp_base */
    0,                                  /* tp_dict */
    0,                                  /* tp_descr_get */
    0,                                  /* tp_descr_set */
    0,                                  /* tp_dictoffset */
    0,                                  /* tp_init */
    PyType_GenericAlloc,                /* tp_alloc */
    zip_new,                            /* tp_new */
    PyObject_GC_Del,                    /* tp_free */
};

https://docs.python.org/3/c-api/typeobj.html discusses PyTypeObject:

Perhaps one of the most important structures of the Python object system is the structure that defines a new type: the PyTypeObject structure. Type objects can be handled using any of the PyObject_* or PyType_* functions, but do not offer much that’s interesting to most Python applications. These objects are fundamental to how objects behave, so they are very important to the interpreter itself and to any extension module that implements new types.

Type objects are fairly large compared to most of the standard types. The reason for the size is that each type object stores a large number of values, mostly C function pointers, each of which implements a small part of the type’s functionality. The fields of the type object are examined in detail in this section. The fields will be described in the order in which they occur in the structure.

Type Object Structures

Let’s try to change something and see if we can see the effects. Add a smiley to the doc string for zip:

PyDoc_STRVAR(zip_doc,
"zip(*iterables, strict=False)\n\
--\n\
\n\
:-) The zip object yields n-length tuples, where n is the number of\n\
iterables passed as positional arguments to zip().  The i-th element\n\
in every tuple comes from the i-th iterable argument to zip().  This\n\
continues until the shortest argument is exhausted.\n\
\n\
If strict is true and one of the arguments is exhausted before the\n\
others, raise a ValueError.\n\
\n\
   >>> list(zip('abcdefg', range(3), range(4)))\n\
   [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]");

Configure and build:

% ./configure
% make

Hm, did that actually yield an executable?

 % find . -type f -perm +111 -print
... lots of stuff ...
./python.exe
... lots of stuff ...

./python.exe sounds promising. Let’s try it:

 % ./python.exe
Python 3.16.0a0 (heads/main:763b6edb0ec, Oct  2 2026, 10:02:45) [Clang 17.0.0 (clang-1700.6.3.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.

>>> help(zip)
Help on class zip in module builtins:

class zip(object)
 |  zip(*iterables, strict=False)
 |
 |  :-) The zip object yields n-length tuples...

Woo! Clean up:

% git checkout Python/bltinmodule.c

https://docs.python.org/3/c-api/typeobj.html#examples notes that there’s a newer, more compact syntax for populating PyTypeObject, using designated initializers. What if we update zip to use this? Just for fun, let’s give it a try:

import re

# pzip_type is a multiline string with the code
>>> fields = re.findall(r"\s*(\S.*?),[ \t]+/\* ([a-z_]+) \*/", pzip_type, re.DOTALL)

>>> fields
[('"zip"', 'tp_name'), ('sizeof(zipobject)', 'tp_basicsize'), ('0', 'tp_itemsize'), ('/* methods */\n    zip_dealloc', 'tp_dealloc'), ('0', 'tp_vectorcall_offset'), ('0', 'tp_getattr'), ('0', 'tp_setattr'), ('0', 'tp_as_async'), ('0', 'tp_repr'), ('0', 'tp_as_number'), ('0', 'tp_as_sequence'), ('0', 'tp_as_mapping'), ('0', 'tp_hash'), ('0', 'tp_call'), ('0', 'tp_str'), ('PyObject_GenericGetAttr', 'tp_getattro'), ('0', 'tp_setattro'), ('0', 'tp_as_buffer'), ('Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |\n        Py_TPFLAGS_BASETYPE', 'tp_flags'), ('zip_doc', 'tp_doc'), ('zip_traverse', 'tp_traverse'), ('0', 'tp_clear'), ('0', 'tp_richcompare'), ('0', 'tp_weaklistoffset'), ('PyObject_SelfIter', 'tp_iter'), ('zip_next', 'tp_iternext'), ('zip_methods', 'tp_methods'), ('0', 'tp_members'), ('0', 'tp_getset'), ('0', 'tp_base'), ('0', 'tp_dict'), ('0', 'tp_descr_get'), ('0', 'tp_descr_set'), ('0', 'tp_dictoffset'), ('0', 'tp_init'), ('PyType_GenericAlloc', 'tp_alloc'), ('zip_new', 'tp_new'), ('PyObject_GC_Del', 'tp_free')]

def clean_value(s):
  return re.sub(r"/\*.*\*/", "", s).strip()

>>> print(",\n".join(f".{name} = {clean_value(value)}" for value, name in fields if clean_value(value) != "0"))

.tp_name = "zip",
.tp_basicsize = sizeof(zipobject),
.tp_dealloc = zip_dealloc,
.tp_getattro = PyObject_GenericGetAttr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
        Py_TPFLAGS_BASETYPE,
.tp_doc = zip_doc,
.tp_traverse = zip_traverse,
.tp_iter = PyObject_SelfIter,
.tp_iternext = zip_next,
.tp_methods = zip_methods,
.tp_alloc = PyType_GenericAlloc,
.tp_new = zip_new,
.tp_free = PyObject_GC_Del

Does it work?

% make
% ./python.exe
>>> list(zip(range(10), range(3)))
[(0, 0), (1, 1), (2, 2)]
>>> list(zip(range(10), range(3), strict=True))
Traceback (most recent call last):
  File "<python-input-5>", line 1, in <module>
    list(zip(range(10), range(3), strict=True))
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: zip() argument 2 is shorter than argument 1

Seems like it… worth submitting a PR for this? Wonder why they haven’t done it before. For now, let’s just clean up and call it a day:

% git checkout Python/bltinmodule.c

Also, one thing I’m curious about… does zip support infinitely long iterables?

def forever(value):
  while True:
    yield value

limit = 10
for x, y in zip(forever("hi"), forever("bye")):
  print(x, y)
  limit -= 1
  if limit <= 0:
    break

hi bye
hi bye
hi bye
hi bye
hi bye
hi bye
hi bye
hi bye
hi bye
hi bye

Fun 🙂


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *