Skip to content

Calls to the type object of a native class use the slow tuple and dict calling convention #1251

Closed
python/mypy
#22197

Description

@rheard

Creating an instance of a native class is only fast when compiled code calls the class by name (Point(1, 2)). That is compiled to a call of the native constructor. Every other call is a call to the type object:

  • a call from interpreted code,
  • cls(...) in a class method, type(self)(...) or self.__class__(...) in compiled code,
  • a call through a variable that holds the class (type[Point], a callback, Any).

These go through type.__call__. CPython packs the arguments into a tuple (and a dict, if there are keyword arguments) and calls tp_new, which passes them to the wrapper function of __init__. That wrapper is a legacy one that parses the tuple and the dict with CPyArg_ParseTupleAndKeywords. Functions and other methods got vectorcall wrappers in python/mypy#9894, but __init__ was left out. There's still a TODO for it in is_fastcall_supported() (mypyc/codegen/emitmodule.py):

        # TODO: Support fastcall for __init__ and __new__.
        return fn.name != "__init__" and fn.name != "__new__"

Example

# point.py
class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __add__(self, other: "Point") -> "Point":
        # type(self), so that a subclass gets an instance of itself back
        return type(self)(self.x + other.x, self.y + other.y)

    @classmethod
    def origin(cls) -> "Point":
        return cls(0, 0)


def make_point(x: int, y: int) -> Point:
    return Point(x, y)

Timings from `python -m timeit -s "from point import Point, make_point; p = Point(1, 2)" "":

statement compiled interpreted
make_point(1, 2) 64 ns 194 ns
Point(1, 2) 103 ns 169 ns
Point(1, y=2) 155 ns 253 ns
Point(x=1, y=2) 172 ns 262 ns
p + p 111 ns 270 ns
Point.origin() 164 ns 241 ns

make_point(1, 2) creates the same object through a compiled function, which has a vectorcall wrapper
and calls the native constructor. So about 40 ns of Point(1, 2) is the calling convention, and 90 to
110 ns when there are keyword arguments. p + p and Point.origin() only run compiled code, but they
pay the same for the calls of type(self) and cls.

Activity

  1. added a commit that references this issue on Oct 15, 2026
    318bc57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions