Skip to content

Commit e7beb7e

Browse files
[ty] Forbid use of super() in NamedTuple subclasses (#21700)
## 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](python/cpython#129352) that says "`NamedTuple` subclasses cannot be inherited from" because in practice that doesn't appear to error at runtime. Closes [#1683](astral-sh/ty#1683).
1 parent b02e821 commit e7beb7e

File tree

6 files changed

+251
-74
lines changed

6 files changed

+251
-74
lines changed

crates/ty/docs/rules.md

Lines changed: 107 additions & 73 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,77 @@ class Vec2(NamedTuple):
408408

409409
Vec2(0.0, 0.0)
410410
```
411+
412+
## `super()` is not supported in NamedTuple methods
413+
414+
Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime. In Python
415+
3.14+, a `TypeError` is raised; in earlier versions, a confusing `RuntimeError` about
416+
`__classcell__` is raised.
417+
418+
```py
419+
from typing import NamedTuple
420+
421+
class F(NamedTuple):
422+
x: int
423+
424+
def method(self):
425+
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
426+
super()
427+
428+
def method_with_args(self):
429+
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
430+
super(F, self)
431+
432+
def method_with_different_pivot(self):
433+
# Even passing a different pivot class fails.
434+
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
435+
super(tuple, self)
436+
437+
@classmethod
438+
def class_method(cls):
439+
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
440+
super()
441+
442+
@staticmethod
443+
def static_method():
444+
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
445+
super()
446+
447+
@property
448+
def prop(self):
449+
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
450+
return super()
451+
```
452+
453+
However, classes that **inherit from** a `NamedTuple` class (but don't directly inherit from
454+
`NamedTuple`) can use `super()` normally:
455+
456+
```py
457+
from typing import NamedTuple
458+
459+
class Base(NamedTuple):
460+
x: int
461+
462+
class Child(Base):
463+
def method(self):
464+
super()
465+
```
466+
467+
And regular classes that don't inherit from `NamedTuple` at all can use `super()` as normal:
468+
469+
```py
470+
class Regular:
471+
def method(self):
472+
super() # fine
473+
```
474+
475+
Using `super()` on a `NamedTuple` class also works fine if it occurs outside the class:
476+
477+
```py
478+
from typing import NamedTuple
479+
480+
class F(NamedTuple):
481+
x: int
482+
483+
super(F, F(42)) # fine
484+
```

crates/ty_python_semantic/src/types/class.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::semantic_index::{
1919
use crate::types::bound_super::BoundSuperError;
2020
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
2121
use crate::types::context::InferContext;
22-
use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE;
22+
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
2323
use crate::types::enums::enum_metadata;
2424
use crate::types::function::{DataclassTransformerParams, KnownFunction};
2525
use crate::types::generics::{
@@ -5546,6 +5546,20 @@ impl KnownClass {
55465546
return;
55475547
};
55485548

5549+
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
5550+
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
5551+
if let Some(builder) = context
5552+
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
5553+
{
5554+
builder.into_diagnostic(format_args!(
5555+
"Cannot use `super()` in a method of NamedTuple class `{}`",
5556+
enclosing_class.name(db)
5557+
));
5558+
}
5559+
overload.set_return_type(Type::unknown());
5560+
return;
5561+
}
5562+
55495563
// The type of the first parameter if the given scope is function-like (i.e. function or lambda).
55505564
// `None` if the scope is not function-like, or has no parameters.
55515565
let first_param = match scope.node(db) {
@@ -5585,6 +5599,22 @@ impl KnownClass {
55855599
overload.set_return_type(bound_super);
55865600
}
55875601
[Some(pivot_class_type), Some(owner_type)] => {
5602+
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
5603+
if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) {
5604+
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
5605+
if let Some(builder) = context
5606+
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
5607+
{
5608+
builder.into_diagnostic(format_args!(
5609+
"Cannot use `super()` in a method of NamedTuple class `{}`",
5610+
enclosing_class.name(db)
5611+
));
5612+
}
5613+
overload.set_return_type(Type::unknown());
5614+
return;
5615+
}
5616+
}
5617+
55885618
let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type)
55895619
.unwrap_or_else(|err| {
55905620
err.report_diagnostic(context, call_expression.into());

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
121121
registry.register_lint(&MISSING_TYPED_DICT_KEY);
122122
registry.register_lint(&INVALID_METHOD_OVERRIDE);
123123
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
124+
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
124125

125126
// String annotations
126127
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -1760,6 +1761,33 @@ declare_lint! {
17601761
}
17611762
}
17621763

1764+
declare_lint! {
1765+
/// ## What it does
1766+
/// Checks for calls to `super()` inside methods of `NamedTuple` classes.
1767+
///
1768+
/// ## Why is this bad?
1769+
/// Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime.
1770+
///
1771+
/// ## Examples
1772+
/// ```python
1773+
/// from typing import NamedTuple
1774+
///
1775+
/// class F(NamedTuple):
1776+
/// x: int
1777+
///
1778+
/// def method(self):
1779+
/// super() # error: super() is not supported in methods of NamedTuple classes
1780+
/// ```
1781+
///
1782+
/// ## References
1783+
/// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
1784+
pub(crate) static SUPER_CALL_IN_NAMED_TUPLE_METHOD = {
1785+
summary: "detects `super()` calls in methods of `NamedTuple` classes",
1786+
status: LintStatus::preview("0.0.1-alpha.30"),
1787+
default_level: Level::Error,
1788+
}
1789+
}
1790+
17631791
declare_lint! {
17641792
/// ## What it does
17651793
/// Checks for calls to `reveal_type` without importing it.

crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Settings: Settings {
9393
"redundant-cast": Warning (Default),
9494
"static-assert-error": Error (Default),
9595
"subclass-of-final-class": Error (Default),
96+
"super-call-in-named-tuple-method": Error (Default),
9697
"too-many-positional-arguments": Error (Default),
9798
"type-assertion-failure": Error (Default),
9899
"unavailable-implicit-super-arguments": Error (Default),

ty.schema.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)