# Python Hashable Objects: Implementing __hash__ and __eq__ for Custom Object Comparison

> This blog is generated as a usecase of this other blog([Automating blog creation with own agent vs mcp](https://blog.buddhag.com.np/automating-blog-creation-mcp-vs-creating-own-agent)) that helped me automate the writing of this blog.

Alright, buckle up buttercups. Today we're diving into the murky, sometimes terrifying, world of hashable objects in Python. Forget those LinkedIn influencers droning on about "synergy" and "disruption." We're talking about the *actual* nuts and bolts of making your Python code not just *work*, but work *efficiently* and *predictably*. And yes, I'm going to make fun of something along the way. Probably many things.

## What in the Holy Guido is a Hashable Object?

Simply put, a hashable object is one that has a hash value that *never changes* during its lifetime. This hash value is an integer, and it's used by Python to quickly look up objects in dictionaries and sets. Think of it like a social security number for your data. Once assigned, it's (supposed to be) permanent.

More formally, an object is hashable if it has:

* A `__hash__()` method which returns an integer.
    
* An `__eq__()` method to compare it to other objects for equality.
    

The crucial point? If two objects are equal (i.e., `a == b` is `True`), then their hash values *must* be the same (i.e., `hash(a) == hash(b)`). Conversely, if their hash values are different, they *should* be unequal. (Collisions happen, but minimizing them is the goal.)

## "TypeError: unhashable type: 'list'" - A Tale of Woe

Ever seen this error? It usually pops up when you try to use a list as a key in a dictionary or as an element in a set. Let's see it in action:

```python
my_list = [1, 2, 3]
try:
    my_dict = {my_list: "value"}
except TypeError as e:
    print(f"Caught the expected error: {e}")

try:
    my_set = {my_list}
except TypeError as e:
    print(f"Caught the expected error: {e}")
```

Why does this happen? Because lists are *mutable*. You can change them after they're created. If you could use a list as a dictionary key, and then change the list, the dictionary would no longer be able to find the value associated with that key. Chaos would reign. Dogs and cats living together! Mass hysteria!

## The `__eq__` Connection: They're Married, Deal With It

The `__hash__()` and `__eq__()` methods are inextricably linked. If you define `__eq__()` for your custom class, you *must* also define `__hash__()`. And, as I mentioned earlier, if two objects are equal according to `__eq__()`, their hash values *must* be the same.

If you define `__eq__` but *not* `__hash__`, Python will implicitly set `__hash__ = None`, and your object will be unhashable. This is a safety mechanism to prevent you from accidentally breaking the hash table invariants. Python is looking out for you, even if it feels like a passive-aggressive intervention.

## Leveraging Hashability: Dictionaries and Sets, Baby!

The primary benefit of hashable objects is their ability to be used as keys in dictionaries and elements in sets. These data structures rely on hashing for fast lookups and membership tests. If you're doing a lot of searching or checking for duplicates, using dictionaries or sets with hashable objects can dramatically improve your code's performance.

Consider this highly contrived example:

```python
# A list of names
names = ["Alice", "Bob", "Charlie", "Alice", "David", "Bob"]

# Using a set to find unique names (efficiently!)
unique_names = set(names)
print(f"Unique names: {unique_names}")

# Checking if a name is in the set (also efficient!)
if "Alice" in unique_names:
    print("Alice is unique-ish.")
```

The `set()` constructor automatically removes duplicates because it relies on hashing. And checking for membership in a set (using the `in` operator) is much faster than iterating through a list.

## Mutability and Hashability: A Thorny Relationship

Generally, mutable objects (like lists and dictionaries) are *not* hashable, while immutable objects (like tuples, strings, and numbers) *are* hashable. This is because the hash value of an object must remain constant throughout its lifetime. If you could change a mutable object after it's been used as a dictionary key, the dictionary would become corrupted.

However, there are exceptions! You *can* create custom mutable objects that are hashable, but you need to be very careful. The key is to ensure that the hash value is based on attributes that *never change* after the object is created. This is tricky, and I generally advise against it unless you have a very good reason.

## `dataclasses` to the Rescue (Sometimes)

Python's `dataclasses` module can automatically generate `__hash__()` and `__eq__()` methods for you, based on the fields you define in your data class. By default, a dataclass is hashable if all of its fields are hashable. You can also explicitly specify that a dataclass should be frozen (immutable) using the `@dataclass(frozen=True)` decorator.

Here's an example:

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)

print(f"p1 == p2: {p1 == p2}")
print(f"hash(p1) == hash(p2): {hash(p1) == hash(p2)}")

my_dict = {p1: "origin"} # This now works!
```

By setting `frozen=True`, we've made the `Point` class immutable and hashable. This is a great way to create simple, data-centric classes that can be used as dictionary keys or set elements.

## Conclusion: Embrace the Hash, Fear the Mutability

Hashable objects are fundamental to Python's data structures and performance. Understanding the relationship between `__hash__()`, `__eq__()`, and mutability is crucial for writing robust and efficient code.

So, go forth and hash! But remember, with great power comes great responsibility (and the potential for really weird bugs if you screw it up). And for the love of all that is holy, stop listening to those LinkedIn influencers. They're probably just trying to sell you something. Now, if you'll excuse me, I need to go yell at a cloud.
