[ty] Forbid use of super() in NamedTuple subclasses (#21700)
Some checks are pending
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

The exact behavior around what's allowed vs. disallowed was partly
detected through trial and error in the runtime.

I was a little confused by [this
comment](https://github.com/python/cpython/pull/129352) that says
"`NamedTuple` subclasses cannot be inherited from" because in practice
that doesn't appear to error at runtime.

Closes [#1683](https://github.com/astral-sh/ty/issues/1683).
This commit is contained in:
Charlie Marsh 2025-11-30 10:49:06 -05:00 committed by GitHub
parent b02e8212c9
commit e7beb7e1f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 251 additions and 74 deletions

View file

@ -408,3 +408,77 @@ class Vec2(NamedTuple):
Vec2(0.0, 0.0)
```
## `super()` is not supported in NamedTuple methods
Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime. In Python
3.14+, a `TypeError` is raised; in earlier versions, a confusing `RuntimeError` about
`__classcell__` is raised.
```py
from typing import NamedTuple
class F(NamedTuple):
x: int
def method(self):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super()
def method_with_args(self):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super(F, self)
def method_with_different_pivot(self):
# Even passing a different pivot class fails.
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super(tuple, self)
@classmethod
def class_method(cls):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super()
@staticmethod
def static_method():
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
super()
@property
def prop(self):
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
return super()
```
However, classes that **inherit from** a `NamedTuple` class (but don't directly inherit from
`NamedTuple`) can use `super()` normally:
```py
from typing import NamedTuple
class Base(NamedTuple):
x: int
class Child(Base):
def method(self):
super()
```
And regular classes that don't inherit from `NamedTuple` at all can use `super()` as normal:
```py
class Regular:
def method(self):
super() # fine
```
Using `super()` on a `NamedTuple` class also works fine if it occurs outside the class:
```py
from typing import NamedTuple
class F(NamedTuple):
x: int
super(F, F(42)) # fine
```

View file

@ -19,7 +19,7 @@ use crate::semantic_index::{
use crate::types::bound_super::BoundSuperError;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE;
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
use crate::types::enums::enum_metadata;
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{
@ -5546,6 +5546,20 @@ impl KnownClass {
return;
};
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
if let Some(builder) = context
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
{
builder.into_diagnostic(format_args!(
"Cannot use `super()` in a method of NamedTuple class `{}`",
enclosing_class.name(db)
));
}
overload.set_return_type(Type::unknown());
return;
}
// The type of the first parameter if the given scope is function-like (i.e. function or lambda).
// `None` if the scope is not function-like, or has no parameters.
let first_param = match scope.node(db) {
@ -5585,6 +5599,22 @@ impl KnownClass {
overload.set_return_type(bound_super);
}
[Some(pivot_class_type), Some(owner_type)] => {
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) {
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
if let Some(builder) = context
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
{
builder.into_diagnostic(format_args!(
"Cannot use `super()` in a method of NamedTuple class `{}`",
enclosing_class.name(db)
));
}
overload.set_return_type(Type::unknown());
return;
}
}
let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type)
.unwrap_or_else(|err| {
err.report_diagnostic(context, call_expression.into());

View file

@ -121,6 +121,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&MISSING_TYPED_DICT_KEY);
registry.register_lint(&INVALID_METHOD_OVERRIDE);
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@ -1760,6 +1761,33 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `super()` inside methods of `NamedTuple` classes.
///
/// ## Why is this bad?
/// Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime.
///
/// ## Examples
/// ```python
/// from typing import NamedTuple
///
/// class F(NamedTuple):
/// x: int
///
/// def method(self):
/// super() # error: super() is not supported in methods of NamedTuple classes
/// ```
///
/// ## References
/// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
pub(crate) static SUPER_CALL_IN_NAMED_TUPLE_METHOD = {
summary: "detects `super()` calls in methods of `NamedTuple` classes",
status: LintStatus::preview("0.0.1-alpha.30"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.