Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions python315-sentinel-values/README.md
Original file line number Diff line number Diff line change
@@ -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/)
8 changes: 8 additions & 0 deletions python315-sentinel-values/config.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions python315-sentinel-values/example_001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("Hello, World!".find("z"))
9 changes: 9 additions & 0 deletions python315-sentinel-values/example_002.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions python315-sentinel-values/example_003.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions python315-sentinel-values/example_004.py
Original file line number Diff line number Diff line change
@@ -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")])
11 changes: 11 additions & 0 deletions python315-sentinel-values/example_005.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from copy import deepcopy


class _MissingType:
def __repr__(self):
return "MISSING"


MISSING = _MissingType()
print(MISSING)
print(deepcopy(MISSING) is MISSING)
8 changes: 8 additions & 0 deletions python315-sentinel-values/example_006.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import enum


class MissingType(enum.Enum):
MISSING = "MISSING"


print(MissingType.MISSING)
2 changes: 2 additions & 0 deletions python315-sentinel-values/example_007.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MISSING = sentinel("MISSING")
print(MISSING)
2 changes: 2 additions & 0 deletions python315-sentinel-values/example_008.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
UNSET = sentinel("UNSET", repr="<unset>")
print(UNSET)
10 changes: 10 additions & 0 deletions python315-sentinel-values/example_009.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Both calls intentionally fail. Catch each error so you can see both.
try:
sentinel("UNSET", "<unset>")
except TypeError as error:
print(f"Expected TypeError: {error}")

try:
sentinel(name="UNSET")
except TypeError as error:
print(f"Expected TypeError: {error}")
7 changes: 7 additions & 0 deletions python315-sentinel-values/example_010.py
Original file line number Diff line number Diff line change
@@ -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"))
1 change: 1 addition & 0 deletions python315-sentinel-values/example_011.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print(sentinel("STOP") is sentinel("STOP"))
6 changes: 6 additions & 0 deletions python315-sentinel-values/example_012.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions python315-sentinel-values/example_013.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from copy import copy, deepcopy

MISSING = sentinel("MISSING")
print(copy(MISSING) is MISSING)
print(deepcopy(MISSING) is MISSING)
11 changes: 11 additions & 0 deletions python315-sentinel-values/example_014.py
Original file line number Diff line number Diff line change
@@ -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}")
6 changes: 6 additions & 0 deletions python315-sentinel-values/example_015.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import inspect

from settings_legacy import get_setting

help(get_setting)
print(inspect.signature(get_setting))
6 changes: 6 additions & 0 deletions python315-sentinel-values/example_016.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import inspect

from settings_sentinel import get_setting

help(get_setting)
print(inspect.signature(get_setting))
5 changes: 5 additions & 0 deletions python315-sentinel-values/example_017.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import typing
from settings_typed_legacy import get_setting

hints = typing.get_type_hints(get_setting)
print(hints["default"])
5 changes: 5 additions & 0 deletions python315-sentinel-values/example_018.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import typing
from settings_typed_sentinel import get_setting

hints = typing.get_type_hints(get_setting)
print(hints["default"])
9 changes: 9 additions & 0 deletions python315-sentinel-values/example_019.py
Original file line number Diff line number Diff line change
@@ -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))
5 changes: 5 additions & 0 deletions python315-sentinel-values/example_020.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions python315-sentinel-values/example_021.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions python315-sentinel-values/example_022.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions python315-sentinel-values/example_023.py
Original file line number Diff line number Diff line change
@@ -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}'...")
15 changes: 15 additions & 0 deletions python315-sentinel-values/example_024.py
Original file line number Diff line number Diff line change
@@ -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}'...")
21 changes: 21 additions & 0 deletions python315-sentinel-values/missing.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions python315-sentinel-values/profiles_legacy.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions python315-sentinel-values/profiles_sentinel.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions python315-sentinel-values/ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
extend = "../pyproject.toml"

# The repository's pinned Ruff predates Python 3.15's sentinel built-in.
builtins = ["sentinel"]
2 changes: 2 additions & 0 deletions python315-sentinel-values/sentinel_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Python 3.15 or newer
print(sentinel)
14 changes: 14 additions & 0 deletions python315-sentinel-values/settings_legacy.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions python315-sentinel-values/settings_sentinel.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions python315-sentinel-values/settings_typed_legacy.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions python315-sentinel-values/settings_typed_sentinel.py
Original file line number Diff line number Diff line change
@@ -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
Loading