Skip to content

Commit e08c5f2

Browse files
committed
feat: support Python 3.9 and 3.10
Lower the minimum supported Python from 3.11 to 3.9 to cover stock macOS and Debian/Ubuntu interpreters. - Fall back to the tomli backport when tomllib is unavailable (< 3.11), in both the manifest loader and the solution-coverage test. - Add 'from __future__ import annotations' to exercise.py so the 'Path | None' dataclass fields are not evaluated at class-definition time on < 3.10. - type_hints4 / type_hints8 teach PEP 604 'int | None' unions: add the future import to the exercise scaffold and reference answer, and assert on the textual annotation in the checks so they pass on 3.9-3.13. - itertools8 teaches itertools.pairwise (3.10+): provide the documented tee-based fallback in the scaffold and answer; the learner's task is unchanged. - Bump version to 0.2.0, add 3.9/3.10 classifiers, and declare the conditional tomli dependency.
1 parent 44ec7bc commit e08c5f2

13 files changed

Lines changed: 81 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
All notable changes to this project are documented here. Pylings follows
44
Semantic Versioning.
55

6+
## [0.2.0] - 2026-05-30
7+
8+
### Added
9+
10+
- Support for Python 3.9 and 3.10 (minimum was 3.11), broadening
11+
compatibility with stock macOS and Debian/Ubuntu interpreters.
12+
13+
### Changed
14+
15+
- The `type_hints4`, `type_hints8`, and `itertools8` exercises carry a small
16+
forward-compatibility shim (`from __future__ import annotations` and a
17+
`tee`-based `pairwise` fallback) so their modern syntax and stdlib usage run
18+
on Python 3.9 and 3.10. The learner's task is unchanged.
19+
20+
### Fixed
21+
22+
- Manifest loading now falls back to the `tomli` backport on Python < 3.11,
23+
where `tomllib` is not available in the standard library. Previously the
24+
package would install but crash on launch under older interpreters.
25+
626
## [0.1.0] - 2026-05-25
727

828
### Added

checks/type_hints/type_hints4.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import types
2-
31
hints = find_index.__annotations__
4-
assert hints.get("items") == list[str], "items should be annotated as list[str]"
5-
assert hints.get("target") is str, "target should be annotated as str"
6-
ret = hints.get("return")
7-
assert isinstance(ret, types.UnionType) and ret == (int | None), \
8-
"return type should be int | None"
2+
assert hints.get("items") == "list[str]", "items should be annotated as list[str]"
3+
assert hints.get("target") == "str", "target should be annotated as str"
4+
assert hints.get("return") == "int | None", "return type should be int | None"
95
print("type_hints4 ✓")

checks/type_hints/type_hints8.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import types
2-
31
hints = summarise.__annotations__
4-
assert hints.get("records") == list[dict[str, int]], \
2+
assert hints.get("records") == "list[dict[str, int]]", \
53
"records should be annotated as list[dict[str, int]]"
6-
assert hints.get("key") is str, "key should be annotated as str"
7-
default_hint = hints.get("default")
8-
assert isinstance(default_hint, types.UnionType) and default_hint == (int | None), \
4+
assert hints.get("key") == "str", "key should be annotated as str"
5+
assert hints.get("default") == "int | None", \
96
"default should be annotated as int | None"
10-
ret = hints.get("return")
11-
assert ret == dict[str, int | None], \
7+
assert hints.get("return") == "dict[str, int | None]", \
128
"return type should be dict[str, int | None]"
139
print("type_hints8 ✓")

exercises/itertools/itertools8.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
#
44
# Goal: Flatten batches, then collect every pair of adjacent flattened values.
55

6-
from itertools import pairwise
6+
try:
7+
from itertools import pairwise
8+
except ImportError: # Python < 3.10
9+
from itertools import tee
10+
11+
def pairwise(iterable):
12+
a, b = tee(iterable)
13+
next(b, None)
14+
return zip(a, b)
715

816
batches = [[1, 2], [3], [4, 5]]
917
flattened = [item for batch in batches for item in batch]

exercises/type_hints/type_hints4.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
#
99
# The signature should read:
1010
# def find_index(items: list[str], target: str) -> int | None:
11+
#
12+
# Keep the `from __future__ import annotations` line below: it lets the modern
13+
# `int | None` syntax work on Python 3.9 and 3.10 as well.
14+
from __future__ import annotations
15+
1116

1217
def find_index(items, target):
1318
try:

exercises/type_hints/type_hints8.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
# key: str,
1414
# default: int | None,
1515
# ) -> dict[str, int | None]:
16+
#
17+
# Keep the `from __future__ import annotations` line below: it lets the modern
18+
# `int | None` syntax work on Python 3.9 and 3.10 as well.
19+
from __future__ import annotations
20+
1621

1722
def summarise(records, key, default):
1823
return {

pylings/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66
from pathlib import Path
77

8-
__version__ = "0.1.0"
8+
__version__ = "0.2.0"
99

1010

1111
def _build_parser() -> argparse.ArgumentParser:

pylings/core/exercise.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# pylings/core/exercise.py
2+
from __future__ import annotations
23

34
from dataclasses import dataclass
45
from pathlib import Path

pylings/core/manifest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# pylings/core/manifest.py
22
from __future__ import annotations
33

4-
import tomllib
4+
try:
5+
import tomllib
6+
except ModuleNotFoundError: # Python < 3.11
7+
import tomli as tomllib
58
from dataclasses import dataclass
69
from pathlib import Path
710

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "python-learnings"
7-
version = "0.1.0"
7+
version = "0.2.0"
88
description = "Python learnings, Rustlings-style, in a terminal TUI."
99
readme = "Readme.md"
10-
requires-python = ">=3.11"
10+
requires-python = ">=3.9"
1111
license = { text = "MIT" }
1212
keywords = [
1313
"python",
@@ -30,6 +30,8 @@ classifiers = [
3030
"License :: OSI Approved :: MIT License",
3131
"Operating System :: OS Independent",
3232
"Programming Language :: Python :: 3",
33+
"Programming Language :: Python :: 3.9",
34+
"Programming Language :: Python :: 3.10",
3335
"Programming Language :: Python :: 3.11",
3436
"Programming Language :: Python :: 3.12",
3537
"Programming Language :: Python :: 3.13",
@@ -40,6 +42,7 @@ classifiers = [
4042
dependencies = [
4143
"textual[syntax]>=8.0.0",
4244
"watchdog>=6.0.0",
45+
"tomli>=1.1.0; python_version < '3.11'",
4346
]
4447

4548
[project.urls]

0 commit comments

Comments
 (0)