Repository navigation
Description
A subclass doesn't inherit its base class's __init__
A pyscript class that subclasses another pyscript class doesn't get the base's __init__. Without an __init__ of its own, the base's never runs: silently if the subclass is created with no arguments, with a TypeError if it's given any. A subclass's own __init__ can't call the base's either, except through pyscript's internal name for it. The author of #890 ran into this while writing its test, and wrapped a manager instead of subclassing it. I've run into the same issue when trying to use subclassing in pyscript in Home Assistant for a device monitoring class, and had to switch that design to composition where inheritance would have been a better fit.
Seen on pyscript 2.1.0 with Home Assistant 2026.9.2. The code involved is unchanged in 2.1.1 and on master.
To reproduce
class Base:
def __init__(self, name="default"):
self.name = name
class NoInitSub(Base):
pass
class SuperSub(Base):
def __init__(self, name):
super(SuperSub, self).__init__(name)
class ClassCallSub(Base):
def __init__(self, name):
Base.__init__(self, name)| Call | pyscript | Python |
|---|---|---|
NoInitSub() |
created, but it has no name: Base.__init__ never ran |
name is "default" |
NoInitSub("given") |
TypeError: NoInitSub() takes no arguments |
name is "given" |
SuperSub("given") |
TypeError: SuperSub.__init__() takes exactly one argument (the instance to initialize) |
name is "given" |
ClassCallSub("given") |
the same TypeError |
name is "given" |
What works today is the internal name: super(SuperSub, self).__init__evalfunc_wrap__(name) or Base.__init__evalfunc_wrap__(self, name).
Cause
pyscript methods are async, so Python can't call __init__ itself. ast_classdef moves each class's __init__ to __init__evalfunc_wrap__, and call_func creates the instance and then awaits that method (eval.py#L1087-L1090, #L1898-L1906). Two things then break inheritance:
- Every class without its own
__init__gets__init__evalfunc_wrap__ = None(line 1087). ThatNonehides the base's, socall_funccreates aNoInitSubwith a plainfunc(*args, **kwargs), which only reachesobject's own constructor. __init__itself is deleted from the class, sosuper(...).__init__andBase.__init__both findobject.__init__.
This is separate from the limitation discussed in #628, which is about methods Python calls by itself (__str__, __eq__, operators). When pyscript code creates an object, pyscript already calls __init__ itself, so async isn't in the way here. Only the lookup is.
Fix 1: delete line 1087
sym_table["__init__evalfunc_wrap__"] = None # delete this line
if "__init__" in sym_table:
sym_table["__init__evalfunc_wrap__"] = sym_table["__init__"]
del sym_table["__init__"]A subclass without its own __init__ then finds its base's __init__evalfunc_wrap__ through normal inheritance, and call_func awaits it as it does for any class with an __init__. A class with no __init__ anywhere in its chain fails call_func's hasattr test and takes the generic call, func(*args, **kwargs). That's the same call it gets today when has_init_wrapper is false. This fixes NoInitSub, both the silent case and the TypeError.
Why the line can go
It's left over from code that has since been removed. When class support was added in 4333d31 (2020-09), there was no None, and a subclass without an __init__ did find its base's. 8dbde8b (2020-10) added a loop in call_func that rebound each method of every new pyscript instance, so every pyscript class had to take that branch. __init__evalfunc_wrap__ doubled as the "pyscript class" marker, and classes without an __init__ got None to pass the hasattr test. That None is what started hiding the base's __init__. 0a12c60 (2025-12) moved method binding to a descriptor, EvalFuncVar.__get__, and dropped the loop, so nothing uses the marker now.
The one case it changes
If a native __init__ comes before the pyscript one in the class order, as in class C(NativeMixin, PyBase) or with a @pyscript_compile class in between, Python runs the native one, and so does pyscript today: the None sends C down the generic call. With fix 1, call_func finds PyBase's __init__evalfunc_wrap__ instead, creates the object with func() (running the native __init__ with no arguments), and then awaits PyBase's. Fix 2 doesn't have this problem, because it takes the first __init__ in the class order, of either kind.
Fix 2 (an option): keep __init__ under its own name
This also fixes SuperSub and ClassCallSub, so subclassing looks like Python. The sketch below is untested.
In ast_classdef, stop deleting __init__, and keep the old name as an alias for code that already uses it as a workaround:
if "__init__" in sym_table:
sym_table["__init__evalfunc_wrap__"] = sym_table["__init__"]In call_func, replace the __init__evalfunc_wrap__ block. Create the instance without Python calling the async __init__, then await the __init__ that inheritance finds:
if inspect.isclass(func):
init = None
for klass in func.__mro__:
if "__init__" in klass.__dict__:
init = klass.__dict__["__init__"]
break
if isinstance(init, EvalFuncVar):
inst = func.__new__(func)
await init.__get__(inst, func).call(self, *args, **kwargs)
return instA class whose __init__ is native (none of pyscript's in its chain) falls through to the generic call, as now. super(Sub, self).__init__(...) and Base.__init__(self, ...) then find the base's method like any other method.
Behavior changes to weigh. Both cases are probably rare:
- Native code that creates a pyscript class itself, such as
defaultdict(MyClass), would fail loudly ("__init__()should return None, not 'coroutine'"). Today the__init__is silently skipped. - A pyscript class with a native base class no longer gets that base's
__init__run automatically with no arguments before its own. As in Python, its__init__would call it. A metaclass with its own__call__would also be bypassed.
Either fix leaves zero-argument super() unsupported ("super(): __class__ cell not found"), along with @property, @classmethod and the dunders Python calls by itself.
A test could compare against CPython the way #890's test_with_matches_python does.
Activity