Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add examples from Python 3.13: Cool New Features #587

Merged
merged 3 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
19 changes: 18 additions & 1 deletion python-313/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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)

Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions python-313/docstrings.py
Original file line number Diff line number Diff line change
@@ -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__)
5 changes: 5 additions & 0 deletions python-313/errors/inverse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def inverse(number):
return 1 / number


print(inverse(0))
4 changes: 4 additions & 0 deletions python-313/errors/kwarg_suggest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
numbers = [2, 0, 2, 4, 1, 0, 0, 1]

# print(sorted(numbers, reversed=True))
print(sorted(numbers, reverse=True))
14 changes: 14 additions & 0 deletions python-313/errors/random.py
Original file line number Diff line number Diff line change
@@ -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}")
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
27 changes: 27 additions & 0 deletions python-313/paths.py
Original file line number Diff line number Diff line change
@@ -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"))
14 changes: 14 additions & 0 deletions python-313/repl/roll_dice.py
Original file line number Diff line number Diff line change
@@ -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}")
23 changes: 23 additions & 0 deletions python-313/replace.py
Original file line number Diff line number Diff line change
@@ -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))
79 changes: 79 additions & 0 deletions python-313/typing/deprecations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Demonstration of PEP 702: Marking deprecations using the type system

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 .
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these two commands be on separate lines?


Note that showing warnings requires setting the reportDeprecated option in
Pyright. This is done in 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)
Comment on lines +75 to +82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyCharm is smart enough to recognize the new decorator and uses strike-through formatting:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Yes, that looks quite similar to VS Code. I'll add PyCharm to the top comment.

image

38 changes: 38 additions & 0 deletions python-313/typing/generic_queue.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions python-313/typing/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tool.pyright]
reportDeprecated = true
48 changes: 48 additions & 0 deletions python-313/typing/readonly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""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 .
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these two commands be on separate lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yes, definitely. It seems that the automatic comment formatter I ran is a bit eager.


Extension of TypedDict:
https://realpython.com/python38-new-features/#more-precise-types
"""

from typing import NotRequired, ReadOnly, TypedDict

# class Version(TypedDict):
# version: str
# release_year: NotRequired[int | None]


# class PythonVersion(TypedDict):
# version: str
# release_year: int


class Version(TypedDict):
version: ReadOnly[str]
release_year: ReadOnly[NotRequired[int | None]]
bzaczynski marked this conversation as resolved.
Show resolved Hide resolved


class PythonVersion(TypedDict):
version: ReadOnly[str]
release_year: ReadOnly[int]


py313 = PythonVersion(version="3.13", release_year=2024)

# Alternative syntax, using TypedDict as an annotation
# py313: PythonVersion = {"version": "3.13", "release_year": 2024}


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']}"


# Only allowed to use PythonVersion instead of Version if the fields are ReadOnly
print(get_version_info(py313))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this counter-intuitive since the Version and PythonVersion classes specify separate types that don't belong to the same type hierarchy. They share the same structure, so they're essentially the same type by means of static duck typing, but they don't explicitly define a protocol. I guess, both are dicts with the same set of key-value pairs, so that should be enough for the type checker to treat them as equals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I believe that TypedDict is all about structural typing. The PEP talks a bit about how the capabilities of ReadOnly can be done with some complicated use of protocols instead.

I haven't quite internalized the rules for when one TypedDict can pass as the other, but everytime I sit down and think for a while, it seems to add up 🙈

34 changes: 34 additions & 0 deletions python-313/typing/tree.py
Original file line number Diff line number Diff line change
@@ -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]))
Loading