Python 3.8: Fixing super() RuntimeError in typing.NamedTuple Subclass (Worked in 3.6)
Python’s typing.NamedTuple is a powerful tool for creating simple, immutable data classes with named fields. It combines the readability of dictionaries with the performance of tuples, making it a favorite for data modeling. However, upgrading Python versions can sometimes break previously working code. A common issue encountered when migrating from Python 3.6 to 3.8 is a RuntimeError when using super() in a subclass of typing.NamedTuple.
This blog post dives deep into why this error occurs, how Python 3.6 and 3.8 differ in handling NamedTuple subclasses, and provides step-by-step solutions to fix the issue. Whether you’re maintaining legacy code or upgrading to a newer Python version, this guide will help you resolve the super() RuntimeError once and for all.
Table of Contents#
- Understanding the Problem
- Why Did It Work in Python 3.6?
- The Root Cause in Python 3.8
- How to Fix the RuntimeError
- Alternative Approaches
- Verification and Testing
- Conclusion
- References
Understanding the Problem#
Let’s start with a concrete example. Suppose you have a base NamedTuple class and a subclass that overrides __init__ to add custom logic. This code works flawlessly in Python 3.6 but throws a RuntimeError in Python 3.8.
Example Code (Fails in Python 3.8)#
from typing import NamedTuple
class Base(NamedTuple):
x: int
y: int
class Sub(Base):
z: int # Additional field in subclass
def __init__(self, x: int, y: int, z: int):
# Call parent __init__ to initialize x and y
super().__init__(x, y) # This line causes the error in Python 3.8
self.z = z # Initialize subclass-specific field
# Create an instance (works in 3.6, fails in 3.8)
obj = Sub(x=1, y=2, z=3)Error Message in Python 3.8#
RuntimeError: super(): __class__ cell not found
This error occurs when the subclass Sub calls super().__init__(x, y). The mystery is: why did this work in Python 3.6 but break in 3.8? To answer that, we need to explore how typing.NamedTuple behaves across Python versions.
Why Did It Work in Python 3.6?#
In Python 3.6, typing.NamedTuple relied on a metaclass (_NamedTupleMeta) to generate classes. This metaclass dynamically created a standard __init__ method that initialized the named fields. For example, the Base class above would have an __init__ method similar to:
def __init__(self, x: int, y: int):
self.x = x
self.y = yWhen you subclassed Base and defined your own __init__, Python’s normal method resolution order (MRO) applied. The super() call in Sub.__init__ would resolve to Base.__init__, as expected. Critically, the __init__ method generated by _NamedTupleMeta in Python 3.6 included the __class__ closure cell—a hidden reference to the class—allowing super() to work without explicit arguments.
The Root Cause in Python 3.8#
Python 3.8 introduced optimizations and changes to typing.NamedTuple (partly to align with PEP 585 and improve performance). One key change was how the __init__ method is generated. Instead of using a dynamic metaclass to create a "regular" __init__, Python 3.8+ uses a more efficient, slot-based approach for NamedTuple subclasses.
The class Cell Problem#
The RuntimeError: __class__ cell not found arises because super() without arguments relies on a __class__ cell in the method’s closure. This cell is automatically created by Python’s compiler when a method is defined inside a class, pointing to the class itself. However, in Python 3.8+, the __init__ method of typing.NamedTuple subclasses is generated without this __class__ cell.
When you define Sub.__init__ and call super(), Python expects the __class__ cell to determine the current class (Sub) and resolve the parent class via MRO. Without this cell, super() cannot infer the current class, leading to the error.
How to Fix the RuntimeError#
The solution hinges on helping super() identify the current class and instance explicitly. Here are two reliable fixes:
Fix 1: Explicitly Pass Class and Instance to super()#
Instead of using super() with no arguments, pass the current class (Sub) and self explicitly:
from typing import NamedTuple
class Base(NamedTuple):
x: int
y: int
class Sub(Base):
z: int
def __init__(self, x: int, y: int, z: int):
# Explicitly pass Sub and self to super()
super(Sub, self).__init__(x, y) # No more __class__ cell dependency
self.z = z
# Now works in Python 3.8+
obj = Sub(x=1, y=2, z=3)
print(obj) # Output: Sub(x=1, y=2, z=3)Why This Works: By passing Sub (the current class) and self (the instance) to super(), we bypass the need for the __class__ cell. super(Sub, self) explicitly tells Python to start the MRO lookup from Sub’s parent class (Base), ensuring Base.__init__ is called.
Fix 2: Avoid Overriding init (If Possible)#
If your subclass only adds new fields (and no custom initialization logic), you can avoid overriding __init__ entirely. typing.NamedTuple automatically generates an __init__ that includes all fields from the subclass and its parents:
from typing import NamedTuple
class Base(NamedTuple):
x: int
y: int
class Sub(Base):
z: int # No __init__ override needed!
# Python 3.8+ auto-generates __init__(x, y, z)
obj = Sub(x=1, y=2, z=3)
print(obj) # Output: Sub(x=1, y=2, z=3)When to Use This: This is ideal if you don’t need custom logic in __init__ (e.g., validation, derived fields). It’s cleaner and avoids the super() issue entirely.
Alternative Approaches#
If you need custom initialization logic, consider these alternatives to typing.NamedTuple:
Alternative 1: Use dataclasses (Python 3.7+)#
dataclasses (introduced in Python 3.7) are more flexible than NamedTuple and natively support subclassing with super(). They also allow mutable instances (unless frozen) and custom __init__ logic via __post_init__.
from dataclasses import dataclass
@dataclass
class Base:
x: int
y: int
@dataclass
class Sub(Base):
z: int
def __post_init__(self):
# Custom logic runs after __init__
self.z_squared = self.z ** 2 # Example: Derived field
obj = Sub(x=1, y=2, z=3)
print(obj) # Output: Sub(x=1, y=2, z=3, z_squared=9)Advantages:
- No
super()issues (MRO works as expected). - Built-in support for
__post_init__(runs after__init__for custom logic). - More features (mutability, default values, validation via
__post_init__).
Alternative 2: collections.namedtuple (Legacy)#
collections.namedtuple (predecessor to typing.NamedTuple) has different behavior but may work in some cases. However, it’s less type-safe and lacks modern features like type hints for fields. Use this only if you need compatibility with very old Python versions.
from collections import namedtuple
Base = namedtuple("Base", ["x", "y"])
class Sub(Base):
def __init__(self, x, y, z):
super().__init__(x, y)
self.z = z
obj = Sub(1, 2, 3)
print(obj) # Output: (1, 2) (Note: z is not in the tuple repr!)Caveat: collections.namedtuple subclasses are still tuples, so added fields like z won’t appear in __repr__ or __eq__ checks.
Verification and Testing#
After applying a fix, verify it works in Python 3.8+ with these steps:
- Instantiate the Subclass: Ensure no
RuntimeErroris raised. - Check Field Values: Confirm all fields (parent and subclass) are initialized correctly.
- Test Custom Logic: If using
__init__or__post_init__, validate that logic runs (e.g., derived fields, validation).
Example Test Code:
# For Fix 1 (explicit super())
obj = Sub(x=1, y=2, z=3)
assert obj.x == 1 and obj.y == 2 and obj.z == 3, "Fields not initialized"
# For dataclasses with __post_init__
assert obj.z_squared == 9, "__post_init__ logic failed"Conclusion#
The super() RuntimeError in Python 3.8+ when subclassing typing.NamedTuple stems from changes in how __init__ methods are generated, which breaks the __class__ cell dependency of super() without arguments. To fix it:
- Use explicit
super(Sub, self)if overriding__init__intyping.NamedTuple. - Avoid
__init__overrides if subclassing only to add fields. - Switch to
dataclassesfor flexibility and native subclassing support.
By choosing the right approach for your use case, you can resolve the error and write maintainable code compatible with Python 3.8+.