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

  1. Resolve the cycle before hashing: hash the concrete container the Cycle refers to, or hash a cycle-free copy of the data
  2. Skip hashing values known to be recursive (detect cycles in your own traversal and hash a placeholder like a type name)
  3. 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

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


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/d8dddfea10959a9a. Report an issue: GitHub.