diff --git a/python-313/README.md b/python-313/README.md index 4fba3e6257..5b99956f47 100644 --- a/python-313/README.md +++ b/python-313/README.md @@ -12,7 +12,7 @@ Note that for testing the free-threading and JIT features, you'll need to build You can learn more about Python 3.13's new features in the following Real Python tutorials: - +- [Python 3.13: Cool New Features for You to Try](https://realpython.com/python313-new-features/) - [Python 3.13 Preview: Free Threading and a JIT Compiler](https://realpython.com/python313-free-threading-jit/) - [Python 3.13 Preview: A Modern REPL](https://realpython.com/python313-repl) @@ -30,11 +30,28 @@ The following examples are used to demonstrate different features of the new REP - [`multiline_editing.py`](repl/multiline_editing.py) - [`power_factory.py](repl/power_factory.py) - [`guessing_game.py](repl/guessing_game.py) +- [`roll_dice.py`](repl/roll_dice.py) + +### Error messages + +Run the scripts in the `errors/` folder to see different error messages produced by Python 3.13. ### Free-Threading and JIT You need to enable a few build options to try out the free-threading and JIT features in Python 3.13. You can find more information in the dedicated [README file](free-threading-jit/README.md). +## Static typing + +Run the scripts in the `typing/` folder to try out the new static typing features. + +## Other features + +The following scripts illustrate other new features in Python 3.13: + +- [`replace.py`](replace.py): Use `copy.replace()` to update immutable data structures. +- [`paths.py`](paths.py) and [`music/`](music/): Glob patterns are more consistent. +- [`docstrings.py`](docstrings.py): Common leading whitespace in docstrings is stripped. + ## Authors - **Bartosz Zaczyński**, E-mail: [bartosz@realpython.com](bartosz@realpython.com) diff --git a/python-313/docstrings.py b/python-313/docstrings.py new file mode 100644 index 0000000000..3b25cc44ff --- /dev/null +++ b/python-313/docstrings.py @@ -0,0 +1,16 @@ +import dataclasses + + +@dataclasses.dataclass +class Person: + """Model a person with a name, location, and Python version.""" + + name: str + place: str + version: str + + +print(Person.__doc__) + +print(len(dataclasses.replace.__doc__)) +print(dataclasses.replace.__doc__) diff --git a/python-313/errors/inverse.py b/python-313/errors/inverse.py new file mode 100644 index 0000000000..5087c5e030 --- /dev/null +++ b/python-313/errors/inverse.py @@ -0,0 +1,5 @@ +def inverse(number): + return 1 / number + + +print(inverse(0)) diff --git a/python-313/errors/kwarg_suggest.py b/python-313/errors/kwarg_suggest.py new file mode 100644 index 0000000000..d5a99cfa89 --- /dev/null +++ b/python-313/errors/kwarg_suggest.py @@ -0,0 +1,4 @@ +numbers = [2, 0, 2, 4, 1, 0, 0, 1] + +# print(sorted(numbers, reversed=True)) +print(sorted(numbers, reverse=True)) diff --git a/python-313/errors/random.py b/python-313/errors/random.py new file mode 100644 index 0000000000..b129b13c10 --- /dev/null +++ b/python-313/errors/random.py @@ -0,0 +1,14 @@ +import random + +num_faces = 6 + +print("Hit enter to roll die (q to quit, number for # of faces) ") +while True: + roll = input() + if roll.lower().startswith("q"): + break + if roll.isnumeric(): + num_faces = int(roll) + + result = random.randint(1, num_faces) + print(f"Rolling a d{num_faces:<2d} - {result:2d}") diff --git a/python-313/music/opera/flower_duet.txt b/python-313/music/opera/flower_duet.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/opera/habanera.txt b/python-313/music/opera/habanera.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/opera/nabucco.txt b/python-313/music/opera/nabucco.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/rap/bedlam_13-13.txt b/python-313/music/rap/bedlam_13-13.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/music/rap/fight_the_power.txt b/python-313/music/rap/fight_the_power.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python-313/paths.py b/python-313/paths.py new file mode 100644 index 0000000000..d3ed8b8ddc --- /dev/null +++ b/python-313/paths.py @@ -0,0 +1,27 @@ +import glob +import re +from pathlib import Path + +print('\nUsing glob("*"):\n') +for path in Path("music").glob("*"): + print(" ", path) + +print('\nUsing glob("**"):\n') +for path in Path("music").glob("**"): + print(" ", path) + +print('\nUsing glob("**/*"):\n') +for path in Path("music").glob("**/*"): + print(" ", path) + +print('\nUsing glob("**/"):\n') +for path in Path("music").glob("**/"): + print(" ", path) + +print("\nglob.translate()\n") +pattern = glob.translate("music/**/*.txt") +print(pattern) + +print(re.match(pattern, "music/opera/flower_duet.txt")) +print(re.match(pattern, "music/progressive_rock/")) +print(re.match(pattern, "music/progressive_rock/fandango.txt")) diff --git a/python-313/repl/roll_dice.py b/python-313/repl/roll_dice.py new file mode 100644 index 0000000000..b129b13c10 --- /dev/null +++ b/python-313/repl/roll_dice.py @@ -0,0 +1,14 @@ +import random + +num_faces = 6 + +print("Hit enter to roll die (q to quit, number for # of faces) ") +while True: + roll = input() + if roll.lower().startswith("q"): + break + if roll.isnumeric(): + num_faces = int(roll) + + result = random.randint(1, num_faces) + print(f"Rolling a d{num_faces:<2d} - {result:2d}") diff --git a/python-313/replace.py b/python-313/replace.py new file mode 100644 index 0000000000..00b994bcc1 --- /dev/null +++ b/python-313/replace.py @@ -0,0 +1,23 @@ +import copy +from datetime import date +from typing import NamedTuple + + +class Person(NamedTuple): + name: str + place: str + version: str + + +person = Person(name="Geir Arne", place="Oslo", version="3.12") +person = Person(name=person.name, place=person.place, version="3.13") +print(person) + +today = date.today() +print(today) +print(today.replace(day=1)) +print(today.replace(month=12, day=24)) + +person = Person(name="Geir Arne", place="Oslo", version="3.12") +print(copy.replace(person, version="3.13")) +print(copy.replace(today, day=1)) diff --git a/python-313/typing/deprecations.py b/python-313/typing/deprecations.py new file mode 100644 index 0000000000..7fe8a8fa99 --- /dev/null +++ b/python-313/typing/deprecations.py @@ -0,0 +1,82 @@ +"""Demonstration of PEP 702: Marking deprecations using the type system + +The deprecations should be marked in PyCharm and VS Code. + +Use PyLance in VS Code by setting Python › Analysis: Type Checking Mode or run +the Pyright CLI: + + $ python -m pip install pyright + $ pyright --pythonversion 3.13 . + +Note that showing warnings with Pyright requires setting the reportDeprecated +option. This is done in the accompanying pyproject.toml. +""" + +from typing import overload +from warnings import deprecated + + +@deprecated("Use + instead of calling concatenate()") +def concatenate(first: str, second: str) -> str: + return first + second + + +@overload +@deprecated("add() is only supported for floats") +def add(x: int, y: int) -> int: ... +@overload +def add(x: float, y: float) -> float: ... + + +def add(x, y): + return x + y + + +class Version: + def __init__(self, major: int, minor: int = 0, patch: int = 0) -> None: + self.major = major + self.minor = minor + self.patch = patch + + @property + @deprecated("Use .patch instead") + def bugfix(self): + return self.patch + + def bump(self, part: str) -> None: + if part == "major": + self.major += 1 + self.minor = 0 + self.patch = 0 + elif part == "minor": + self.minor += 1 + self.patch = 0 + elif part == "patch": + self.patch += 1 + else: + raise ValueError("part must be 'major', 'minor', or 'patch'") + + @deprecated("Use .bump() instead") + def increase(self, part: str) -> None: + return self.bump(part) + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + +@deprecated("Use Version instead") +class VersionType: + def __init__(self, major: int, minor: int = 0, patch: int = 0) -> None: + self.major = major + self.minor = minor + self.patch = patch + + +concatenate("three", "thirteen") +add(3, 13) +VersionType(3, 13) + +version = Version(3, 13) +version.increase("patch") +print(version) +print(version.bugfix) diff --git a/python-313/typing/generic_queue.py b/python-313/typing/generic_queue.py new file mode 100644 index 0000000000..812c835971 --- /dev/null +++ b/python-313/typing/generic_queue.py @@ -0,0 +1,38 @@ +from collections import deque + + +class Queue[T]: + def __init__(self) -> None: + self.elements: deque[T] = deque() + + def push(self, element: T) -> None: + self.elements.append(element) + + def pop(self) -> T: + return self.elements.popleft() + + +# %% Python 3.13 +# +# class Queue[T=str]: +# def __init__(self) -> None: +# self.elements: deque[T] = deque() +# +# def push(self, element: T) -> None: +# self.elements.append(element) +# +# def pop(self) -> T: +# return self.elements.popleft() + +# %% Use the queue +# +string_queue = Queue() +integer_queue = Queue[int]() + +string_queue.push("three") +string_queue.push("thirteen") +print(string_queue.elements) + +integer_queue.push(3) +integer_queue.push(13) +print(integer_queue.elements) diff --git a/python-313/typing/pyproject.toml b/python-313/typing/pyproject.toml new file mode 100644 index 0000000000..be829bb0a3 --- /dev/null +++ b/python-313/typing/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pyright] +reportDeprecated = true diff --git a/python-313/typing/readonly.py b/python-313/typing/readonly.py new file mode 100644 index 0000000000..05ed0f92f2 --- /dev/null +++ b/python-313/typing/readonly.py @@ -0,0 +1,56 @@ +"""Demonstration of PEP 705: TypedDict: read-only items + +Use PyLance in VS Code by setting Python › Analysis: Type Checking Mode or run +the Pyright CLI: + + $ python -m pip install pyright $ pyright --pythonversion 3.13 . + +Extension of TypedDict: +https://realpython.com/python38-new-features/#more-precise-types +""" + +from typing import NotRequired, ReadOnly, TypedDict + +# %% Without ReadOnly + +# class Version(TypedDict): +# version: str +# release_year: NotRequired[int | None] + + +# class PythonVersion(TypedDict): +# version: str +# release_year: int + + +# %% Using ReadOnly +# +# Can only use PythonVersion as a Version if the differing fields are ReadOnly +class Version(TypedDict): + version: str + release_year: ReadOnly[NotRequired[int | None]] + + # Note that ReadOnly can be nested with other special forms in any order + # release_year: NotRequired[ReadOnly[int | None]] + + +class PythonVersion(TypedDict): + version: str + release_year: ReadOnly[int] + + +# %% Work with Version and PythonVersion +# +def get_version_info(ver: Version) -> str: + if "release_year" in ver: + return f"Version {ver['version']} released in {ver['release_year']}" + else: + return f"Version {ver['version']}" + + +py313 = PythonVersion(version="3.13", release_year=2024) + +# Alternative syntax, using TypedDict as an annotation +# py313: PythonVersion = {"version": "3.13", "release_year": 2024} + +print(get_version_info(py313)) diff --git a/python-313/typing/tree.py b/python-313/typing/tree.py new file mode 100644 index 0000000000..f0fb417dd4 --- /dev/null +++ b/python-313/typing/tree.py @@ -0,0 +1,34 @@ +from typing import TypeGuard + +type Tree = list[Tree | int] + + +def is_tree(obj: object) -> TypeGuard[Tree]: + return isinstance(obj, list) + + +def get_left_leaf_value(tree_or_leaf: Tree | int) -> int: + if is_tree(tree_or_leaf): + return get_left_leaf_value(tree_or_leaf[0]) + else: + return tree_or_leaf + + +# %% Python 3.13 +# +# from typing import TypeIs +# +# type Tree = list[Tree | int] +# +# def is_tree(obj: object) -> TypeIs[Tree]: +# return isinstance(obj, list) +# +# def get_left_leaf_value(tree_or_leaf: Tree | int) -> int: +# if is_tree(tree_or_leaf): +# return get_left_leaf_value(tree_or_leaf[0]) +# else: +# return tree_or_leaf + +# %% Use the tree +# +print(get_left_leaf_value([[[[3, 13], 12], 11], 10]))