pydantic/monty · error
cycle values are not hashable
Error message
cycle values are not hashable
What it means
hash() on a MontyObject hits a dedicated panic for Self::Cycle values. Cycle is an internal placeholder used while hashing/traversing containers to mark a value already being hashed; a top-level hash of a cycle means a self-referential value reached the hash, which has no well-defined hash, so the library panics instead of producing an unstable hash.
Source
Thrown at crates/monty-types/src/object.rs:641
bi.to_signed_bytes_le().hash(state);
}
}
Self::Float(f) => f.to_bits().hash(state),
Self::String(string) => string.hash(state),
Self::Bytes(bytes) => bytes.hash(state),
Self::Date(date) => date.hash(state),
Self::DateTime(datetime) => datetime.hash(state),
Self::Time(time) => time.hash(state),
Self::TimeDelta(delta) => delta.hash(state),
Self::TimeZone(timezone) => timezone.hash(state),
Self::Path(path) => path.hash(state),
Self::FileHandle(MontyFileHandle { path, mode, position }) => {
path.hash(state);
mode.as_str().hash(state);
position.hash(state);
}
Self::Type(t) => t.name().hash(state),
Self::Cycle(_, _) => panic!("cycle values are not hashable"),
_ => panic!("{} python values are not hashable", self.type_name()),
}
}
}
impl PartialEq for MontyObject {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Ellipsis, Self::Ellipsis) => true,
(Self::NotImplemented, Self::NotImplemented) => true,
(Self::None, Self::None) => true,
(Self::Bool(a), Self::Bool(b)) => a == b,
(Self::Int(a), Self::Int(b)) => a == b,
(Self::BigInt(a), Self::BigInt(b)) => a == b,
// Cross-compare Int and BigInt without allocating a temporary BigInt.
(Self::Int(a), Self::BigInt(b)) | (Self::BigInt(b), Self::Int(a)) => b.to_i64() == Some(*a),
// Use to_bits() for float comparison to be consistent with Hash
(Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),View on GitHub (pinned to adc986b362)
Solutions
- Resolve the cycle before hashing: hash the concrete container the Cycle refers to, or hash a cycle-free copy of the data
- Skip hashing values known to be recursive (detect cycles in your own traversal and hash a placeholder like a type name)
- Do not construct MontyObject::Cycle manually; let the library's cycle-detecting traversal manage it
Example fix
// before let h = hash_of(&cycle_marked_value); // panics // after let resolved = resolve_cycles(value); // produce a DAG/acyclic copy let h = hash_of(&resolved);
Defensive patterns
Strategy: validation
Validate before calling
// Rust host-side cannot_hash_cycles = matches!(value, MontyObject::Cycle(_, _));
Type guard
fn is_cycle(v: &MontyObject) -> bool { matches!(v, MontyObject::Cycle(_, _)) } Try / catch
// hash() panics rather than returning Err — guard before calling assert!(!is_cycle(&value), 'resolve cycles before hashing'); let h = hash_of(&value);
Prevention
- Never construct MontyObject::Cycle in host code
- Resolve or copy cycle-free data before hashing
- Track recursion yourself and hash a type placeholder for recursive values
When it happens
Trigger: Calling hash (via hash_of on the host side) with a MontyObject::Cycle — a self-referential structure produced by deserialization of cyclic Python data — instead of the resolved object it stands for.
Common situations: Host code hashing values returned from a Monty run that contained self-referential lists/dicts; custom code constructing MontyObject::Cycle directly; a bug in cycle resolution before hashing.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- {} python values are not hashable
- component value arena contains a cycle
- gather commit frame id is not a GatherFuture
- gather item is not a Coroutine, ExternalFuture, or GatherFut
- Cannot get identity of Dereferenced object
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/d8dddfea10959a9a.
Report an issue: GitHub.