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
Prerequisites
pip install streamlitStep 1: Create the Script
Save as typing_test.py:
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
streamlit run typing_test.pyType 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
start_time is being reset on every rerun; only set it when the key is missing ("start_time" not in st.session_state).if typed: *before* the input rendered, or the check runs after the comparison block; order matters in a rerun script.zip stops at the shorter string; that's why the error count adds the length difference separately.key and pop that key too.Key Concepts
if key not in st.session_state is your "once" hook.What to Try Next
st_autorefresh and count completed words instead.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.