Skip to content

Commit

Permalink
Add 3.12 examples from the typing preview (#438)
Browse files Browse the repository at this point in the history
* Add 3.12 examples from the typing preview

* Use Python 3.12 for linting

* Upgrade Flake8

* Downgrade back to Python 3.11 for linting

* Reset cache

* Combine 3.11 and 3.12 code in the same file

* Update README

* README LE Static Typing

---------

Co-authored-by: KateFinegan <[email protected]>
  • Loading branch information
gahjelle and KateFinegan authored Sep 25, 2023
1 parent 0e7f7a4 commit e06ae43
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 5 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
fail-fast: false
matrix:
include:
- {name: Linux311, python: '3.11.0-rc.1', os: ubuntu-latest}
- {name: Linux311, python: '3.11.5', os: ubuntu-latest}
steps:
- name: Check out repository
uses: actions/checkout@v2
Expand All @@ -33,9 +33,9 @@ jobs:
uses: actions/cache@v3
with:
path: ./venv
key: ${{ matrix.name }}-pip-${{ hashFiles('requirements.txt') }}
key: ${{ matrix.name }}-v1-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ matrix.name }}-pip-
${{ matrix.name }}-v1-pip-
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
Expand Down
35 changes: 34 additions & 1 deletion python-312/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ You can learn more about Python 3.12's new features in the following Real Python
- [Python 3.12 Preview: Ever Better Error Messages](https://realpython.com/python312-error-messages/)
- [Python 3.12 Preview: Support For the Linux `perf` Profiler](https://realpython.com/python312-perf-profiler/)
- [Python 3.12 Preview: More Intuitive and Consistent F-Strings](https://realpython.com/python312-f-strings/)
- [Python 3.12 Preview: Subinterpreters](https://realpython.com/python312-subinterpreters/)
- [Python 3.12 Preview: Static Typing Improvements](https://realpython.com/python312-typing/)

You'll find examples from all these tutorials in this repository.
You'll find examples from these tutorials in this repository.

## Examples

Expand Down Expand Up @@ -136,6 +138,37 @@ Pythonista!

In this example, you can see how the new implementation of f-strings allows you to include backslashes in embedded expressions. This wasn't possible with f-strings in earlier versions of Python.

### Static Typing Improvements

You'll find all static typing examples inside the [`typing/`](typing/) directory. You should install the Pyright type checker from PyPI:

```console
$ python -m pip install pyright
```

You can then run type checks by running `pyright`. For some features, you need to specify `--pythonversion 3.12`.

#### Type Variables and Generic Classes, Functions, and Type Aliases

You can find comparisons between the old and the new syntax for type variables in the following files, with the new 3.12 syntax shown in the commented part of the code:

- [`generic_queue.py`](typing/generic_queue.py)
- [`list_helpers.py`](typing/list_helpers.py)
- [`concatenation.py`](typing/concatenation.py)
- [`inspect_string.py`](typing/inspect_string.py)
- [`deck.py`](typing/deck.py)
- [`alias.py`](typing/alias.py)

Additionally, [`typed_queue.py`](typing/typed_queue.py) shows the implementation of typed queues without using type variables.

#### Modeling Inheritance With `@override`

The file [`quiz.py`](typing/quiz.py) shows how to use the new `@override` decorator. In addition to the code in the tutorial, this file includes support for reading questions from files. This is done to show that `@override` works well together with other decorators like `@classmethod`.

#### Annotating `**kwargs` With Typed Dictionaries

The file [`options.py`](typing/options.py) shows how you can use a typed dictionary to annotate variable keyword arguments.

## Authors

- **Martin Breuss**, E-mail: [[email protected]]([email protected])
Expand Down
14 changes: 14 additions & 0 deletions python-312/typing/alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import TypeAlias, TypeVar

T = TypeVar("T")

Ordered: TypeAlias = list[T] | tuple[T, ...]

numbers: Ordered[int] = (1, 2, 3)


# %% Python 3.12

# type Ordered[T] = list[T] | tuple[T, ...]
#
# numbers: Ordered[int] = (1, 2, 3)
13 changes: 13 additions & 0 deletions python-312/typing/concatenation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import TypeVar

T = TypeVar("T", str, bytes)


def concatenate(first: T, second: T) -> T:
return first + second


# %% Python 3.12

# def concatenate[T: (str, bytes)](first: T, second: T) -> T:
# return first + second
18 changes: 18 additions & 0 deletions python-312/typing/deck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import random
from typing import TypeAlias

CardDeck: TypeAlias = list[tuple[str, int]]


def shuffle(deck: CardDeck) -> CardDeck:
return random.sample(deck, k=len(deck))


# %% Python 3.12

# import random
#
# type CardDeck = list[tuple[str, int]]
#
# def shuffle(deck: CardDeck) -> CardDeck:
# return random.sample(deck, k=len(deck))
31 changes: 31 additions & 0 deletions python-312/typing/generic_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from collections import deque
from typing import Generic, TypeVar

T = TypeVar("T")


class Queue(Generic[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.12

# 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()
25 changes: 25 additions & 0 deletions python-312/typing/inspect_string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import TypeVar

S = TypeVar("S", bound=str)


class Words(str):
def __len__(self):
return len(self.split())


def inspect(text: S) -> S:
print(f"'{text.upper()}' has length {len(text)}")
return text


# %% Python 3.12

# class Words(str):
# def __len__(self):
# return len(self.split())
#
#
# def inspect[S: str](text: S) -> S:
# print(f"'{text.upper()}' has length {len(text)}")
# return text
15 changes: 15 additions & 0 deletions python-312/typing/list_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import TypeVar

T = TypeVar("T")


def push_and_pop(elements: list[T], element: T) -> T:
elements.append(element)
return elements.pop(0)


# %% Python 3.12

# def push_and_pop[T](elements: list[T], element: T) -> T:
# elements.append(element)
# return elements.pop(0)
34 changes: 34 additions & 0 deletions python-312/typing/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Required, TypedDict, Unpack


class Options(TypedDict, total=False):
line_width: int
level: Required[str]
propagate: bool


def show_options(program_name: str, **kwargs: Unpack[Options]) -> None:
print(program_name.upper())
for option, value in kwargs.items():
print(f"{option:<15} {value}")


def show_options_explicit(
program_name: str,
*,
level: str,
line_width: int | None = None,
propagate: bool | None = None,
) -> None:
options = {
"line_width": line_width,
"level": level,
"propagate": propagate,
}
print(program_name.upper())
for option, value in options.items():
if value is not None:
print(f"{option:<15} {value}")


show_options("logger", line_width=80, level="INFO", propagate=False)
5 changes: 5 additions & 0 deletions python-312/typing/oslo.question
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
In which country is Oslo the capital?
Norway
Sweden
Ireland
Canada
2 changes: 2 additions & 0 deletions python-312/typing/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tool.pyright]
# reportImplicitOverride = true
72 changes: 72 additions & 0 deletions python-312/typing/quiz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pathlib
import random
from dataclasses import dataclass
from string import ascii_lowercase
from typing import Self, override


@dataclass
class Question:
question: str
answer: str

@classmethod
def from_file(cls, path: pathlib.Path) -> Self:
question, answer, *_ = path.read_text(encoding="utf-8").split("\n")
return cls(question, answer)

def ask(self) -> bool:
answer = input(f"\n{self.question} ")
return answer == self.answer


@dataclass
class MultipleChoiceQuestion(Question):
distractors: list[str]

@classmethod
@override
def from_file(cls, path: pathlib.Path) -> Self:
question, answer, *distractors = (
path.read_text(encoding="utf-8").strip().split("\n")
)
return cls(question, answer, distractors)

@override
def ask(self) -> bool:
print(f"\n{self.question}")

alternatives = random.sample(
self.distractors + [self.answer], k=len(self.distractors) + 1
)
labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
for label, alternative in labeled_alternatives.items():
print(f" {label}) {alternative}", end="")

answer = input("\n\nChoice? ")
return labeled_alternatives.get(answer) == self.answer


questions = [
Question("Who created Python?", "Guido van Rossum"),
MultipleChoiceQuestion(
"What's a PEP?",
"A Python Enhancement Proposal",
distractors=[
"A Pretty Exciting Policy",
"A Preciously Evolved Python",
"A Potentially Epic Prize",
],
),
MultipleChoiceQuestion.from_file(pathlib.Path("oslo.question")),
]

score = 0
for question in random.sample(questions, k=len(questions)):
if question.ask():
score += 1
print("Yes, that's correct!")
else:
print(f"No, the answer is '{question.answer}'")

print(f"\nYou got {score} out of {len(questions)} correct")
23 changes: 23 additions & 0 deletions python-312/typing/typed_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from collections import deque


class IntegerQueue:
def __init__(self) -> None:
self.elements: deque[int] = deque()

def push(self, element: int) -> None:
self.elements.append(element)

def pop(self) -> int:
return self.elements.popleft()


class StringQueue:
def __init__(self) -> None:
self.elements: deque[str] = deque()

def push(self, element: str) -> None:
self.elements.append(element)

def pop(self) -> str:
return self.elements.popleft()
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
black[jupyter]==22.6.0
flake8==5.0.4
flake8==6.1.0

0 comments on commit e06ae43

Please sign in to comment.