Editor

Typing Speed Test in Python

Intermediate · Games · about 45 minutes to build · 114 lines

Type three sentences as fast as you can and see your speed in words per minute, your accuracy and the words you got wrong.

The program

Press Run and answer its questions in the box that appears under the code.

typing_speed_test.py
"""Typing speed test: type sentences, get words per minute and accuracy."""
import random
import time
from difflib import SequenceMatcher
SENTENCES = [
"The quick brown fox jumps over the lazy dog.",
"Practice a little every day and your speed will grow.",
"Python code is meant to be read by people as well as computers.",
"Small steps every day add up to big results.",
"Typing fast is useful, but typing correctly matters more.",
"A good programmer writes code that other people can understand.",
]
ROUNDS = 3
CHARS_PER_WORD = 5 # typing tests count every 5 characters as one word
MIN_SECONDS = 1.0 # anything quicker is too short to time fairly
MAX_MISTAKES_SHOWN = 3
# --- Scoring one attempt ---
def words_per_minute(typed, seconds):
"""Speed in words per minute, where a word is 5 characters."""
return (len(typed) / CHARS_PER_WORD) / (seconds / 60)
def accuracy(target, typed):
"""Percentage of characters that match, using difflib to line them up."""
matcher = SequenceMatcher(None, target, typed)
matched = sum(block.size for block in matcher.get_matching_blocks())
return 100 * matched / max(len(target), len(typed))
def word_mistakes(target, typed):
"""List the words that were mistyped, missed or added."""
want, got = target.split(), typed.split()
mistakes = []
for tag, w1, w2, g1, g2 in SequenceMatcher(None, want, got).get_opcodes():
expected, actual = " ".join(want[w1:w2]), " ".join(got[g1:g2])
if tag == "replace":
mistakes.append(f'"{actual}" should be "{expected}"')
elif tag == "delete":
mistakes.append(f'missed "{expected}"')
elif tag == "insert":
mistakes.append(f'extra "{actual}"')
return mistakes
# --- Playing ---
def play_round(number, sentence):
"""Time one sentence; return (speed, accuracy), or None if skipped."""
print(f"\nRound {number} of {ROUNDS}. Type this sentence:\n")
print(" " + sentence + "\n")
input("Press Enter to start the clock: ")
start = time.perf_counter()
typed = input("Type it here: ").strip()
seconds = time.perf_counter() - start
if not typed:
print("Nothing was typed, so this round is skipped.")
return None
score = accuracy(sentence, typed)
if seconds < MIN_SECONDS:
speed = None
print(" Time: under 1 second, too quick to time")
else:
speed = words_per_minute(typed, seconds)
print(f" Time: {seconds:.1f} seconds")
print(f" Speed: {speed:.0f} words per minute")
print(f" Accuracy: {score:.0f}%")
mistakes = word_mistakes(sentence, typed)
for mistake in mistakes[:MAX_MISTAKES_SHOWN]:
print(f" Mistake: {mistake}")
if len(mistakes) > MAX_MISTAKES_SHOWN:
print(f" ...and {len(mistakes) - MAX_MISTAKES_SHOWN} more")
return speed, score
def show_summary(results):
"""Print the average speed and accuracy over the rounds that counted."""
print("\nSummary")
if not results:
print(" No rounds were typed.")
return
speeds = [speed for speed, _ in results if speed is not None]
scores = [score for _, score in results]
if speeds:
average = sum(speeds) / len(speeds)
print(f" Average speed: {average:.0f} words per minute")
print(f" Average accuracy: {sum(scores) / len(scores):.0f}%")
def main():
"""Play rounds of the typing test until the player stops."""
print("Typing speed test")
print(f"Type {ROUNDS} sentences as fast and as accurately as you can.")
while True:
results = []
for number, sentence in enumerate(random.sample(SENTENCES, ROUNDS), 1):
result = play_round(number, sentence)
if result is not None:
results.append(result)
show_summary(results)
again = input("\nPlay again? (y/n) ").strip().lower()
while again not in ("y", "n"):
again = input("Please type y or n: ").strip().lower()
if again == "n":
print("Goodbye.")
break
if __name__ == "__main__":
main()

Open it in the full editor to save it, share it and keep building.

Sample run

What the program printed, and what was typed (in yellow).

Typing speed test
Type 3 sentences as fast and as accurately as you can.

Round 1 of 3. Type this sentence:

    Python code is meant to be read by people as well as computers.

Press Enter to start the clock: 
Type it here: Python code is meant to be read by people as well as computers.
  Time:      14.4 seconds
  Speed:     53 words per minute
  Accuracy:  100%

Round 2 of 3. Type this sentence:

    Practice a little every day and your speed will grow.

Press Enter to start the clock: 
Type it here: Practise a litle every day and your speed will grow.
  Time:      11.9 seconds
  Speed:     52 words per minute
  Accuracy:  96%
  Mistake:   "Practise" should be "Practice"
  Mistake:   "litle" should be "little"

Round 3 of 3. Type this sentence:

    Small steps every day add up to big results.

Press Enter to start the clock: 
Type it here: Small steps every day add to big results
  Time:      9.3 seconds
  Speed:     52 words per minute
  Accuracy:  91%
  Mistake:   missed "up"
  Mistake:   "results" should be "results."

Summary
  Average speed:     52 words per minute
  Average accuracy:  96%

Play again? (y/n) maybe
Please type y or n: n
Goodbye.

How it is built

1. Words per minute

Typing tests count every 5 characters as one word, so long words count for more than short ones. Dividing by the minutes taken gives the speed.

def words_per_minute(typed, seconds):
    """Speed in words per minute, where a word is 5 characters."""
    return (len(typed) / CHARS_PER_WORD) / (seconds / 60)

2. Accuracy

SequenceMatcher lines the typed text up with the sentence, so one missing letter does not make every letter after it look wrong. The sizes of the matching blocks are added up and divided by the longer of the two lengths.

def accuracy(target, typed):
    """Percentage of characters that match, using difflib to line them up."""
    matcher = SequenceMatcher(None, target, typed)
    matched = sum(block.size for block in matcher.get_matching_blocks())
    return 100 * matched / max(len(target), len(typed))

3. Listing the mistakes

The same matcher works on lists of words. get_opcodes() describes how to turn one list into the other: replace means a mistyped word, delete a missed word and insert an extra one.

def word_mistakes(target, typed):
    """List the words that were mistyped, missed or added."""
    want, got = target.split(), typed.split()
    mistakes = []
    for tag, w1, w2, g1, g2 in SequenceMatcher(None, want, got).get_opcodes():
        expected, actual = " ".join(want[w1:w2]), " ".join(got[g1:g2])
        if tag == "replace":
            mistakes.append(f'"{actual}" should be "{expected}"')
        elif tag == "delete":
            mistakes.append(f'missed "{expected}"')
        elif tag == "insert":
            mistakes.append(f'extra "{actual}"')
    return mistakes

4. One timed round

time.perf_counter() is read when the player presses Enter to start and again when the answer arrives; the difference is the typing time. A round finished in under a second is too quick to be real typing, so it shows no speed.

def play_round(number, sentence):
    """Time one sentence; return (speed, accuracy), or None if skipped."""
    print(f"\nRound {number} of {ROUNDS}. Type this sentence:\n")
    print("    " + sentence + "\n")
    input("Press Enter to start the clock: ")
    start = time.perf_counter()
    typed = input("Type it here: ").strip()
    seconds = time.perf_counter() - start
    if not typed:
        print("Nothing was typed, so this round is skipped.")
        return None
    score = accuracy(sentence, typed)
    if seconds < MIN_SECONDS:
        speed = None
        print("  Time:      under 1 second, too quick to time")
    else:
        speed = words_per_minute(typed, seconds)
        print(f"  Time:      {seconds:.1f} seconds")
        print(f"  Speed:     {speed:.0f} words per minute")
    print(f"  Accuracy:  {score:.0f}%")
    mistakes = word_mistakes(sentence, typed)
    for mistake in mistakes[:MAX_MISTAKES_SHOWN]:
        print(f"  Mistake:   {mistake}")
    if len(mistakes) > MAX_MISTAKES_SHOWN:
        print(f"  ...and {len(mistakes) - MAX_MISTAKES_SHOWN} more")
    return speed, score

5. The summary

Each round returns a (speed, accuracy) tuple, or None when nothing was typed. The summary averages the speeds that were timed and all the accuracies.

def show_summary(results):
    """Print the average speed and accuracy over the rounds that counted."""
    print("\nSummary")
    if not results:
        print("  No rounds were typed.")
        return
    speeds = [speed for speed, _ in results if speed is not None]
    scores = [score for _, score in results]
    if speeds:
        average = sum(speeds) / len(speeds)
        print(f"  Average speed:     {average:.0f} words per minute")
    print(f"  Average accuracy:  {sum(scores) / len(scores):.0f}%")

Make it bigger

  • Show net speed as well, which takes one word per minute off for each mistake.
  • Save the best speed in a file and tell the player when they beat it.
  • Add a hard mode with longer sentences full of capitals, digits and punctuation.
  • Let the player choose how many rounds to play.

What it practises