astral-sh/ruff · error
dynamic class definitions should only be used for assignment
Error message
dynamic class definitions should only be used for assignments
What it means
ty models 'dynamic classes' created by factory calls assigned to a name, e.g. `Color = Enum('Color', 'RED GREEN')`. `dynamic_class_header_range` maps such a class back to its source range for diagnostics. The `Definition` anchor path assumes the stored `Definition` is an assignment: `definition.kind(db).value(&module)` must return the right-hand-side expression. The expect fires when a `DynamicClassHeaderAnchor::Definition` was built for a definition that has no value node in the loaded module (not an assignment, or a definition resolved against a different parse).
Source
Thrown at crates/ty_python_semantic/src/types/class.rs:114
StringAnnotation { offset: u32, range: TextRange },
}
/// Returns the source range of a call that creates a dynamic class.
///
/// ```python
/// Color = Enum("Color", "RED GREEN")
/// # ^^^^^^^^^^^^^^^^^^^^^^^^^^
/// ```
fn dynamic_class_header_range<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
anchor: DynamicClassHeaderAnchor<'db>,
) -> TextRange {
let module = parsed_module(db, scope.python_file(db)).load(db);
match anchor {
DynamicClassHeaderAnchor::Definition(definition) => definition
.kind(db)
.value(&module)
.expect("dynamic class definitions should only be used for assignments")
.range(),
DynamicClassHeaderAnchor::ScopeOffset(offset) => {
let (offset, relative_range) = match offset {
DynamicClassScopeOffset::Node(offset) => (offset, None),
DynamicClassScopeOffset::StringAnnotation { offset, range } => {
(offset, Some(range))
}
};
let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));
let anchor_u32 = scope_anchor
.as_u32()
.expect("anchor should not be NodeIndex::NONE");
let absolute_index = NodeIndex::from(anchor_u32 + offset);
if let Some(relative_range) = relative_range {
let string: &ast::ExprStringLiteral = module
.get_by_index(absolute_index)
.try_into()View on GitHub (pinned to 15f3fe6b15)
Solutions
- At every `DynamicClassHeaderAnchor::Definition(definition)` construction site (dynamic_literal.rs:248, enum_literal.rs:175, named_tuple.rs:219, typed_dict.rs:949), verify `definition.kind(db).value(&module)` is Some first; if not, use the ScopeOffset anchor or skip the range.
- Reproduce with a minimal Python file (variations of `X = Enum('X', 'A B')`) and run `INSTA_FORCE_PASS=1 cargo nextest run -p ty_python_semantic`.
- Confirm the module passed to `.value()` is parsed from the same file the definition belongs to, not merely the scope's file.
- If it reproduces on an unmodified ty build, file an issue at astral-sh/ruff with the input file and backtrace.
Example fix
// before
let anchor = DynamicClassHeaderAnchor::Definition(definition);
// after: only anchor assignments that have a value node in this module
let anchor = match definition.kind(db).value(&module) {
Some(_) => DynamicClassHeaderAnchor::Definition(definition),
None => return None,
}; Defensive patterns
Strategy: try-catch
Try / catch
// embedding ty: keep span computation from killing the host
let range = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
dynamic_class_header_range(db, scope, anchor)
}))
.unwrap_or_else(|_| file.range()); // fall back to the whole file Prevention
- Pin the ty/ruff version you validate against; do not mix builds.
- Embedders: install a panic hook that logs and quarantine the file rather than crashing the process.
- If a stock build panics, capture RUST_BACKTRACE=1 output and file it at github.com/astral-sh/ruff/issues with the input file.
When it happens
Trigger: Calling `header_range`/`header_span` on a dynamic class whose anchor is `DynamicClassAnchor::Definition` (anchors are built in dynamic_literal.rs, enum_literal.rs, named_tuple.rs, typed_dict.rs) while `definition.kind(db).value(&module)` returns None - i.e. the definition kind is not an assignment with a value expression in that module.
Common situations: Contributing to ty and extending dynamic-class detection (new factory functions, walrus targets, rebinding) so anchors get built for non-assignment definitions; refactors that change `DefinitionKind` handling; an anchor kept alive without a Salsa dependency on the file's parse so it is resolved against a stale/different AST.
Related errors
- anchor should not be NodeIndex::NONE
- scope offset should point to ExprCall
- DynamicClassAnchor::Definition should only be used for assig
- Definition value should be a call expression
- Expected `NamedTuple` definition to be an assignment
AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20).
Data as JSON: /api/errors/690de27d2a5e057e.
Report an issue: GitHub.