Test-Driven Development (TDD) inverts the normal coding order: you write the test before writing any implementation code, then write just enough code to make the test pass, then improve the code. The discipline is captured in a three-phase loop called Red-Green-Refactor.
## The Red-Green-Refactor cycle
**Red** — Write a test for behaviour that does not yet exist. Run it. It fails — the output is red. This is intentional and correct. A failing test before any implementation proves that your test is actually testing something real. If a brand-new test passes without any code changes, either the behaviour was already implemented elsewhere or the test is wrong.
**Green** — Write the minimum code that makes the failing test pass. Do not add anything extra. No "I'll need this later" code. No polish. The sole objective is green output. Write the simplest thing that could possibly work.
**Refactor** — Now that the code works, improve it. Remove duplication, rename for clarity, extract helpers, reorganise. The tests guard you: if a refactor breaks something, you hear about it immediately. This is the phase where you write good code — not in the Green phase.
```
[Red] Write failing test for the next behaviour
↓
[Green] Write minimum code to make it pass
↓
[Refactor] Improve the code while tests stay green
↓
[Red] Write the next failing test ...
```
Each loop is short — a few minutes at most. The cycle is intentionally small. You never accumulate large amounts of unverified code.
## Why write the test first?
The case for test-first is not just discipline — it changes what you build and how you build it.
**Tests written after code describe what the code does, not what it should do.** You already know the implementation when you write after-the-fact tests, so you unconsciously confirm what exists rather than specifying what was intended. Edge cases you didn't implement don't get tested because you didn't think to test them.
**Hard-to-test code signals a design problem.** A function that needs 8 setup steps before you can call it is hard to write a test for — and hard to use in production. With TDD, you discover this before the code is written. The pain of a difficult test drives a better design.
**The test is the first client of your API.** Writing a test is the first time you "call" your code. If calling it feels awkward in the test, the API is awkward. TDD surfaces API problems while you can still fix them at zero cost.
**Coverage of your specification is automatic.** Every line of implementation exists because a failing test demanded it. If no test demanded a line, that line doesn't get written.
## When TDD pays off
TDD delivers the most value when:
- **The logic is non-trivial.** Validation rules, business constraints, state machines, parsing, calculations with multiple branching conditions — any code where correctness depends on subtle combinations of inputs.
- **You are designing the API.** TDD is a design tool. A test that is easy to write suggests a good API; a painful test suggests a bad one. The friction appears before the code is committed.
- **You will refactor.** Refactoring without tests is guesswork. With TDD, every behaviour has a test before any refactoring begins — you can restructure freely and trust the tests to catch regressions.
- **Correctness matters more than speed.** Financial calculations, security-sensitive code, data transformations with edge cases — places where a silent wrong answer is worse than an obvious crash.
## When TDD is less valuable
TDD doesn't fit every situation:
- **Exploratory spikes.** When you don't know what you're building yet — prototyping an approach, learning an unfamiliar library, trying out an algorithm — write the spike first to learn, then discard it and start TDD on the real implementation. Tests on throwaway code are waste.
- **Thin glue code.** Route definitions, migration files, declarative config — code with no logic to test. Testing that Django mapped `/users/` to `UserView` adds no confidence.
- **UI rendering and layout.** Asserting pixel positions or animation behaviour is fragile and high-maintenance. Test the logic that feeds the UI; use lighter-weight tools for UI integration.
- **Thin external API wrappers.** `return requests.get(url).json()` has nothing meaningful to test beyond "did we call requests". Such tests are brittle and don't add confidence.
## How TDD shapes code structure
Code grown test-first tends to acquire certain structural properties — not because of discipline but because the tests demand it during development:
**Smaller functions.** A function that requires 8 arguments or deep setup to call is painful to test. The pain appears before the code exists, so you make the function smaller.
**Explicit dependencies.** If a function needs a database connection, the test has to provide one. The path of least resistance is making the dependency a parameter (dependency injection) rather than a hidden module-level import. TDD makes hidden globals and side-effectful imports immediately painful.
**More pure functions.** Functions that take arguments and return values — no shared state, no file I/O, no network — are the easiest to test. TDD consistently nudges you toward writing more of them.
**Minimal, cohesive classes.** A class that does too much requires too much setup in each test. The test friction signals the design problem before it is baked in.
## Common objections
**"TDD takes twice as long."**
The test has to be written at some point — the question is when. Writing it first guides the implementation and often makes it faster. The real time sink is after-the-fact debugging of untested code. TDD shifts cost from debugging to design, which is a better trade.
**"I don't know what I'm building yet."**
You don't need to know the whole design upfront. You only need to know the next behaviour. Write one test for the smallest next thing. TDD is incremental — you discover the design through the tests, not before them.
**"My codebase is untestable."**
Untestable code is a symptom of design problems: tight coupling, hidden dependencies, global state. TDD applied to new code prevents these from forming. Applied to existing code, TDD acts as a pressure valve — it makes the design problems visible as test pain, motivating you to fix them.
TDD in Practice: Building BankAccount Step by Step
## The application we will build
A `BankAccount` class with `deposit`, `withdraw`, and `balance`, grown using strict TDD — one test at a time, minimum code to pass, then refactor. Two files:
```
bank.py # production code — starts empty
test_bank.py # tests — grows one test at a time
```
At the start, both files are empty.
---
## Cycle 1: a new account has zero balance
**Red** — write the first test:
```python
# test_bank.py
from bank import BankAccount
def test_new_account_has_zero_balance():
account = BankAccount()
assert account.balance == 0
```
Run it:
```
$ pytest test_bank.py -v
FAILED - ModuleNotFoundError: No module named 'bank'
```
Good — it fails. We have permission to write code.
**Green** — absolute minimum:
```python
# bank.py
class BankAccount:
def __init__(self):
self.balance = 0
```
```
1 passed
```
**Refactor** — nothing to clean up yet.
---
## Cycle 2: depositing money increases the balance
**Red:**
```python
def test_deposit_increases_balance():
account = BankAccount()
account.deposit(100)
assert account.balance == 100
```
```
FAILED - AttributeError: 'BankAccount' object has no attribute 'deposit'
```
**Green:**
```python
def deposit(self, amount):
self.balance += amount
```
```
2 passed
```
**Refactor** — still nothing to deduplicate.
---
## Cycle 3: withdrawing money decreases the balance
**Red:**
```python
def test_withdraw_decreases_balance():
account = BankAccount()
account.deposit(200)
account.withdraw(50)
assert account.balance == 150
```
**Green:**
```python
def withdraw(self, amount):
self.balance -= amount
```
```
3 passed
```
---
## Cycle 4: overdraft raises ValueError
Here the test forces us to specify the contract — what error type, what message — before any code exists:
**Red:**
```python
import pytest
def test_withdraw_raises_when_insufficient_funds():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Insufficient funds"):
account.withdraw(150)
```
```
FAILED - DID NOT RAISE <class 'ValueError'>
```
**Green:**
```python
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
```
```
4 passed
```
The error message `"Insufficient funds"` was decided in the test — the specification drives the implementation detail.
---
## Cycle 5: deposit must be a positive amount
**Red:**
```python
def test_deposit_rejects_non_positive_amount():
account = BankAccount()
with pytest.raises(ValueError, match="Amount must be positive"):
account.deposit(-50)
```
**Green:**
```python
def deposit(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
```
```
5 passed
```
---
## Cycle 6: withdraw amount must also be positive
Withdrawing zero or a negative number would silently corrupt the balance. The test catches this before it can happen:
**Red:**
```python
def test_withdraw_rejects_non_positive_amount():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Amount must be positive"):
account.withdraw(0)
```
**Green:**
```python
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
```
```
6 passed
```
---
## Refactor: extract `_validate_positive`
Both `deposit` and `withdraw` now have identical validation. Before this grows further, extract it:
**Step 1** — add the helper (all 6 tests still pass):
```python
def _validate_positive(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
```
**Step 2** — replace the duplicated checks (all 6 tests still pass):
```python
def deposit(self, amount):
self._validate_positive(amount)
self.balance += amount
def withdraw(self, amount):
self._validate_positive(amount)
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
```
```
6 passed
```
The refactor is safe because every behaviour has a test.
---
## Final state of both files
```python
# test_bank.py
import pytest
from bank import BankAccount
def test_new_account_has_zero_balance():
account = BankAccount()
assert account.balance == 0
def test_deposit_increases_balance():
account = BankAccount()
account.deposit(100)
assert account.balance == 100
def test_withdraw_decreases_balance():
account = BankAccount()
account.deposit(200)
account.withdraw(50)
assert account.balance == 150
def test_withdraw_raises_when_insufficient_funds():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Insufficient funds"):
account.withdraw(150)
def test_deposit_rejects_non_positive_amount():
account = BankAccount()
with pytest.raises(ValueError, match="Amount must be positive"):
account.deposit(-50)
def test_withdraw_rejects_non_positive_amount():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Amount must be positive"):
account.withdraw(0)
```
```python
# bank.py
class BankAccount:
def __init__(self):
self.balance = 0
def deposit(self, amount):
self._validate_positive(amount)
self.balance += amount
def withdraw(self, amount):
self._validate_positive(amount)
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
def _validate_positive(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
```
---
## What TDD gave us
- 6 behaviours, each specified by a test before the code existed.
- Error messages were decided in the tests — the specification drives implementation details, not the other way around.
- The `_validate_positive` helper emerged from refactoring, not upfront design. The test suite made it safe to extract.
- There is no code in `bank.py` that isn't covered by a test — every line is there because a failing test demanded it.
- At any point in the cycle, running `pytest -v` tells you exactly where you stand.
## The TDD cycle
```
1. [Red] Write a failing test for the next behaviour
2. [Green] Write the minimum code to make it pass — nothing extra
3. [Refactor] Clean up the code while all tests stay green
4. Repeat for the next behaviour
```
## pytest commands for TDD
```bash
# Stop at first failure — fastest feedback while implementing
pytest -x
# Run only tests matching a name pattern
pytest -k "withdraw"
# Run one specific test
pytest test_bank.py::test_withdraw_raises_when_insufficient_funds -v
# Verbose: see every test name and pass/fail
pytest -v
# Watch mode: re-run on file save (requires pytest-watch)
pip install pytest-watch
ptw -- -x
```
## Test naming for TDD
Good names read as a specification. Pattern: `test_<subject>_<behaviour>_<condition>`.
| Name | Readable as |
|---|---|
| `test_new_account_has_zero_balance` | "A new account has zero balance" |
| `test_withdraw_raises_when_insufficient_funds` | "Withdraw raises when funds are insufficient" |
| `test_deposit_rejects_non_positive_amount` | "Deposit rejects non-positive amounts" |
| `test_email_validator_returns_false_for_empty_string` | Condition is explicit |
Avoid: `test_bank_1`, `test_edge_case`, `test_stuff`. When a test fails, its name should tell you what broke without reading the body.
## Verifying the Red phase
A new test that passes immediately (without code changes) means one of two things:
1. The behaviour was already implemented → keep the test as a regression guard.
2. The test is wrong → it's not actually exercising the behaviour you think.
To verify: temporarily sabotage the implementation and confirm the test goes red:
```python
def deposit(self, amount):
pass # deliberately broken
# If test_deposit_increases_balance now FAILS → test is correct
# If it still PASSES → the test is not testing deposit
```
Restore the implementation after verifying.
## Refactoring safely
Rule: **all tests must pass before and after every refactor step**. Never refactor while any test is red.
```python
# Before: duplication in two methods
def deposit(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
...
# Refactor step 1: add helper (tests still pass)
def _validate_positive(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
# Refactor step 2: replace duplicates (tests still pass)
def deposit(self, amount):
self._validate_positive(amount)
self.balance += amount
```
Make one small change, run tests, confirm green, then make the next change.
## TDD with parametrize
`@pytest.mark.parametrize` maps naturally to TDD: write the full spec (all test cases) before any implementation. All cases fail simultaneously (Red); one implementation makes them all pass (Green):
```python
import pytest
@pytest.mark.parametrize("amount,is_valid", [
(100, True),
(0.01, True),
(0, False),
(-50, False),
])
def test_deposit_validates_amount(amount, is_valid):
account = BankAccount()
if is_valid:
account.deposit(amount)
assert account.balance == pytest.approx(amount)
else:
with pytest.raises(ValueError):
account.deposit(amount)
```
The parametrize output shows each case separately — you can track progress through the Red phase as cases turn green one by one.
## Characterization tests for legacy code
When adding tests to existing code without tests, write "characterization tests" — tests that document what the code currently does:
```python
# Step 1: write tests to capture current behaviour (they pass immediately)
def test_word_count_splits_on_spaces():
stats = TextStats("hello world foo")
assert stats.word_count() == 3
# Step 2: refactor implementation while keeping tests green
# Step 3: add new tests for new behaviour
```
The tests aren't specifying what the code should do — they're capturing what it does. Once captured, you can refactor safely.
## Making time-dependent code testable
Inject time as a dependency rather than calling `time.time()` directly:
```python
import time
class RateLimiter:
def __init__(self, calls_per_second, time_fn=None):
self.limit = calls_per_second
self._time = time_fn if time_fn is not None else time.time # None = use real clock
self._window_start = self._time()
self._call_count = 0
def allow(self):
now = self._time()
if now - self._window_start >= 1.0:
self._window_start = now
self._call_count = 0
if self._call_count < self.limit:
self._call_count += 1
return True
return False
```
In tests, pass a fake clock — no `time.sleep` needed:
```python
fake_time = [0.0]
limiter = RateLimiter(2, time_fn=lambda: fake_time[0])
fake_time[0] = 1.5 # advance time instantly
```
## Common TDD mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Writing several tests before any Green | Cascade of failures, hard to isolate | One test at a time |
| Green phase adds more code than needed | Unspecified behaviour with no tests | Make only the failing test pass |
| Skipping Refactor phase | Technical debt accumulates each cycle | Always refactor, even briefly |
| Not confirming Red before coding | Test may not be testing anything real | Always watch the test fail first |
| Testing implementation internals | Tests break on refactor, not on behaviour change | Test behaviour (inputs/outputs), not internals |
| Refactoring in large steps | Hard to pinpoint which step broke a test | Small steps, run tests between each |
Using TDD, implement a `Stack` class with four methods: `push(item)`, `pop()`, `peek()`, and `is_empty()`.
**Step 1 -- Red:** Fill in all 7 test bodies first. Run pytest -- all 7 should fail with `AttributeError` or `AssertionError`. Do not touch `stack.py` yet.
Tests to write:
1. `test_new_stack_is_empty` -- `Stack().is_empty()` returns `True`
2. `test_push_makes_stack_non_empty` -- after `push(1)`, `is_empty()` returns `False`
3. `test_pop_returns_last_pushed_item` -- push 1, 2, 3; `pop()` returns `3`
4. `test_pop_removes_the_item` -- after push and pop, `is_empty()` returns `True`
5. `test_peek_returns_top_without_removing` -- `peek()` returns the top item; `is_empty()` is still `False` afterwards
6. `test_pop_raises_on_empty_stack` -- `pop()` on empty stack raises `IndexError`
7. `test_peek_raises_on_empty_stack` -- `peek()` on empty stack raises `IndexError`
**Step 2 -- Green/Refactor:** Implement `Stack` one method at a time, running pytest after each addition.
Using TDD, implement `validate_email(s: str) -> bool`.
**Step 1 — Red:** Write all 5 test bodies first. Run pytest — all 5 fail because the function always returns `None` (which is falsy). Do not implement anything yet.
Required tests:
1. `test_valid_email_returns_true` — `"[email protected]"` → `True`
2. `test_missing_at_sign_returns_false` — `"userexample.com"` → `False`
3. `test_missing_local_part_returns_false` — `"@example.com"` → `False`
4. `test_missing_domain_returns_false` — `"user@"` → `False`
5. `test_empty_string_returns_false` — `""` → `False`
**Step 2 — Green:** Implement one validation rule at a time. After each rule, run pytest. Add only the code needed to make the currently-failing test pass.
# email_validator.py
def validate_email(s: str) -> bool:
if not s:
return False
if '@' not in s:
return False
local, _, domain = s.partition('@')
if not local:
return False
if not domain:
return False
return True
# test_email.py
from email_validator import validate_email
def test_valid_email_returns_true():
assert validate_email("[email protected]") is True
def test_missing_at_sign_returns_false():
assert validate_email("userexample.com") is False
def test_missing_local_part_returns_false():
assert validate_email("@example.com") is False
def test_missing_domain_returns_false():
assert validate_email("user@") is False
def test_empty_string_returns_false():
assert validate_email("") is False
03
Write characterization tests for legacy code, then refactor safely
You are given a `TextStats` class with existing behaviour but no tests. Your task has two parts:
**Part 1 — Characterize:** Write 4 tests that document the current behaviour. These tests should pass immediately once you fill them in (the code already works). They are not specifying new behaviour — they are capturing existing behaviour so refactoring is safe.
Tests to write:
1. `test_word_count` — `TextStats("hello world foo").word_count()` returns `3`
2. `test_char_count` — `TextStats("hello").char_count()` returns `5`
3. `test_most_common_word` — `TextStats("cat dog cat").most_common_word()` returns `"cat"`
4. `test_most_common_word_is_case_insensitive` — `TextStats("Cat cat Dog").most_common_word()` returns `"cat"`
**Part 2 — Refactor:** Once all 4 tests pass, rewrite `most_common_word` using `collections.Counter`. Run the tests after — they must still pass.
```python
# text_stats.py — given, do not modify in Part 1
class TextStats:
def __init__(self, text: str):
self.text = text
def word_count(self):
return len(self.text.split())
def char_count(self):
return len(self.text)
def most_common_word(self):
words = self.text.lower().split()
counts = {}
for word in words:
if word in counts:
counts[word] += 1
else:
counts[word] = 1
best, best_count = None, 0
for word, count in counts.items():
if count > best_count:
best, best_count = word, count
return best
```
# text_stats.py — given, do not modify in Part 1
class TextStats:
def __init__(self, text: str):
self.text = text
def word_count(self):
return len(self.text.split())
def char_count(self):
return len(self.text)
def most_common_word(self):
words = self.text.lower().split()
counts = {}
for word in words:
if word in counts:
counts[word] += 1
else:
counts[word] = 1
best, best_count = None, 0
for word, count in counts.items():
if count > best_count:
best, best_count = word, count
return best
# test_text_stats.py
from text_stats import TextStats
def test_word_count():
pass
def test_char_count():
pass
def test_most_common_word():
pass
def test_most_common_word_is_case_insensitive():
pass
Solution
# test_text_stats.py
from text_stats import TextStats
def test_word_count():
stats = TextStats("hello world foo")
assert stats.word_count() == 3
def test_char_count():
stats = TextStats("hello")
assert stats.char_count() == 5
def test_most_common_word():
stats = TextStats("cat dog cat")
assert stats.most_common_word() == "cat"
def test_most_common_word_is_case_insensitive():
stats = TextStats("Cat cat Dog")
assert stats.most_common_word() == "cat"
# text_stats.py — after refactor
from collections import Counter
class TextStats:
def __init__(self, text: str):
self.text = text
def word_count(self):
return len(self.text.split())
def char_count(self):
return len(self.text)
def most_common_word(self):
words = self.text.lower().split()
if not words:
return None
return Counter(words).most_common(1)[0][0]
04
TDD with a time-dependent RateLimiter — inject the clock
Using TDD, implement a `RateLimiter(calls_per_second, time_fn=None)` class with a single method `allow() -> bool`.
The `time_fn` parameter accepts any callable returning the current time as a float. Defaulting to `time.time`. This makes the clock injectable — tests pass a fake clock instead of sleeping.
**Step 1 — Red:** Write all 5 tests using a fake clock (no `time.sleep`). All fail because `RateLimiter` doesn't exist yet.
Tests to write:
1. `test_first_call_is_always_allowed`
2. `test_calls_within_limit_are_allowed` — at limit=2 two calls at t=0 are both `True`
3. `test_calls_exceeding_limit_are_denied` — at limit=2 the third call at t=0 is `False`
4. `test_calls_reset_after_one_second` — after advancing fake time to 1.0, calls are allowed again
5. `test_two_limiters_are_independent` — exhausting one limiter does not affect another
**Step 2 — Green/Refactor:** Implement `RateLimiter` using a fixed 1-second window. The window resets when `now - window_start >= 1.0`.
Using TDD with `@pytest.mark.parametrize`, implement `to_roman(n: int) -> str` that converts a positive integer to its Roman numeral string.
**Step 1 — Red:** The parametrized test is already written below. Run it — all 10 cases fail because `to_roman` returns `None`. Do not implement anything yet. Read all 10 cases to understand the full contract.
**Step 2 — Green:** Implement `to_roman`. A clean approach: a list of `(value, numeral)` pairs in descending order, including the subtractive cases (`900 → "CM"`, `400 → "CD"`, `90 → "XC"`, etc.). Loop through: while `n >= value`, append the numeral and subtract the value.
**Step 3 — Refactor:** Once all 10 cases pass, review the implementation. Is the lookup table in the right order? Are all subtractive pairs present?
```python
# test_roman.py — the spec is already written; run it as-is
import pytest
from roman import to_roman
@pytest.mark.parametrize("n,expected", [
(1, "I"),
(4, "IV"),
(9, "IX"),
(14, "XIV"),
(40, "XL"),
(90, "XC"),
(400, "CD"),
(900, "CM"),
(1994, "MCMXCIV"),
(2024, "MMXXIV"),
])
def test_to_roman(n, expected):
assert to_roman(n) == expected
```
# roman.py
def to_roman(n: int) -> str:
pass
# test_roman.py — do not modify; run pytest and watch all 10 fail
import pytest
from roman import to_roman
@pytest.mark.parametrize("n,expected", [
(1, "I"),
(4, "IV"),
(9, "IX"),
(14, "XIV"),
(40, "XL"),
(90, "XC"),
(400, "CD"),
(900, "CM"),
(1994, "MCMXCIV"),
(2024, "MMXXIV"),
])
def test_to_roman(n, expected):
assert to_roman(n) == expected
Solution
# roman.py
def to_roman(n: int) -> str:
values = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"),
(1, "I"),
]
result = ""
for value, numeral in values:
while n >= value:
result += numeral
n -= value
return result
# test_roman.py — unchanged
import pytest
from roman import to_roman
@pytest.mark.parametrize("n,expected", [
(1, "I"),
(4, "IV"),
(9, "IX"),
(14, "XIV"),
(40, "XL"),
(90, "XC"),
(400, "CD"),
(900, "CM"),
(1994, "MCMXCIV"),
(2024, "MMXXIV"),
])
def test_to_roman(n, expected):
assert to_roman(n) == expected
No split tab
Cookie preferences
We use necessary cookies to run the site. With your permission, we can also save your site preferences and use analytics and advertising cookies to understand usage and support the project.
Open tools in tabs.Exercises, IDE tools, and trainers stay available as site tabs.
Switch without losing context.Move between explanations, code, and utilities while keeping your place.
Use the sidebar as your map.The left panels hold navigation, settings, files, libraries, and tool controls.
PythonJavaScriptSQLite
One IDE, three practical modes
Python in the browser.Run small scripts, try libraries, and practice API requests without installing anything.
JavaScript for quick experiments.Test browser-friendly code and compare ideas next to your learning materials.
SQLite for data practice.Open the database explorer to inspect tables, write queries, and learn SQL workflows locally.
TopicIDE
Work side by side with split tabs
Keep instructions visible.Open an exercise or reference page beside the IDE instead of jumping back and forth.
Compare tools while you learn.Place regex checks, explanations, and code experiments next to each other when the task needs it.
Close the split when you are done.The workspace returns to a single focused tab, and your open site tabs remain available.
Code Typing Trainer
Or plain text
This trainer is designed for a physical keyboard.Open this section on a laptop or desktop with a wide screen. Touch typing practice will not work correctly on a phone.
Speed: 0 chars/min
0 words/min
Best speed (60s): 0 chars/min
0 words/min
Errors: 0
Total time: 0.0 s
To practice touch typing, avoid looking at your physical keyboard.