diff --git a/python315-sentinel-values/README.md b/python315-sentinel-values/README.md new file mode 100644 index 0000000000..81a47c9583 --- /dev/null +++ b/python315-sentinel-values/README.md @@ -0,0 +1,3 @@ +# Python 3.15 Preview: Sentinel Values + +This folder provides the code examples for the Real Python tutorial [Python 3.15 Preview: Sentinel Values](https://realpython.com/python315-sentinel-values/) diff --git a/python315-sentinel-values/config.py b/python315-sentinel-values/config.py new file mode 100644 index 0000000000..0f803663a0 --- /dev/null +++ b/python315-sentinel-values/config.py @@ -0,0 +1,8 @@ +UNSET = sentinel("UNSET") +INHERIT = sentinel("INHERITED") # Made a typo in the variable name + + +class Config: + AUTO = sentinel("Config.AUTO") + # Some code here... + NO_LIMIT = sentinel("NO_LIMIT") # Forgot the qualified name diff --git a/python315-sentinel-values/example_001.py b/python315-sentinel-values/example_001.py new file mode 100644 index 0000000000..19980e012e --- /dev/null +++ b/python315-sentinel-values/example_001.py @@ -0,0 +1 @@ +print("Hello, World!".find("z")) diff --git a/python315-sentinel-values/example_002.py b/python315-sentinel-values/example_002.py new file mode 100644 index 0000000000..69643f14e2 --- /dev/null +++ b/python315-sentinel-values/example_002.py @@ -0,0 +1,9 @@ +import configparser +import inspect +import typing +import unittest.mock + +print(configparser._UNSET) +print(inspect.Parameter.empty) +print(typing.NoDefault) +print(unittest.mock.DEFAULT) diff --git a/python315-sentinel-values/example_003.py b/python315-sentinel-values/example_003.py new file mode 100644 index 0000000000..fc5b8b0707 --- /dev/null +++ b/python315-sentinel-values/example_003.py @@ -0,0 +1,7 @@ +import pickle +from copy import deepcopy + +_MISSING = object() +print(_MISSING) +print(deepcopy(_MISSING) is _MISSING) +print(pickle.loads(pickle.dumps(_MISSING)) is _MISSING) diff --git a/python315-sentinel-values/example_004.py b/python315-sentinel-values/example_004.py new file mode 100644 index 0000000000..3eaa29bab1 --- /dev/null +++ b/python315-sentinel-values/example_004.py @@ -0,0 +1,4 @@ +grades = {"Jane": 90, "John": None} +print(grades.get("John") is grades.get("Linda")) +print("Pythonista!".find("z")) +print("Pythonista!"["Pythonista!".find("z")]) diff --git a/python315-sentinel-values/example_005.py b/python315-sentinel-values/example_005.py new file mode 100644 index 0000000000..1be8c48dc5 --- /dev/null +++ b/python315-sentinel-values/example_005.py @@ -0,0 +1,11 @@ +from copy import deepcopy + + +class _MissingType: + def __repr__(self): + return "MISSING" + + +MISSING = _MissingType() +print(MISSING) +print(deepcopy(MISSING) is MISSING) diff --git a/python315-sentinel-values/example_006.py b/python315-sentinel-values/example_006.py new file mode 100644 index 0000000000..8fc07eaa4b --- /dev/null +++ b/python315-sentinel-values/example_006.py @@ -0,0 +1,8 @@ +import enum + + +class MissingType(enum.Enum): + MISSING = "MISSING" + + +print(MissingType.MISSING) diff --git a/python315-sentinel-values/example_007.py b/python315-sentinel-values/example_007.py new file mode 100644 index 0000000000..1b36cedc90 --- /dev/null +++ b/python315-sentinel-values/example_007.py @@ -0,0 +1,2 @@ +MISSING = sentinel("MISSING") +print(MISSING) diff --git a/python315-sentinel-values/example_008.py b/python315-sentinel-values/example_008.py new file mode 100644 index 0000000000..a29a05abbc --- /dev/null +++ b/python315-sentinel-values/example_008.py @@ -0,0 +1,2 @@ +UNSET = sentinel("UNSET", repr="") +print(UNSET) diff --git a/python315-sentinel-values/example_009.py b/python315-sentinel-values/example_009.py new file mode 100644 index 0000000000..cf32b0cb48 --- /dev/null +++ b/python315-sentinel-values/example_009.py @@ -0,0 +1,10 @@ +# Both calls intentionally fail. Catch each error so you can see both. +try: + sentinel("UNSET", "") +except TypeError as error: + print(f"Expected TypeError: {error}") + +try: + sentinel(name="UNSET") +except TypeError as error: + print(f"Expected TypeError: {error}") diff --git a/python315-sentinel-values/example_010.py b/python315-sentinel-values/example_010.py new file mode 100644 index 0000000000..0b5192ccc6 --- /dev/null +++ b/python315-sentinel-values/example_010.py @@ -0,0 +1,7 @@ +MISSING = sentinel("MISSING") +print(bool(MISSING)) +print(hash(MISSING)) +config = {MISSING: "unset", "timeout": 30} +print(config[MISSING]) +print(MISSING in {MISSING, 1, 2}) +print(MISSING == sentinel("MISSING")) diff --git a/python315-sentinel-values/example_011.py b/python315-sentinel-values/example_011.py new file mode 100644 index 0000000000..7176a0f1f2 --- /dev/null +++ b/python315-sentinel-values/example_011.py @@ -0,0 +1 @@ +print(sentinel("STOP") is sentinel("STOP")) diff --git a/python315-sentinel-values/example_012.py b/python315-sentinel-values/example_012.py new file mode 100644 index 0000000000..f4227f7b36 --- /dev/null +++ b/python315-sentinel-values/example_012.py @@ -0,0 +1,6 @@ +A = sentinel("A") +B = sentinel("B") +print(type(A) is type(B)) +print(isinstance(A, sentinel)) +print(isinstance(B, sentinel)) +print(A is B) diff --git a/python315-sentinel-values/example_013.py b/python315-sentinel-values/example_013.py new file mode 100644 index 0000000000..13f41b3956 --- /dev/null +++ b/python315-sentinel-values/example_013.py @@ -0,0 +1,5 @@ +from copy import copy, deepcopy + +MISSING = sentinel("MISSING") +print(copy(MISSING) is MISSING) +print(deepcopy(MISSING) is MISSING) diff --git a/python315-sentinel-values/example_014.py b/python315-sentinel-values/example_014.py new file mode 100644 index 0000000000..ba5c26adfa --- /dev/null +++ b/python315-sentinel-values/example_014.py @@ -0,0 +1,11 @@ +import pickle +from config import Config, INHERIT, UNSET + +print(pickle.loads(pickle.dumps(UNSET)) is UNSET) +print(pickle.loads(pickle.dumps(Config.AUTO)) is Config.AUTO) +# These names intentionally don't match their module/class bindings. +for value in (INHERIT, Config.NO_LIMIT): + try: + pickle.dumps(value) + except pickle.PicklingError as error: + print(f"Expected PicklingError: {error}") diff --git a/python315-sentinel-values/example_015.py b/python315-sentinel-values/example_015.py new file mode 100644 index 0000000000..09d83cbeb8 --- /dev/null +++ b/python315-sentinel-values/example_015.py @@ -0,0 +1,6 @@ +import inspect + +from settings_legacy import get_setting + +help(get_setting) +print(inspect.signature(get_setting)) diff --git a/python315-sentinel-values/example_016.py b/python315-sentinel-values/example_016.py new file mode 100644 index 0000000000..1bdcbd0ac9 --- /dev/null +++ b/python315-sentinel-values/example_016.py @@ -0,0 +1,6 @@ +import inspect + +from settings_sentinel import get_setting + +help(get_setting) +print(inspect.signature(get_setting)) diff --git a/python315-sentinel-values/example_017.py b/python315-sentinel-values/example_017.py new file mode 100644 index 0000000000..caaca1f291 --- /dev/null +++ b/python315-sentinel-values/example_017.py @@ -0,0 +1,5 @@ +import typing +from settings_typed_legacy import get_setting + +hints = typing.get_type_hints(get_setting) +print(hints["default"]) diff --git a/python315-sentinel-values/example_018.py b/python315-sentinel-values/example_018.py new file mode 100644 index 0000000000..9b93d260f4 --- /dev/null +++ b/python315-sentinel-values/example_018.py @@ -0,0 +1,5 @@ +import typing +from settings_typed_sentinel import get_setting + +hints = typing.get_type_hints(get_setting) +print(hints["default"]) diff --git a/python315-sentinel-values/example_019.py b/python315-sentinel-values/example_019.py new file mode 100644 index 0000000000..0ff261a565 --- /dev/null +++ b/python315-sentinel-values/example_019.py @@ -0,0 +1,9 @@ +from profiles_legacy import apply_patch, parse_patch + +profile = { + "name": "J. Doe", + "bio": "Data scientist", + "email": "jane@example.com", +} +patch = parse_patch({"name": "jane", "bio": None}) +print(apply_patch(profile, patch)) diff --git a/python315-sentinel-values/example_020.py b/python315-sentinel-values/example_020.py new file mode 100644 index 0000000000..9378489e59 --- /dev/null +++ b/python315-sentinel-values/example_020.py @@ -0,0 +1,5 @@ +from copy import deepcopy +from profiles_legacy import _MISSING, parse_patch + +patch = parse_patch({"name": "jane", "bio": None}) +print(deepcopy(patch)["email"] is _MISSING) diff --git a/python315-sentinel-values/example_021.py b/python315-sentinel-values/example_021.py new file mode 100644 index 0000000000..dbaceb8076 --- /dev/null +++ b/python315-sentinel-values/example_021.py @@ -0,0 +1,5 @@ +import pickle +from profiles_legacy import _MISSING, parse_patch + +patch = parse_patch({"name": "jane", "bio": None}) +print(pickle.loads(pickle.dumps(patch))["email"] is _MISSING) diff --git a/python315-sentinel-values/example_022.py b/python315-sentinel-values/example_022.py new file mode 100644 index 0000000000..c048a52a73 --- /dev/null +++ b/python315-sentinel-values/example_022.py @@ -0,0 +1,7 @@ +import pickle +from copy import deepcopy +from profiles_sentinel import MISSING, parse_patch + +patch = parse_patch({"name": "jane", "bio": None}) +print(deepcopy(patch)["email"] is MISSING) +print(pickle.loads(pickle.dumps(patch))["email"] is MISSING) diff --git a/python315-sentinel-values/example_023.py b/python315-sentinel-values/example_023.py new file mode 100644 index 0000000000..17525c04b2 --- /dev/null +++ b/python315-sentinel-values/example_023.py @@ -0,0 +1,15 @@ +STOP = "done" + + +def producer(): + words = ["ready", "done", "pending"] # Simulate produced words + for word in words: + yield word + yield STOP + + +words_stream = producer() +for word in words_stream: + if word == STOP: + break + print(f"Processing '{word}'...") diff --git a/python315-sentinel-values/example_024.py b/python315-sentinel-values/example_024.py new file mode 100644 index 0000000000..db100c3a6a --- /dev/null +++ b/python315-sentinel-values/example_024.py @@ -0,0 +1,15 @@ +STOP = sentinel("STOP") + + +def producer(): + words = ["ready", "done", "pending"] + for word in words: + yield word + yield STOP + + +words_stream = producer() +for word in words_stream: + if word is STOP: + break + print(f"Processing '{word}'...") diff --git a/python315-sentinel-values/missing.py b/python315-sentinel-values/missing.py new file mode 100644 index 0000000000..a412ffd48a --- /dev/null +++ b/python315-sentinel-values/missing.py @@ -0,0 +1,21 @@ +class _MissingType: + def __init__(self, name): + self._name = name + + def __repr__(self): + return self._name + + def __copy__(self): + return self + + def __deepcopy__(self, memo): + return self + + def __reduce__(self): + return self._name + + def __eq__(self, other): + return self is other + + def __hash__(self): + return id(self) diff --git a/python315-sentinel-values/profiles_legacy.py b/python315-sentinel-values/profiles_legacy.py new file mode 100644 index 0000000000..871b64188f --- /dev/null +++ b/python315-sentinel-values/profiles_legacy.py @@ -0,0 +1,15 @@ +# Python 3.14 and older approach +_MISSING = object() + + +def parse_patch(form): + fields = ("name", "bio", "email") + return {field: form.get(field, _MISSING) for field in fields} + + +def apply_patch(profile, patch): + updated = dict(profile) + for field, value in patch.items(): + if value is not _MISSING: + updated[field] = value + return updated diff --git a/python315-sentinel-values/profiles_sentinel.py b/python315-sentinel-values/profiles_sentinel.py new file mode 100644 index 0000000000..4b35766688 --- /dev/null +++ b/python315-sentinel-values/profiles_sentinel.py @@ -0,0 +1,15 @@ +# Python 3.15 or newer +MISSING = sentinel("MISSING") + + +def parse_patch(form): + fields = ("name", "bio", "email") + return {field: form.get(field, MISSING) for field in fields} + + +def apply_patch(profile, patch): + updated = dict(profile) + for field, value in patch.items(): + if value is not MISSING: + updated[field] = value + return updated diff --git a/python315-sentinel-values/ruff.toml b/python315-sentinel-values/ruff.toml new file mode 100644 index 0000000000..daf3de5373 --- /dev/null +++ b/python315-sentinel-values/ruff.toml @@ -0,0 +1,4 @@ +extend = "../pyproject.toml" + +# The repository's pinned Ruff predates Python 3.15's sentinel built-in. +builtins = ["sentinel"] diff --git a/python315-sentinel-values/sentinel_type.py b/python315-sentinel-values/sentinel_type.py new file mode 100644 index 0000000000..cf8f34ac30 --- /dev/null +++ b/python315-sentinel-values/sentinel_type.py @@ -0,0 +1,2 @@ +# Python 3.15 or newer +print(sentinel) diff --git a/python315-sentinel-values/settings_legacy.py b/python315-sentinel-values/settings_legacy.py new file mode 100644 index 0000000000..5230518270 --- /dev/null +++ b/python315-sentinel-values/settings_legacy.py @@ -0,0 +1,14 @@ +# Python 3.14 and older approach +_NO_DEFAULT = object() + + +def get_setting(config, path, default=_NO_DEFAULT): + current = config + for part in path.split("."): + try: + current = current[part] + except (KeyError, TypeError): + if default is _NO_DEFAULT: + raise KeyError(path) from None + return default + return current diff --git a/python315-sentinel-values/settings_sentinel.py b/python315-sentinel-values/settings_sentinel.py new file mode 100644 index 0000000000..ecf7dfbfe3 --- /dev/null +++ b/python315-sentinel-values/settings_sentinel.py @@ -0,0 +1,14 @@ +# Python 3.15 or newer +NO_DEFAULT = sentinel("NO_DEFAULT") + + +def get_setting(config, path, default=NO_DEFAULT): + current = config + for part in path.split("."): + try: + current = current[part] + except (KeyError, TypeError): + if default is NO_DEFAULT: + raise KeyError(path) from None + return default + return current diff --git a/python315-sentinel-values/settings_typed_legacy.py b/python315-sentinel-values/settings_typed_legacy.py new file mode 100644 index 0000000000..f3fe1bfbbd --- /dev/null +++ b/python315-sentinel-values/settings_typed_legacy.py @@ -0,0 +1,18 @@ +# Python 3.14 and older approach, with type hints +_NO_DEFAULT = object() + + +def get_setting( + config: dict[str, object], + path: str, + default: object = _NO_DEFAULT, +) -> str | int: + current = config + for part in path.split("."): + try: + current = current[part] + except (KeyError, TypeError): + if default is _NO_DEFAULT: + raise KeyError(path) from None + return default + return current diff --git a/python315-sentinel-values/settings_typed_sentinel.py b/python315-sentinel-values/settings_typed_sentinel.py new file mode 100644 index 0000000000..7c36ec3786 --- /dev/null +++ b/python315-sentinel-values/settings_typed_sentinel.py @@ -0,0 +1,18 @@ +# Python 3.15 or newer, with type hints +NO_DEFAULT = sentinel("NO_DEFAULT") + + +def get_setting( + config: dict[str, object], + path: str, + default: str | int | NO_DEFAULT = NO_DEFAULT, +) -> str | int: + current = config + for part in path.split("."): + try: + current = current[part] + except (KeyError, TypeError): + if default is NO_DEFAULT: + raise KeyError(path) from None + return default + return current