{"id":"dsa-hash-own-keys","slug":"own-keys","title":"Hashing your own keys","text":"Numbers, strings and tuples work as dict keys and set items. Lists don't:\n\n```python\nplaces = {}\nplaces[[2, 3]] = \"home\"\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 2, in <module>\n    places[[2, 3]] = \"home\"\n    ~~~~~~^^^^^^^^\nTypeError: cannot use 'list' as a dict key (unhashable type: 'list')\n```\n\n### Why a list can't be a key\n\nA table finds a key by its hash, worked out when the key goes in. A list can change after that: append to it, and its hash, if it had one, would change too. Its pair would then sit in the bucket of the old hash, where no lookup ever looks again. So among Python's own types, only those whose values can't change, the **immutable** ones, are **hashable**: numbers, strings and tuples. A tuple can't change, so `(2, 3)` works as a key, and the usual way to use a list as a key is to make a tuple of it first: `places[tuple(position)] = \"home\"`. A tuple is hashable only if everything in it is: `([2], 3)` isn't.\n\n### Keys of your own\n\nObjects of your own classes can be keys, but by default both `==` and the hash go by identity: two `Point(1, 2)` objects are two different keys. Once a class has `__eq__`, Python takes its default hash away, and putting a point in a set stops with `TypeError: cannot use 'Point' as a set element (unhashable type: 'Point')`. You write **`__hash__`** too, and the two methods must agree:\n\n**Objects that are equal must have equal hashes.** A table looks for a key only in the bucket of its hash, so if two equal points had different hashes, a lookup for one would never find the other. Objects that aren't equal may share a hash; that's just a collision.\n\nThe easy way to keep the rule is to hash a tuple of exactly the fields that `__eq__` compares. `__hash__` returns a whole number, and the table works out the bucket from it:\n\n```python\nclass Point:\n    def __init__(self, x, y):\n        self.x = x\n        self.y = y\n\n    def __eq__(self, other):\n        return isinstance(other, Point) and self.x == other.x and self.y == other.y\n\n    def __hash__(self):\n        return hash((self.x, self.y))\n\n    def __repr__(self):\n        return f\"Point({self.x}, {self.y})\"\n\n\np = Point(1, 2)\nvisited = {p}\nvisited.add(Point(1, 2))\nprint(len(visited), Point(1, 2) in visited, Point(2, 1) in visited)\n\np.x = 5\nprint(visited)\nprint(p in visited, Point(1, 2) in visited)\n```\n\n```output\n1 True False\n{Point(5, 2)}\nFalse False\n```\n\nThe second `Point(1, 2)` is equal to the first and has the same hash, so the set keeps one point. Then the program changes the point while it's in the set. The set still holds it, as the print shows, but nothing finds it any more: `p in visited` looks in the bucket of the new hash, and `Point(1, 2)` finds the right bucket but no equal point in it. So **a key must not change while it's in a table**: hash only fields that never change after `__init__`, or use tuples.\n\n### A legal hash that's terrible\n\n`return 0` keeps the rule too: equal objects get equal hashes. But then every object collides with every other, and the table is one long chain. This class counts how often the set calls its `__eq__`:\n\n```python\nclass Ticket:\n    comparisons = 0\n\n    def __init__(self, number):\n        self.number = number\n\n    def __eq__(self, other):\n        Ticket.comparisons += 1\n        return self.number == other.number\n\n    def __hash__(self):\n        return 0\n\n\ntickets = set()\nfor i in range(1000):\n    tickets.add(Ticket(i))\nprint(len(tickets), Ticket.comparisons)\n```\n\n```output\n1000 613645\n```\n\nEach new ticket is compared with the tickets already there, so building a set of n tickets is O(n²), and each lookup O(n). With `return hash(self.number)` instead, the program prints 0 comparisons: the set calls `__eq__` only for keys whose hashes are equal, and these numbers all have different hashes.\n","task":"`Version` is the version number of a program, such as 3.14: a major number and a minor number. Give it `__eq__` and `__hash__`, so that versions work in sets and as dict keys. Two versions are equal when both the major and the minor numbers are equal, and a version is never equal to something that isn't a version. The hidden test puts 50,000 different versions in a set: a hash that's the same for every version makes that O(n²). This program:\n\n```python\nversions = {Version(3, 14), Version(2, 7), Version(3, 14)}\nprint(len(versions), Version(2, 7) in versions, Version(3, 13) in versions)\nprint(Version(3, 14) == Version(3, 14), Version(3, 14) == (3, 14))\n```\n\nprints:\n\n```\n2 True False\nTrue False\n```\n","kind":"code","seed":"class Version:\n    def __init__(self, major, minor):\n        self.major = major\n        self.minor = minor\n\n\nif __name__ == \"__main__\":\n    versions = {Version(3, 14), Version(2, 7), Version(3, 14)}\n    print(len(versions), Version(2, 7) in versions, Version(3, 13) in versions)\n    print(Version(3, 14) == Version(3, 14), Version(3, 14) == (3, 14))\n","solution":"class Version:\n    def __init__(self, major, minor):\n        self.major = major\n        self.minor = minor\n\n    def __eq__(self, other):\n        return isinstance(other, Version) and self.major == other.major and self.minor == other.minor\n\n    def __hash__(self):\n        return hash((self.major, self.minor))\n\n\nif __name__ == \"__main__\":\n    versions = {Version(3, 14), Version(2, 7), Version(3, 14)}\n    print(len(versions), Version(2, 7) in versions, Version(3, 13) in versions)\n    print(Version(3, 14) == Version(3, 14), Version(3, 14) == (3, 14))\n","hints":["`__eq__` returns `True` when `other` is a `Version` with the same major and the same minor number. Check `isinstance(other, Version)` first, so that comparing with a tuple gives `False` instead of an error.","`__hash__` must give equal versions equal hashes: hash a tuple of exactly the fields `__eq__` compares, `hash((self.major, self.minor))`.","A hash that's the same for every version, such as `return 0`, keeps the rule, but every version collides, and the big set takes O(n²)."],"tests":[{"name":"Test 1","stdin":"","call":"versions = {Version(3, 14), Version(2, 7), Version(3, 14)}\nprint(len(versions), Version(2, 7) in versions, Version(3, 13) in versions)\nprint(Version(3, 14) == Version(3, 14), Version(3, 14) == (3, 14))\n","expect":"2 True False\nTrue False","hidden":false},{"name":"Test 2","stdin":"","call":"notes = {Version(3, 12): \"old\", Version(3, 13): \"stable\"}\nnotes[Version(3, 12)] = \"security fixes\"\nprint(len(notes), notes[Version(3, 12)], notes[Version(3, 13)])\n","expect":"2 security fixes stable","hidden":true},{"name":"Test 3","stdin":"","call":"print(Version(4, 5) != Version(5, 4), Version(4, 5) != Version(4, 5), Version(3, 14) == \"3.14\")\nprint(hash(Version(2, 7)) == hash(Version(2, 7)))\n","expect":"True False False\nTrue","hidden":true},{"name":"Test 4","stdin":"","call":"installs = [Version(1, 0), Version(1, 1), Version(2, 0), Version(1, 1), Version(1, 0)]\nprint(len(set(installs)), installs.count(Version(1, 1)))\n","expect":"3 2","hidden":true},{"name":"Test 5","stdin":"","call":"versions = {Version(i // 400, i % 400) for i in range(50_000)}\nprint(len(versions), Version(124, 399) in versions, Version(125, 0) in versions)\n","expect":"50000 True False","hidden":true,"big":true}],"require":[],"forbid":[],"more":[{"task":"Write `group_anagrams(words)`. Two words are **anagrams** when they have the same letters in another order, such as \"listen\" and \"silent\". Uppercase and lowercase letters are different letters: \"Night\" and \"thing\" aren't anagrams, but \"night\" and \"thing\" are. Return a list of groups, each a list of words that are anagrams of each other. Keep the order of `words`: the group whose first word comes earliest in `words` comes first, and inside each group the words stay in the order they had.\n\nUse a dict: the sorted letters of a word make a good key, but `sorted` gives a list, which can't be a key. The hidden test groups 60,000 strings of digits into about 28,000 groups, so comparing each word with every group so far is too slow.\n\nFor example, this call:\n\n```python\nprint(group_anagrams([\"stop\", \"melon\", \"pots\", \"lemon\", \"tops\", \"kiwi\"]))\n```\n\nprints:\n\n```\n[['stop', 'pots', 'tops'], ['melon', 'lemon'], ['kiwi']]\n```\n","kind":"code","seed":"def group_anagrams(words):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(group_anagrams([\"stop\", \"melon\", \"pots\", \"lemon\", \"tops\", \"kiwi\"]))\n","solution":"def group_anagrams(words):\n    groups = {}\n    for word in words:\n        key = tuple(sorted(word))\n        if key not in groups:\n            groups[key] = []\n        groups[key].append(word)\n    return list(groups.values())\n\n\nif __name__ == \"__main__\":\n    print(group_anagrams([\"stop\", \"melon\", \"pots\", \"lemon\", \"tops\", \"kiwi\"]))\n","hints":["Anagrams have the same letters, so sorting their letters gives the same result: `sorted(\"pots\")` and `sorted(\"stop\")` are both `['o', 'p', 's', 't']`.","That list can't be a dict key, but a tuple can: `key = tuple(sorted(word))`. (`\"\".join(sorted(word))`, a string, works too.)","Keep a dict from each key to its list of words. A dict remembers the order its keys were added in, so `list(groups.values())` gives the groups in the order of their first words."],"tests":[{"name":"print(group_anagrams([\"stop\", \"melon\", \"pots\", \"lemon\", \"tops\", \"kiwi\"]))","stdin":"","call":"print(group_anagrams([\"stop\", \"melon\", \"pots\", \"lemon\", \"tops\", \"kiwi\"]))\n","expect":"[['stop', 'pots', 'tops'], ['melon', 'lemon'], ['kiwi']]","hidden":false},{"name":"print(group_anagrams([]), group_anagrams([\"solo\"]))","stdin":"","call":"print(group_anagrams([]), group_anagrams([\"solo\"]))\n","expect":"[] [['solo']]","hidden":true},{"name":"group_anagrams([\"rat\", \"tar\", \"rat\", \"art\"])","stdin":"","call":"group_anagrams([\"rat\", \"tar\", \"rat\", \"art\"])\n","expect":"[['rat', 'tar', 'rat', 'art']]","hidden":true,"returns":true},{"name":"group_anagrams([\"abc\", \"abd\", \"cab\", \"dab\", \"bad\"])","stdin":"","call":"group_anagrams([\"abc\", \"abd\", \"cab\", \"dab\", \"bad\"])\n","expect":"[['abc', 'cab'], ['abd', 'dab', 'bad']]","hidden":true,"returns":true},{"name":"group_anagrams([\"aab\", \"abb\", \"bab\", \"aba\"])","stdin":"","call":"group_anagrams([\"aab\", \"abb\", \"bab\", \"aba\"])\n","expect":"[['aab', 'aba'], ['abb', 'bab']]","hidden":true,"returns":true},{"name":"group_anagrams([\"Night\", \"thing\", \"night\", \"a\", \"a\"])","stdin":"","call":"group_anagrams([\"Night\", \"thing\", \"night\", \"a\", \"a\"])\n","expect":"[['Night'], ['thing', 'night'], ['a', 'a']]","hidden":true,"returns":true},{"name":"Test 7","stdin":"","call":"groups = group_anagrams([str(i * i) for i in range(60_000)])\nprint(len(groups), max(len(group) for group in groups), groups[1], groups[-1])\n","expect":"27807 35 ['1'] ['3599400025']","hidden":true,"big":true}],"require":[],"forbid":[]},{"task":"Two `Student` objects are the same student when their ids are equal, even when the name was typed in another way. `__eq__` says so, but a set of students still counts the same student twice. Find the problem and fix it.\n\nFor example, this program:\n\n```python\nsignups = [Student(1001, \"Ada Lovelace\"), Student(1002, \"Ben Ng\"), Student(1001, \"ada lovelace\")]\nprint(len(set(signups)))\n```\n\nshould print:\n\n```\n2\n```\n","kind":"code","seed":"class Student:\n    def __init__(self, student_id, name):\n        self.student_id = student_id\n        self.name = name\n\n    def __eq__(self, other):\n        return isinstance(other, Student) and self.student_id == other.student_id\n\n    def __hash__(self):\n        return hash((self.student_id, self.name))\n\n\nif __name__ == \"__main__\":\n    signups = [Student(1001, \"Ada Lovelace\"), Student(1002, \"Ben Ng\"), Student(1001, \"ada lovelace\")]\n    print(len(set(signups)))\n","solution":"class Student:\n    def __init__(self, student_id, name):\n        self.student_id = student_id\n        self.name = name\n\n    def __eq__(self, other):\n        return isinstance(other, Student) and self.student_id == other.student_id\n\n    def __hash__(self):\n        return hash(self.student_id)\n\n\nif __name__ == \"__main__\":\n    signups = [Student(1001, \"Ada Lovelace\"), Student(1002, \"Ben Ng\"), Student(1001, \"ada lovelace\")]\n    print(len(set(signups)))\n","hints":["`__eq__` compares only the ids. What does `__hash__` look at?","Two equal students with differently typed names get different hashes, so the set never even compares them with `==`: equal objects must have equal hashes.","Hash only what `__eq__` compares: `return hash(self.student_id)`."],"tests":[{"name":"Test 1","stdin":"","call":"signups = [Student(1001, \"Ada Lovelace\"), Student(1002, \"Ben Ng\"), Student(1001, \"ada lovelace\")]\nprint(len(set(signups)))\n","expect":"2","hidden":false},{"name":"print(Student(5, \"Kai\") in {Student(5, \"kai\")}, Student(6, \"Kai\") in {Student(5, \"Kai\")})","stdin":"","call":"print(Student(5, \"Kai\") in {Student(5, \"kai\")}, Student(6, \"Kai\") in {Student(5, \"Kai\")})\n","expect":"True False","hidden":true},{"name":"Test 3","stdin":"","call":"grades = {Student(7, \"Mia\"): 9}\ngrades[Student(7, \"Mia K.\")] = 10\nprint(len(grades), grades[Student(7, \"M\")])\n","expect":"1 10","hidden":true},{"name":"Test 4","stdin":"","call":"names = [\"Eve\", \"eve\", \"EVE\", \"Eve L.\"]\nprint(len({Student(42, name) for name in names}))\n","expect":"1","hidden":true}],"require":[],"forbid":[]}]}