[ty] Emit diagnostics for invalid base classes in type(...) (#22499)

## Summary

Tackles a few TODOs from https://github.com/astral-sh/ruff/pull/22291.
This commit is contained in:
Charlie Marsh
2026-01-14 08:56:04 -05:00
committed by GitHub
parent b24afb643c
commit e41f045ec5
11 changed files with 485 additions and 40 deletions

View File

@@ -908,12 +908,34 @@ Bad: type[Unrelated] = type("Bad", (Base,), {})
## Special base classes
Some special base classes work with dynamic class creation, but special semantics may not be fully
synthesized:
synthesized.
### Invalid special bases
Dynamic classes cannot directly inherit from `Generic`, `Protocol`, or `TypedDict`. These special
forms require class syntax for their semantics to be properly applied:
```py
from typing import Generic, Protocol, TypeVar
from typing_extensions import TypedDict
T = TypeVar("T")
# error: [invalid-base] "Invalid base for class created via `type()`"
GenericClass = type("GenericClass", (Generic[T],), {})
# error: [unsupported-dynamic-base] "Unsupported base for class created via `type()`"
ProtocolClass = type("ProtocolClass", (Protocol,), {})
# error: [invalid-base] "Invalid base for class created via `type()`"
TypedDictClass = type("TypedDictClass", (TypedDict,), {})
```
### Protocol bases
Inheriting from a class that is itself a protocol is valid:
```py
# Protocol bases work - the class is created as a subclass of the protocol
from typing import Protocol
from ty_extensions import reveal_mro
@@ -930,8 +952,9 @@ reveal_type(instance) # revealed: ProtoImpl
### TypedDict bases
Inheriting from a class that is itself a TypedDict is valid:
```py
# TypedDict bases work but TypedDict semantics aren't applied to dynamic subclasses
from typing_extensions import TypedDict
from ty_extensions import reveal_mro
@@ -964,26 +987,26 @@ reveal_mro(Point3D) # revealed: (<class 'Point3D'>, <class 'Point'>, <class 'tu
### Enum bases
Creating a class via `type()` that inherits from any Enum class fails at runtime because `EnumMeta`
expects special attributes in the class dict that `type()` doesn't provide:
```py
# Enum subclassing via type() is not supported - EnumMeta requires special dict handling
# that type() cannot provide. This applies even to empty enums.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
# Enums with members are final and cannot be subclassed
# error: [subclass-of-final-class]
ExtendedColor = type("ExtendedColor", (Color,), {})
class EmptyEnum(Enum):
pass
# TODO: We should emit a diagnostic here - type() cannot create Enum subclasses
ExtendedColor = type("ExtendedColor", (Color,), {})
reveal_type(ExtendedColor) # revealed: <class 'ExtendedColor'>
# Even empty enums fail - EnumMeta requires special dict handling
# TODO: We should emit a diagnostic here too
ValidExtension = type("ValidExtension", (EmptyEnum,), {})
reveal_type(ValidExtension) # revealed: <class 'ValidExtension'>
# Empty enums fail because EnumMeta requires special dict handling
# error: [invalid-base] "Invalid base for class created via `type()`"
InvalidExtension = type("InvalidExtension", (EmptyEnum,), {})
```
## `__init_subclass__` keyword arguments
@@ -1046,3 +1069,27 @@ reveal_type(Dynamic) # revealed: <class 'Dynamic'>
# Metaclass attributes are accessible on the class
reveal_type(Dynamic.custom_attr) # revealed: str
```
## `final()` on dynamic classes
Using `final()` as a function (not a decorator) on dynamic classes has no effect. The class is
passed through unchanged:
```py
from typing import final
# TODO: Add a diagnostic for ineffective use of `final()` here.
FinalClass = final(type("FinalClass", (), {}))
reveal_type(FinalClass) # revealed: <class 'FinalClass'>
# Subclassing is allowed because `final()` as a function has no effect
class Child(FinalClass): ...
# Same with base classes
class Base: ...
# TODO: Add a diagnostic for ineffective use of `final()` here.
FinalDerived = final(type("FinalDerived", (Base,), {}))
class Child2(FinalDerived): ...
```

View File

@@ -0,0 +1,78 @@
# Unsupported base for dynamic `type()` classes
<!-- snapshot-diagnostics -->
## `@final` class
Classes decorated with `@final` cannot be subclassed:
```py
from typing import final
@final
class FinalClass:
pass
X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
```
## `Generic` base
Dynamic classes created via `type()` cannot inherit from `Generic`:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
X = type("X", (Generic[T],), {}) # error: [invalid-base]
```
## `Protocol` base
Dynamic classes created via `type()` cannot inherit from `Protocol`:
```py
from typing import Protocol
X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
```
## `TypedDict` base
Dynamic classes created via `type()` cannot inherit from `TypedDict` directly. Use
`TypedDict("Name", ...)` instead:
```py
from typing_extensions import TypedDict
X = type("X", (TypedDict,), {}) # error: [invalid-base]
```
## Enum base
Dynamic classes created via `type()` cannot inherit from Enum classes because `EnumMeta` expects
special dict attributes that `type()` doesn't provide:
```py
from enum import Enum
class MyEnum(Enum):
pass
X = type("X", (MyEnum,), {}) # error: [invalid-base]
```
## Enum with members
Enums with members are final and cannot be subclassed at all:
```py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
X = type("X", (Color,), {}) # error: [subclass-of-final-class]
```

View File

@@ -0,0 +1,39 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - Enum base
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from enum import Enum
2 |
3 | class MyEnum(Enum):
4 | pass
5 |
6 | X = type("X", (MyEnum,), {}) # error: [invalid-base]
```
# Diagnostics
```
error[invalid-base]: Invalid base for class created via `type()`
--> src/mdtest_snippet.py:6:16
|
4 | pass
5 |
6 | X = type("X", (MyEnum,), {}) # error: [invalid-base]
| ^^^^^^ Has type `<class 'MyEnum'>`
|
info: Creating an enum class via `type()` is not supported
info: Consider using `Enum("X", [])` instead
info: rule `invalid-base` is enabled by default
```

View File

@@ -0,0 +1,38 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - Enum with members
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from enum import Enum
2 |
3 | class Color(Enum):
4 | RED = 1
5 | GREEN = 2
6 |
7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class]
```
# Diagnostics
```
error[subclass-of-final-class]: Class `X` cannot inherit from final class `Color`
--> src/mdtest_snippet.py:7:16
|
5 | GREEN = 2
6 |
7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class]
| ^^^^^
|
info: rule `subclass-of-final-class` is enabled by default
```

View File

@@ -0,0 +1,38 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `@final` class
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import final
2 |
3 | @final
4 | class FinalClass:
5 | pass
6 |
7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
```
# Diagnostics
```
error[subclass-of-final-class]: Class `X` cannot inherit from final class `FinalClass`
--> src/mdtest_snippet.py:7:16
|
5 | pass
6 |
7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
| ^^^^^^^^^^
|
info: rule `subclass-of-final-class` is enabled by default
```

View File

@@ -0,0 +1,38 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `Generic` base
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import Generic, TypeVar
2 |
3 | T = TypeVar("T")
4 |
5 | X = type("X", (Generic[T],), {}) # error: [invalid-base]
```
# Diagnostics
```
error[invalid-base]: Invalid base for class created via `type()`
--> src/mdtest_snippet.py:5:16
|
3 | T = TypeVar("T")
4 |
5 | X = type("X", (Generic[T],), {}) # error: [invalid-base]
| ^^^^^^^^^^ Has type `<special-form 'typing.Generic[T]'>`
|
info: Classes created via `type()` cannot be generic
info: Consider using `class X(Generic[...]): ...` instead
info: rule `invalid-base` is enabled by default
```

View File

@@ -0,0 +1,36 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `Protocol` base
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import Protocol
2 |
3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
```
# Diagnostics
```
info[unsupported-dynamic-base]: Unsupported base for class created via `type()`
--> src/mdtest_snippet.py:3:16
|
1 | from typing import Protocol
2 |
3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
| ^^^^^^^^ Has type `<special-form 'typing.Protocol'>`
|
info: Classes created via `type()` cannot be protocols
info: Consider using `class X(Protocol): ...` instead
info: rule `unsupported-dynamic-base` is enabled by default
```

View File

@@ -0,0 +1,36 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `TypedDict` base
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import TypedDict
2 |
3 | X = type("X", (TypedDict,), {}) # error: [invalid-base]
```
# Diagnostics
```
error[invalid-base]: Invalid base for class created via `type()`
--> src/mdtest_snippet.py:3:16
|
1 | from typing_extensions import TypedDict
2 |
3 | X = type("X", (TypedDict,), {}) # error: [invalid-base]
| ^^^^^^^^^ Has type `<special-form 'typing.TypedDict'>`
|
info: Classes created via `type()` cannot be TypedDicts
info: Consider using `TypedDict("X", {})` instead
info: rule `invalid-base` is enabled by default
```

View File

@@ -562,25 +562,16 @@ impl<'db> ClassLiteral<'db> {
}
/// Returns the generic context if this is a generic class.
///
// TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts
// to inherit from `Generic[T]`, since dynamic classes can't be generic. See also: `is_protocol`.
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
self.as_static().and_then(|class| class.generic_context(db))
}
/// Returns whether this class is a protocol.
///
// TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts
// to inherit from `Protocol`, since dynamic classes can't be protocols. See also: `generic_context`.
pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool {
self.as_static().is_some_and(|class| class.is_protocol(db))
}
/// Returns whether this class is a `TypedDict`.
// TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts
// to inherit from `TypedDict`. To create a dynamic TypedDict, you should invoke
// `TypedDict` as a function, not `type`. See also: `generic_context`, `is_protocol`.
pub fn is_typed_dict(self, db: &'db dyn Db) -> bool {
match self {
Self::Static(class) => class.is_typed_dict(db),
@@ -634,10 +625,12 @@ impl<'db> ClassLiteral<'db> {
}
/// Returns whether this class is final.
// TODO: Support `@final` on dynamic classes, e.g. `X = final(type("X", (), {}))`.
// We should either recognize and track this, or emit a diagnostic if unsupported.
pub(crate) fn is_final(self, db: &'db dyn Db) -> bool {
self.as_static().is_some_and(|class| class.is_final(db))
match self {
Self::Static(class) => class.is_final(db),
// Dynamic classes created via `type()` cannot be marked as final.
Self::Dynamic(_) => false,
}
}
/// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`,

View File

@@ -67,7 +67,6 @@ pub(crate) fn enum_metadata<'db>(
//
// MyEnum = type("MyEnum", (BaseEnum,), {"A": 1, "B": 2})
// ```
// TODO: Add a diagnostic for including an enum in a `type(...)` call.
return None;
}
};

View File

@@ -6443,26 +6443,129 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.iter()
.enumerate()
.map(|(idx, base)| {
let diagnostic_node = bases_tuple_elts
.and_then(|elts| elts.get(idx))
.unwrap_or(bases_node);
// First try the standard conversion.
if let Some(class_base) =
ClassBase::try_from_type(db, *base, placeholder_class)
{
// Collect disjoint bases for instance-layout-conflict checking.
if let ClassBase::Class(base_class) = class_base {
if let Some(disjoint_base) = base_class.nearest_disjoint_base(db) {
disjoint_bases.insert(
disjoint_base,
idx,
base_class.class_literal(db),
);
// Check for special bases that are not allowed for dynamic classes.
// Dynamic classes can't be generic, protocols, TypedDicts, or enums.
match class_base {
ClassBase::Generic | ClassBase::TypedDict => {
if let Some(builder) =
self.context.report_lint(&INVALID_BASE, diagnostic_node)
{
let mut diagnostic = builder.into_diagnostic(
"Invalid base for class created via `type()`",
);
diagnostic.set_primary_message(format_args!(
"Has type `{}`",
base.display(db)
));
match class_base {
ClassBase::Generic => {
diagnostic.info(
"Classes created via `type()` cannot be generic",
);
diagnostic.info(format_args!(
"Consider using `class {name}(Generic[...]): ...` instead"
));
}
ClassBase::TypedDict => {
diagnostic.info(
"Classes created via `type()` cannot be TypedDicts",
);
diagnostic.info(format_args!(
"Consider using `TypedDict(\"{name}\", {{}})` instead"
));
}
_ => unreachable!(),
}
}
return ClassBase::unknown();
}
}
return class_base;
}
ClassBase::Protocol => {
if let Some(builder) = self
.context
.report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node)
{
let mut diagnostic = builder.into_diagnostic(
"Unsupported base for class created via `type()`",
);
diagnostic.set_primary_message(format_args!(
"Has type `{}`",
base.display(db)
));
diagnostic.info(
"Classes created via `type()` cannot be protocols",
);
diagnostic.info(format_args!(
"Consider using `class {name}(Protocol): ...` instead"
));
}
return ClassBase::unknown();
}
ClassBase::Class(class_type) => {
// Check if base is @final (includes enums with members).
if class_type.is_final(db) {
if let Some(builder) = self
.context
.report_lint(&SUBCLASS_OF_FINAL_CLASS, diagnostic_node)
{
builder.into_diagnostic(format_args!(
"Class `{name}` cannot inherit from final class `{}`",
class_type.name(db)
));
}
return ClassBase::unknown();
}
let diagnostic_node = bases_tuple_elts
.and_then(|elts| elts.get(idx))
.unwrap_or(bases_node);
// Enum subclasses require the EnumMeta metaclass, which
// expects special dict attributes that `type()` doesn't provide.
if let Some((static_class, _)) =
class_type.static_class_literal(db)
{
if is_enum_class_by_inheritance(db, static_class) {
if let Some(builder) = self
.context
.report_lint(&INVALID_BASE, diagnostic_node)
{
let mut diagnostic = builder.into_diagnostic(
"Invalid base for class created via `type()`",
);
diagnostic.set_primary_message(format_args!(
"Has type `{}`",
base.display(db)
));
diagnostic.info(
"Creating an enum class via `type()` is not supported",
);
diagnostic.info(format_args!(
"Consider using `Enum(\"{name}\", [])` instead"
));
}
return ClassBase::unknown();
}
}
// Collect disjoint bases for instance-layout-conflict checking.
if let Some(disjoint_base) = class_type.nearest_disjoint_base(db)
{
disjoint_bases.insert(
disjoint_base,
idx,
class_type.class_literal(db),
);
}
return class_base;
}
ClassBase::Dynamic(_) => return class_base,
}
}
// If that fails, check if the type is "type-like" (e.g., `type[Base]`).
// For type-like bases we emit `unsupported-dynamic-base` and use