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

  1. 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.
  2. 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`.
  3. Confirm the module passed to `.value()` is parsed from the same file the definition belongs to, not merely the scope's file.
  4. 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

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


AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20). Data as JSON: /api/errors/690de27d2a5e057e. Report an issue: GitHub.