Skip to content

fix(py_class): support super().__init__() in init=False subclasses#532

Merged
junrushao merged 3 commits intoapache:mainfrom
junrushao:junrus/2026-04-10/fix-pyclass-init
Apr 10, 2026
Merged

fix(py_class): support super().__init__() in init=False subclasses#532
junrushao merged 3 commits intoapache:mainfrom
junrushao:junrus/2026-04-10/fix-pyclass-init

Conversation

@junrushao
Copy link
Copy Markdown
Member

Summary

  • Fix segfault when @py_class(init=False) subclasses use super().__init__() followed by field assignment
  • Add ffi.NewEmpty C++ function to allocate zero-initialized objects by type index
  • Detect super-init-from-subclass pattern in _make_init() and pre-allocate instead of dispatching to __ffi_init__

Motivation

The common Python pattern of defining a custom __init__ that calls super().__init__() then sets fields crashes when used with @py_class(init=False):

@py_class(init=False)
class PointerType(Node):
    element_type: Object

    def __init__(self, element_type):
        super().__init__()       # parent's auto-init dispatches to child's C++ ctor → crash
        self.element_type = element_type

The parent's auto-generated __init__ forwards to __ffi_init__ using the child type's C++ constructor, which expects field arguments that were not provided.

Design

C++ side (src/ffi/extra/dataclass.cc): Register ffi.NewEmpty(type_index) -> ObjectRef that allocates a zero-initialized object via CreateEmptyObject. The calloc'd state is valid: None for Any/ObjectRef fields, 0 for scalars.

Python side (python/tvm_ffi/registry.py):

  • _ffi_alloc_empty(obj): Calls ffi.NewEmpty to allocate an empty FFI object for type(obj) and moves the handle into obj. No-op if already allocated.
  • _is_super_init_from_subclass(self): Compares type(self).__tvm_ffi_type_info__ identity against the declaring class's type_info. Returns True only for registered @py_class subclasses — undecorated subclasses inherit the parent's type_info so the identity check correctly filters them out.
  • Both _make_init code paths (with/without __post_init__) intercept the no-args super-init-from-subclass call and allocate an empty object instead of dispatching to __ffi_init__.

Test plan

  • uv run pytest -vvs tests/python/test_dataclass_py_class.py::TestSuperInitPattern — 12 tests pass
  • uv run pytest -vvs tests/python/ — 1970 passed, 38 skipped, 3 xfailed, 0 failures

Tests cover: basic super-init, deep hierarchy (Node→BaseType→PointerType), calloc defaults, intermediate custom/auto inits, normal init unaffected, non-py_class subclass error handling, isinstance checks, field overwrite, copy/deepcopy, and direct-from-Object inheritance.

🤖 Generated with Claude Code

When a `@py_class(init=False)` subclass defines a custom `__init__`
that calls `super().__init__()` followed by field assignment, the
parent's auto-generated `__init__` would forward to `__ffi_init__`
with the child type's C++ constructor, which requires field arguments
that were not provided, causing a segfault.

The fix adds a `ffi.NewEmpty` C++ global function that allocates a
zero-initialized (calloc'd) object by type index via
`CreateEmptyObject`. On the Python side, each auto-generated
`__init__` now detects the super().__init__() call pattern --
no args/kwargs and `type(self)` differs from the declaring class --
and allocates an empty object of the child's type instead of
dispatching to `__ffi_init__`. The `_install_init` wrapper for
`init=False` classes with user-defined `__init__` pre-allocates the
empty object before calling the user's `__init__`, ensuring field
setters operate on a valid C++ backing object.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enables support for the super().__init__() pattern in @py_class(init=False) subclasses by introducing a mechanism to pre-allocate empty, zero-initialized FFI objects. This prevents crashes that previously occurred when calling parent constructors without field arguments. The implementation includes a new C++ helper ffi.NewEmpty and updates to the Python registry to handle object allocation during initialization. I have provided a suggestion to use functools.wraps in the _install_init wrapper to better preserve metadata during function wrapping.

Comment thread python/tvm_ffi/registry.py
- Ruff: move closing docstring quote to its own line
- Ty: add `type: ignore[missing-argument]` for intentional
  no-args calls that are intercepted at runtime
- Replace functools.cache (3.9+) with functools.lru_cache(maxsize=None)
- Use functools.wraps for the _install_init wrapper to properly
  preserve __doc__, __annotations__, and other metadata
@junrushao junrushao merged commit 2b59825 into apache:main Apr 10, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants