DevelopmentApril 07, 20254 min read

Typing Speed Test using Python and Streamlit

Build a typing speed test with Python and Streamlit — measure your WPM and accuracy live with session-state timing and instant scoring.

Galvan

Galvan

Founder & Creator

Introduction

A typing speed test is the perfect project for learning time-based logic in Streamlit — an app where the state isn't just what you clicked, but *when* you clicked it. You get a random sentence, type it as fast as you can, and the app scores words-per-minute and accuracy the moment you finish.

The interesting engineering: Streamlit reruns the script on every keystroke, so naive timing gets destroyed by reruns. The fix — snapshotting time.time() into st.session_state — is the exact pattern you would reuse for quiz timers or the pomodoro-style apps.

Features

  • Random sentences — a fresh prompt every round.
  • Live WPM + accuracy — computed the instant the text matches.
  • Character-level diff — see exactly which characters you got wrong.
  • Personal best — your top WPM stored across rounds in the session.
  • Restart button — new sentence, clean slate.
  • Prerequisites

  • Python 3.8+ — from python.org.
  • Streamlit — install with pip:
  • code
    pip install streamlit

    Step 1: Create the Script

    Save as typing_test.py:

    code
    import streamlit as st
    import time
    import random
    
    SENTENCES = [
        "the quick brown fox jumps over the lazy dog",
        "streamlit turns python scripts into shareable web apps",
        "practice makes perfect so keep your fingers on the home row",
        "clean code reads like well written prose",
        "small daily improvements are the key to staggering long term results",
    ]
    
    st.set_page_config(page_title="Typing Speed Test", page_icon="⌨️")
    st.title("⌨️ Typing Speed Test")
    
    if "target" not in st.session_state:
        st.session_state.target = random.choice(SENTENCES)
    
    st.markdown(f"### Type this:\n\n> *{st.session_state.target}*")
    
    typed = st.text_input("Your typing", placeholder="Start typing — the timer starts on your first keystroke")
    
    target = st.session_state.target
    if typed and "start_time" not in st.session_state:
        st.session_state.start_time = time.time()
    
    if "start_time" in st.session_state and typed:
        elapsed = time.time() - st.session_state.start_time
    
        if typed == target:
            words = len(target.split())
            wpm = round(words / (elapsed / 60))
            correct = sum(1 for a, b in zip(target, typed) if a == b)
            accuracy = round(100 * correct / len(target))
    
            best = st.session_state.get("best", 0)
            st.session_state.best = max(best, wpm)
    
            c1, c2, c3 = st.columns(3)
            c1.metric("WPM", wpm)
            c2.metric("Accuracy", f"{accuracy}%")
            c3.metric("Time", f"{elapsed:.1f}s")
            st.balloons()
            if wpm > best and best > 0:
                st.success("🏆 New personal best!")
    
        else:
            wrong = sum(1 for a, b in zip(target, typed) if a != b) + max(0, len(target) - len(typed))
            st.caption(f"{wrong} wrong character(s) so far — keep going!")
    
    if st.button("🔄 New sentence"):
        for key in ["target", "start_time"]:
            st.session_state.pop(key, None)
        st.rerun()

    Step 2: Run the App

    code
    streamlit run typing_test.py

    Type the sentence exactly (case and spaces matter), and your score appears the moment the final character matches.

    How It Works

    The whole app is three session-state keys: target (the sentence), start_time (set on the first keystroke), and best (your record). Because every keystroke triggers a full rerun, the timer must live *outside* the script's normal flow — the first keystroke snapshots time.time(), and every rerun after that compares against the stored snapshot. Store the time in a plain variable instead and it resets on every keystroke, making WPM meaningless.

    Scoring uses the industry-standard definition: WPM = words ÷ minutes, where the clock stops the instant the typed text equals the target. Accuracy is a positional character comparison with zip — pairing target and typed characters by index. The trailing max(0, len(target) - len(typed)) counts characters you haven't typed yet as errors-in-progress for the live hint.

    The restart button pops the state keys and reruns, which regenerates the target — state cleanup is as important as state creation.

    Common Errors & Fixes

  • WPM is absurdly highstart_time is being reset on every rerun; only set it when the key is missing ("start_time" not in st.session_state).
  • Timer never starts — you checked if typed: *before* the input rendered, or the check runs after the comparison block; order matters in a rerun script.
  • `zip` hides missing characterszip stops at the shorter string; that's why the error count adds the length difference separately.
  • New sentence button doesn't reset the input box — the text widget keeps its own state; give it a key and pop that key too.
  • Key Concepts

  • Time in session state — snapshots survive reruns; local variables don't.
  • First-event detectionif key not in st.session_state is your "once" hook.
  • zip for diffs — positional comparison of two strings in one line.
  • State cleanup — popping keys is how an app "restarts".
  • What to Try Next

  • Add a 60-second sprint mode with st_autorefresh and count completed words instead.
  • Show a live character diff in red/green using the zip comparison.
  • Persist personal bests to CSV — the expense tracker's storage pattern fits directly.
  • Add difficulty levels (punctuation-heavy sentences) with a sidebar selectbox, like the quiz app.
  • FAQ

    Is WPM measured in 5-character words like other sites?

    Professional tests standardize on 5 keystrokes = 1 word. Swap len(target.split()) for len(target) / 5 to match monkeytype-style scoring.

    Why does the input lose focus sometimes?

    Reruns recreate widgets; clicking elsewhere during a rerun drops focus. Keep the input as the only interactive element on its row to minimize this.

    Can two players compete on one keyboard?

    Fun idea — store both players' best scores in st.session_state under separate keys and show a leaderboard table after each round.