From a5983bae6366043a43f1b49a537b14aaa28a8b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Mon, 26 Oct 2026 10:30:59 +0000 Subject: [PATCH 1/2] Sync hashtable with the updated tutorial (Python 3.15) - BLANK and DELETED are built-in sentinel() objects instead of object() - HashTable.pairs in 05_separate_chaining unpacks the buckets in a set comprehension (PEP 798) - requirements.txt: pytest 9.1.1, pytest-unordered 0.8.0 on Python 3.15 - README: the code requires Python 3.15 or later - pyproject.toml: exclude hashtable from ruff, like the python315-* folders (ruff 0.14.1 targets py314: sentinel is F821 there, and unpacking in a comprehension is a syntax error) --- .../02_insert_a_key_value_pair/hashtable.py | 2 +- .../03_find_a_value_by_key/hashtable.py | 2 +- .../04_delete_a_key_value_pair/hashtable.py | 2 +- .../hashtable.py | 2 +- hashtable/02_linear_probing/hashtable.py | 2 +- hashtable/03_autoresize/hashtable.py | 2 +- hashtable/04_load_factor/hashtable.py | 2 +- hashtable/05_separate_chaining/hashtable.py | 2 +- hashtable/README.md | 4 +++- hashtable/requirements.txt | 15 ++++++--------- pyproject.toml | 1 + 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/hashtable/01_hashtable_prototype/02_insert_a_key_value_pair/hashtable.py b/hashtable/01_hashtable_prototype/02_insert_a_key_value_pair/hashtable.py index c018b7f71d..4f8fe0917f 100644 --- a/hashtable/01_hashtable_prototype/02_insert_a_key_value_pair/hashtable.py +++ b/hashtable/01_hashtable_prototype/02_insert_a_key_value_pair/hashtable.py @@ -1,6 +1,6 @@ # hashtable.py -BLANK = object() +BLANK = sentinel("BLANK") class HashTable: diff --git a/hashtable/01_hashtable_prototype/03_find_a_value_by_key/hashtable.py b/hashtable/01_hashtable_prototype/03_find_a_value_by_key/hashtable.py index 480f1c12f5..1222f4b093 100644 --- a/hashtable/01_hashtable_prototype/03_find_a_value_by_key/hashtable.py +++ b/hashtable/01_hashtable_prototype/03_find_a_value_by_key/hashtable.py @@ -1,6 +1,6 @@ # hashtable.py -BLANK = object() +BLANK = sentinel("BLANK") class HashTable: diff --git a/hashtable/01_hashtable_prototype/04_delete_a_key_value_pair/hashtable.py b/hashtable/01_hashtable_prototype/04_delete_a_key_value_pair/hashtable.py index 0b4d533bd2..44b2c6db16 100644 --- a/hashtable/01_hashtable_prototype/04_delete_a_key_value_pair/hashtable.py +++ b/hashtable/01_hashtable_prototype/04_delete_a_key_value_pair/hashtable.py @@ -1,6 +1,6 @@ # hashtable.py -BLANK = object() +BLANK = sentinel("BLANK") class HashTable: diff --git a/hashtable/01_hashtable_prototype/05_update_the_value_of_an_existing_pair/hashtable.py b/hashtable/01_hashtable_prototype/05_update_the_value_of_an_existing_pair/hashtable.py index 0b4d533bd2..44b2c6db16 100644 --- a/hashtable/01_hashtable_prototype/05_update_the_value_of_an_existing_pair/hashtable.py +++ b/hashtable/01_hashtable_prototype/05_update_the_value_of_an_existing_pair/hashtable.py @@ -1,6 +1,6 @@ # hashtable.py -BLANK = object() +BLANK = sentinel("BLANK") class HashTable: diff --git a/hashtable/02_linear_probing/hashtable.py b/hashtable/02_linear_probing/hashtable.py index 17ba0e5398..cf2b918f41 100644 --- a/hashtable/02_linear_probing/hashtable.py +++ b/hashtable/02_linear_probing/hashtable.py @@ -2,7 +2,7 @@ from typing import Any, NamedTuple -DELETED = object() +DELETED = sentinel("DELETED") class Pair(NamedTuple): diff --git a/hashtable/03_autoresize/hashtable.py b/hashtable/03_autoresize/hashtable.py index 77b8963761..eba084d860 100644 --- a/hashtable/03_autoresize/hashtable.py +++ b/hashtable/03_autoresize/hashtable.py @@ -2,7 +2,7 @@ from typing import Any, NamedTuple -DELETED = object() +DELETED = sentinel("DELETED") class Pair(NamedTuple): diff --git a/hashtable/04_load_factor/hashtable.py b/hashtable/04_load_factor/hashtable.py index e440e16b5a..c8202b45c3 100644 --- a/hashtable/04_load_factor/hashtable.py +++ b/hashtable/04_load_factor/hashtable.py @@ -2,7 +2,7 @@ from typing import Any, NamedTuple -DELETED = object() +DELETED = sentinel("DELETED") class Pair(NamedTuple): diff --git a/hashtable/05_separate_chaining/hashtable.py b/hashtable/05_separate_chaining/hashtable.py index 7ff3754c94..60926b2605 100644 --- a/hashtable/05_separate_chaining/hashtable.py +++ b/hashtable/05_separate_chaining/hashtable.py @@ -91,7 +91,7 @@ def get(self, key, default=None): @property def pairs(self): - return {pair for bucket in self._buckets for pair in bucket} + return {*bucket for bucket in self._buckets} @property def values(self): diff --git a/hashtable/README.md b/hashtable/README.md index 68772e9cec..1df7aa37ba 100644 --- a/hashtable/README.md +++ b/hashtable/README.md @@ -4,10 +4,12 @@ Code supplementing the [tutorial on the hash table](https://realpython.com/pytho ## Installation +The code requires Python 3.15 or later. It uses the built-in `sentinel()` and unpacking in comprehensions, which are new in Python 3.15. + To get started, create and activate a new virtual environment, and then install the required dependencies into it: ```shell -$ python3 -m venv venv --prompt=hashtable +$ python3.15 -m venv venv --prompt=hashtable $ source venv/bin/activate (hashtable) $ python -m pip install -r requirements.txt ``` diff --git a/hashtable/requirements.txt b/hashtable/requirements.txt index 5371299fc3..43d9132597 100644 --- a/hashtable/requirements.txt +++ b/hashtable/requirements.txt @@ -1,9 +1,6 @@ -attrs==21.4.0 -iniconfig==1.1.1 -packaging==21.3 -pluggy==1.0.0 -py==1.11.0 -pyparsing==3.0.7 -pytest==7.0.1 -pytest-unordered==0.4.1 -tomli==2.0.1 +iniconfig==2.3.1 +packaging==26.3 +pluggy==1.7.0 +pygments==2.21.0 +pytest==9.1.1 +pytest-unordered==0.8.0 diff --git a/pyproject.toml b/pyproject.toml index e26dbfd9e6..e6e772c7a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ exclude = [ "python315-frozendict/", "python315-lazy-imports", "python315-new-features", + "hashtable", "ai-benchmark", "python-debugging-pdb" ] From 4ec1aa974278fdfc560384ae5cd79bad9c704585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Mon, 26 Oct 2026 13:17:36 +0000 Subject: [PATCH 2/2] hashtable: a deep copy of the pairs in the defensive-copying step A shallow copy of the list still shares mutable values, so a caller could change the table through it (reported by a reader in 2022). From the next step on, .pairs is a set, whose members must be hashable, so a shallow copy is enough there. --- .../07_use_defensive_copying/hashtable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hashtable/01_hashtable_prototype/07_use_defensive_copying/hashtable.py b/hashtable/01_hashtable_prototype/07_use_defensive_copying/hashtable.py index 2b648b9a10..9fd10feeeb 100644 --- a/hashtable/01_hashtable_prototype/07_use_defensive_copying/hashtable.py +++ b/hashtable/01_hashtable_prototype/07_use_defensive_copying/hashtable.py @@ -1,5 +1,6 @@ # hashtable.py +import copy from typing import Any, NamedTuple @@ -46,7 +47,7 @@ def get(self, key, default=None): @property def pairs(self): - return [pair for pair in self._pairs if pair] + return copy.deepcopy([pair for pair in self._pairs if pair]) def _index(self, key): return hash(key) % len(self)