astral-sh/ruff · error

there must be a first type if there are conflicting types

Error message

there must be a first type if there are conflicting types

What it means

DeclaredTypeAndConflictingTypesBuilder assembles a symbol's declared type plus the list of conflicting declared types recorded across multiple declarations. Its invariant is that whenever a conflicting type is recorded, the original first declared type is stashed in first_type; build() panics if conflicting_types is non-empty while first_type was never set.

Source

Thrown at crates/ty_python_semantic/src/place.rs:1990

                }
            } else {
                self.first_type = Some(element_ty);
            }
        }

        self.qualifiers = self.qualifiers.union(element.qualifiers());
    }

    fn build(mut self) -> DeclaredTypeAndConflictingTypes<'db> {
        let type_and_quals =
            TypeAndQualifiers::new(self.inner.build(), TypeOrigin::Declared, self.qualifiers);
        if self.conflicting_types.is_empty() {
            (type_and_quals, None)
        } else {
            self.conflicting_types.insert_before(
                0,
                self.first_type
                    .expect("there must be a first type if there are conflicting types"),
            );
            (
                type_and_quals,
                Some(self.conflicting_types.into_boxed_slice()),
            )
        }
    }
}

/// Implementation of [`place_from_declarations`].
///
/// ## Implementation Note
/// This function gets called cross-module. It, therefore, shouldn't
/// access any AST nodes from the file containing the declarations.
fn place_from_declarations_impl<'db>(
    db: &'db dyn Db,
    env: &ProgramEnvironment<'db>,
    declarations_iterator: DeclarationsIterator<'_, 'db>,

View on GitHub (pinned to fca5c7cf2c)

Solutions

  1. Minimize the module to the two conflicting annotations that trigger the panic and file a ty issue with it
  2. As a contributor: ensure every path that pushes onto conflicting_types sets first_type beforehand (record the original type before treating later types as conflicts)
  3. Workaround until fixed: unify or remove one of the conflicting declarations for that name
Defensive patterns

Strategy: fallback

Try / catch

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| check_file(&db, file)));
match result {
    Ok(diags) => diags,
    Err(payload) => { log::warn!("ty panicked while declaring {}: {:?}", file, payload); vec![] }
}

Prevention

When it happens

Trigger: A symbol that receives two or more different declared types (for example `x: int` in one branch and `x: str]` in another, or repeated annotations) where the code path that pushed the conflicting type never recorded the first type first - a builder misuse or a missed update when a new conflict-recording path was added.

Common situations: Modules with multiple differently-annotated declarations of the same name (conditional imports, branch-local annotations); contributor changes to place_from_declarations conflict collection.

Related errors


AI-assisted analysis of astral-sh/ruff@fca5c7cf2c (2026-08-20). Data as JSON: /api/errors/85469f50f6419ad7. Report an issue: GitHub.