Skip to content
Closed
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
12 changes: 9 additions & 3 deletions checks/enums/enums1.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
assert Color.RED.value == "red"
assert Color.BLUE.value == "blue"
assert favorite is Color.RED
assert Color.RED.value == "red", (
f"Color.RED.value should be 'red', got {Color.RED.value!r}"
)
assert Color.BLUE.value == "blue", (
f"Color.BLUE.value should be 'blue', got {Color.BLUE.value!r}"
)
assert favorite is Color.RED, (
f"favorite should be Color.RED, got {favorite!r}"
)
print("enums1 ok")
10 changes: 8 additions & 2 deletions checks/enums/enums2.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
assert [member.name for member in Status] == ["TODO", "DOING", "DONE"]
assert [member.value for member in Status] == [1, 2, 3]
assert [member.name for member in Status] == ["TODO", "DOING", "DONE"], (
"Status names should be ['TODO', 'DOING', 'DONE'], got "
f"{[member.name for member in Status]!r}"
)
assert [member.value for member in Status] == [1, 2, 3], (
f"Status values should be [1, 2, 3], got "
f"{[member.value for member in Status]!r}"
)
print("enums2 ok")
8 changes: 6 additions & 2 deletions checks/enums/enums3.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
assert selected_name == "HIGH"
assert selected_value == 5
assert selected_name == "HIGH", (
f"selected_name should be 'HIGH', got {selected_name!r}"
)
assert selected_value == 5, (
f"selected_value should be 5, got {selected_value!r}"
)
print("enums3 ok")
4 changes: 3 additions & 1 deletion checks/enums/enums4.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
assert size_codes == ["S", "M", "L"]
assert size_codes == ["S", "M", "L"], (
f"size_codes should be ['S', 'M', 'L'], got {size_codes!r}"
)
print("enums4 ok")
12 changes: 9 additions & 3 deletions checks/enums/enums5.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
assert is_terminal(TicketState.OPEN) is False
assert is_terminal(TicketState.CLOSED) is True
assert is_terminal(TicketState.CANCELLED) is True
assert is_terminal(TicketState.OPEN) is False, (
"is_terminal(TicketState.OPEN) should be False"
)
assert is_terminal(TicketState.CLOSED) is True, (
"is_terminal(TicketState.CLOSED) should be True"
)
assert is_terminal(TicketState.CANCELLED) is True, (
"is_terminal(TicketState.CANCELLED) should be True"
)
print("enums5 ok")
8 changes: 6 additions & 2 deletions checks/enums/enums6.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
assert Direction.NORTH.delta() == (0, -1)
assert Direction.EAST.delta() == (1, 0)
assert Direction.NORTH.delta() == (0, -1), (
"Direction.NORTH.delta() should return (0, -1)"
)
assert Direction.EAST.delta() == (1, 0), (
"Direction.EAST.delta() should return (1, 0)"
)
print("enums6 ok")
9 changes: 5 additions & 4 deletions checks/generators/generators1.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
assert list(count_up_to(4)) == [1, 2, 3, 4]
assert list(count_up_to(1)) == [1]
assert list(count_up_to(0)) == []
print("generators1 ✓")
# generators1.py
assert list(count_up_to(4)) == [1, 2, 3, 4], "Expected list(count_up_to(4)) to be [1, 2, 3, 4]"
assert list(count_up_to(1)) == [1], "Expected list(count_up_to(1)) to be [1]"
assert list(count_up_to(0)) == [], "Expected list(count_up_to(0)) to be [] (no elements when limit is 0)"
print("generators1 ✓")
14 changes: 7 additions & 7 deletions checks/generators/generators10.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
assert list(running_total([1, 2, 3, 4])) == [1, 3, 6, 10]
assert list(running_total([10, -3, 7])) == [10, 7, 14]
assert list(running_total([])) == []
assert list(running_total([5])) == [5]
# Works as a streaming pipeline over a generator input.
assert list(running_total(x * x for x in range(1, 5))) == [1, 5, 14, 30]
print("generators10 ✓")
# generators10.py
assert list(running_total([1, 2, 3, 4])) == [1, 3, 6, 10], "Expected list(running_total([1, 2, 3, 4])) to be [1, 3, 6, 10]"
assert list(running_total([10, -3, 7])) == [10, 7, 14], "Expected list(running_total([10, -3, 7])) to be [10, 7, 14]"
assert list(running_total([])) == [], "Expected list(running_total([])) to be [] (no elements to sum)"
assert list(running_total([5])) == [5], "Expected list(running_total([5])) to be [5]"
assert list(running_total(x * x for x in range(1, 5))) == [1, 5, 14, 30], "Expected running_total over a generator input (1, 4, 9, 16) to yield [1, 5, 14, 30]"
print("generators10 ✓")
7 changes: 4 additions & 3 deletions checks/generators/generators3.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# generators3.py — first assert already has a message, leave it exactly as-is
assert collected == [2, 4, 6, 8], f"expected [2, 4, 6, 8], got {collected}"
assert list(evens_up_to(5)) == [2, 4]
assert list(evens_up_to(1)) == []
print("generators3 ✓")
assert list(evens_up_to(5)) == [2, 4], "Expected list(evens_up_to(5)) to be [2, 4]"
assert list(evens_up_to(1)) == [], "Expected list(evens_up_to(1)) to be [] (no even numbers \u2264 1)"
print("generators3 ✓")
8 changes: 4 additions & 4 deletions checks/generators/generators6.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# generators6.py — first assert unchanged; big/1000 are plain variables, safe to echo
assert first_ten == list(range(1, 11)), (
f"expected [1..10], got {first_ten}"
)
# Confirm the generator really is infinite: pull 1000 values.
big = list(itertools.islice(natural_numbers(), 1000))
assert len(big) == 1000
assert big[-1] == 1000
print("generators6 ✓")
assert len(big) == 1000, f"Expected 1000 values pulled from natural_numbers(), got {len(big)}"
assert big[-1] == 1000, f"Expected the 1000th value to be 1000, got {big[-1]!r}"
print("generators6 ✓")
8 changes: 4 additions & 4 deletions checks/generators/generators7.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# generators7.py — first assert unchanged
import itertools

first8 = list(itertools.islice(fibonacci(), 8))
assert first8 == [0, 1, 1, 2, 3, 5, 8, 13], (
f"expected [0, 1, 1, 2, 3, 5, 8, 13], got {first8}"
)
# Confirm more terms keep coming.
first15 = list(itertools.islice(fibonacci(), 15))
assert len(first15) == 15
assert first15[-1] == 377
print("generators7 ✓")
assert len(first15) == 15, f"Expected 15 terms from fibonacci(), got {len(first15)}"
assert first15[-1] == 377, f"Expected the 15th Fibonacci term to be 377, got {first15[-1]!r}"
print("generators7 ✓")
14 changes: 7 additions & 7 deletions checks/generators/generators8.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
assert list(Countdown(3)) == [3, 2, 1]
assert list(Countdown(1)) == [1]
assert list(Countdown(5)) == [5, 4, 3, 2, 1]
# Should be re-iterable: a fresh iteration each time.
# generators8.py — Countdown is the class under test, no re-invocation in messages
assert list(Countdown(3)) == [3, 2, 1], "Expected list(Countdown(3)) to be [3, 2, 1]"
assert list(Countdown(1)) == [1], "Expected list(Countdown(1)) to be [1]"
assert list(Countdown(5)) == [5, 4, 3, 2, 1], "Expected list(Countdown(5)) to be [5, 4, 3, 2, 1]"
cd = Countdown(3)
assert list(cd) == [3, 2, 1]
assert list(cd) == [3, 2, 1]
print("generators8 ✓")
assert list(cd) == [3, 2, 1], "Expected first iteration over cd to be [3, 2, 1]"
assert list(cd) == [3, 2, 1], "Expected second iteration over cd to also be [3, 2, 1] \u2014 Countdown should be re-iterable, not exhausted after one pass"
print("generators8 ✓")
14 changes: 7 additions & 7 deletions checks/generators/generators9.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
assert list(Counter(4)) == [0, 1, 2, 3]
assert list(Counter(1)) == [0]
assert list(Counter(0)) == []
# StopIteration must be raised at the right moment.
# generators9.py
assert list(Counter(4)) == [0, 1, 2, 3], "Expected list(Counter(4)) to be [0, 1, 2, 3]"
assert list(Counter(1)) == [0], "Expected list(Counter(1)) to be [0]"
assert list(Counter(0)) == [], "Expected list(Counter(0)) to be [] (no elements when stop is 0)"
c = Counter(2)
assert next(c) == 0
assert next(c) == 1
assert next(c) == 0, "Expected the first value from Counter(2) to be 0"
assert next(c) == 1, "Expected the second value from Counter(2) to be 1"
try:
next(c)
raise AssertionError("expected StopIteration after stop elements")
except StopIteration:
pass
print("generators9 ✓")
print("generators9 ✓")
10 changes: 9 additions & 1 deletion checks/itertools/itertools3.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
assert coordinates == [(0, "low"), (0, "high"), (1, "low"), (1, "high")]
assert coordinates == [
(0, "low"),
(0, "high"),
(1, "low"),
(1, "high"),
], (
"coordinates should contain every (x, y) pair from xs and ys, "
f"got {coordinates!r}"
)
print("itertools3 ok")
8 changes: 7 additions & 1 deletion checks/itertools/itertools4.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
assert grouped == {"fruit": ["apple", "pear"], "veg": ["carrot"]}
assert grouped == {
"fruit": ["apple", "pear"],
"veg": ["carrot"],
}, (
"grouped should map each category to its item names, "
f"got {grouped!r}"
)
print("itertools4 ok")
5 changes: 4 additions & 1 deletion checks/itertools/itertools5.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
assert running_totals == [3, 7, 12, 18]
assert running_totals == [3, 7, 12, 18], (
"running_totals should contain cumulative sums [3, 7, 12, 18], "
f"got {running_totals!r}"
)
print("itertools5 ok")
5 changes: 4 additions & 1 deletion checks/itertools/itertools6.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
assert repeated == ["red", "blue", "red", "blue", "red", "blue"]
assert repeated == ["red", "blue", "red", "blue", "red", "blue"], (
"repeated should cycle through colors for six values, "
f"got {repeated!r}"
)
print("itertools6 ok")
5 changes: 4 additions & 1 deletion checks/itertools/itertools7.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
assert pairs == [("a", 1), ("b", "?"), ("c", "?")]
assert pairs == [("a", 1), ("b", "?"), ("c", "?")], (
"pairs should fill missing right-side values with '?', "
f"got {pairs!r}"
)
print("itertools7 ok")
10 changes: 8 additions & 2 deletions checks/itertools/itertools8.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
assert flattened == [1, 2, 3, 4, 5]
assert adjacent_pairs == [(1, 2), (2, 3), (3, 4), (4, 5)]
assert flattened == [1, 2, 3, 4, 5], (
"flattened should contain all batch items in order, "
f"got {flattened!r}"
)
assert adjacent_pairs == [(1, 2), (2, 3), (3, 4), (4, 5)], (
"adjacent_pairs should contain each neighboring pair, "
f"got {adjacent_pairs!r}"
)
print("itertools8 ok")
4 changes: 2 additions & 2 deletions checks/oop_advanced/oop_advanced10.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
assert issubclass(Square, Shape)
assert Square(4).area() == 16
assert issubclass(Square, Shape), "Square should inherit from Shape"
assert Square(4).area() == 16, "Square(4).area() should be 16 (4 * 4)"
print("oop_advanced10 ok")
5 changes: 4 additions & 1 deletion checks/oop_advanced/oop_advanced11.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
product = Product("book", 12)
assert product.to_dict() == {"name": "book", "price": 12}
assert product.to_dict() == {"name": "book", "price": 12}, (
"Product('book', 12).to_dict() should return "
"{'name': 'book', 'price': 12}"
)
print("oop_advanced11 ok")
10 changes: 8 additions & 2 deletions checks/oop_advanced/oop_advanced12.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
result = Vector(1, 2) + Vector(3, 4)
assert isinstance(result, Vector)
assert (result.x, result.y) == (4, 6)
assert isinstance(result, Vector), (
f"Adding two Vector instances should return a Vector, "
f"got {type(result).__name__}"
)
assert (result.x, result.y) == (4, 6), (
f"Vector(1, 2) + Vector(3, 4) should be (4, 6), "
f"got {(result.x, result.y)!r}"
)
print("oop_advanced12 ok")
8 changes: 6 additions & 2 deletions checks/oop_advanced/oop_advanced7.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
assert Book("A", "123") == Book("B", "123")
assert Book("A", "123") != Book("C", "999")
assert Book("A", "123") == Book("B", "123"), (
"Two books with the same ISBN should be equal"
)
assert Book("A", "123") != Book("C", "999"), (
"Two books with different ISBNs should not be equal"
)
print("oop_advanced7 ok")
5 changes: 4 additions & 1 deletion checks/oop_advanced/oop_advanced8.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
coord = Coordinate.from_text("3,4")
assert (coord.x, coord.y) == (3, 4)
assert (coord.x, coord.y) == (3, 4), (
f"Coordinate.from_text('3,4') should produce (3, 4), "
f"got {(coord.x, coord.y)!r}"
)
print("oop_advanced8 ok")
8 changes: 5 additions & 3 deletions checks/oop_advanced/oop_advanced9.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
assert Grade.is_valid(0) is True
assert Grade.is_valid(100) is True
assert Grade.is_valid(101) is False
assert Grade.is_valid(0) is True, "Grade 0 should be a valid score"
assert Grade.is_valid(100) is True, "Grade 100 should be a valid score"
assert Grade.is_valid(101) is False, (
"Grade 101 should be rejected as out of range"
)
print("oop_advanced9 ok")
8 changes: 6 additions & 2 deletions checks/pathlib/pathlib1.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from pathlib import Path

assert path == Path("notes/today.txt")
assert filename == "today.txt"
assert path == Path("notes/today.txt"), (
f"path should be Path('notes/today.txt'), got {path!r}"
)
assert filename == "today.txt", (
f"filename should be 'today.txt', got {filename!r}"
)
print("pathlib1 ok")
4 changes: 3 additions & 1 deletion checks/pathlib/pathlib2.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from pathlib import Path

assert source == Path("pythonlings/src/main.py")
assert source == Path("pythonlings/src/main.py"), (
f"source should be Path('pythonlings/src/main.py'), got {source!r}"
)
print("pathlib2 ok")
8 changes: 5 additions & 3 deletions checks/pathlib/pathlib3.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pathlib import Path

assert parent == Path("docs")
assert stem == "guide"
assert suffix == ".md"
assert parent == Path("docs"), (
f"parent should be Path('docs'), got {parent!r}"
)
assert stem == "guide", f"stem should be 'guide', got {stem!r}"
assert suffix == ".md", f"suffix should be '.md', got {suffix!r}"
print("pathlib3 ok")
5 changes: 4 additions & 1 deletion checks/pathlib/pathlib4.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from pathlib import Path

assert python_files == [Path("app.py"), Path("tests.py")]
assert python_files == [Path("app.py"), Path("tests.py")], (
f"python_files should be [Path('app.py'), Path('tests.py')], "
f"got {python_files!r}"
)
print("pathlib4 ok")
4 changes: 3 additions & 1 deletion checks/pathlib/pathlib5.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from pathlib import Path

assert final == Path("reports/final.txt")
assert final == Path("reports/final.txt"), (
f"final should be Path('reports/final.txt'), got {final!r}"
)
print("pathlib5 ok")
8 changes: 6 additions & 2 deletions checks/pathlib/pathlib6.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from pathlib import Path

assert processed == Path("data/raw.json")
assert parts[-2:] == ("data", "raw.json")
assert processed == Path("data/raw.json"), (
f"processed should be Path('data/raw.json'), got {processed!r}"
)
assert parts[-2:] == ("data", "raw.json"), (
f"parts[-2:] should be ('data', 'raw.json'), got {parts[-2:]!r}"
)
print("pathlib6 ok")
4 changes: 2 additions & 2 deletions docs-site/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Pythonlings follows a tight edit-check-advance loop. Here is what happens at eac
## The Learner Loop

1. **Open the next pending exercise** — the TUI shows the first incomplete exercise in the curriculum.
2. **Edit the broken code** — fix the intentional mistake directly in the built-in editor (or any external editor you prefer).
3. **Checks rerun automatically** — roughly 0.6 s after you stop typing, Pythonlings re-evaluates the exercise in a fresh subprocess. There is no filesystem watcher; the debounce fires inside the TUI editor.
2. **Edit the broken code** — fix the intentional mistake directly in the built-in editor. This is the automatic edit-check-advance workflow. If you use an external editor, save the file and run `pythonlings run <name>` to check it manually.
3. **Checks rerun automatically** — roughly 0.6 s after you stop typing in the built-in editor, Pythonlings re-evaluates the exercise in a fresh subprocess. There is no filesystem watcher, so external saves do not trigger this automatic check.
4. **Remove the `# I AM NOT DONE` marker** — the marker is your explicit "I'm finished" signal. Passing checks alone does not advance the exercise (see below).
5. **Advance** — once checks are green *and* the marker is gone, the exercise is marked complete and the TUI moves to the next one.

Expand Down
5 changes: 3 additions & 2 deletions docs-site/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ hide:
<div class="pl-hero" markdown>
<div class="pl-eyebrow">Rustlings for Python</div>
<div class="pl-title">Learn Python by fixing tiny broken programs.</div>
<div class="pl-subtitle">292 exercises across 31 topics. Hidden checks rerun the
instant you save — fix the code, watch it go green, advance.</div>
<div class="pl-subtitle">292 exercises across 31 topics. In the built-in editor,
hidden checks rerun after you stop typing — fix the code, watch it go green,
advance.</div>
<div class="pl-install">$ uvx pythonlings init</div>
<div class="pl-ctas">
<a class="pl-btn" href="quick-start/">Get started →</a>
Expand Down
8 changes: 5 additions & 3 deletions docs-site/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pythonlings update # pull new exercises into an existing workspace
## Exercise Loop

Each exercise contains a `# I AM NOT DONE` marker. Open the file in the TUI
editor (or any editor), fix the broken code, then remove the marker. Pythonlings
runs the hidden check automatically; when the check passes and the marker is gone,
the exercise is marked complete and progress advances to the next one.
editor and fix the broken code, then remove the marker. The built-in editor runs
the hidden check automatically; if you use an external editor, save the file and
run `pythonlings run <name>` to check it manually. When the check passes and the
marker is gone in the TUI, the exercise is marked complete and progress advances
to the next one.