Focused practice · 9 min read

Python Tokenization Exercises: Tokens, Vectors, Cosine

Tokenization is the first place where an AI pipeline can quietly lose information. These exercises make the choices visible: what counts as a token, how the vocabulary is shared, and what a similarity score can and cannot tell you.

Normalize before you split

Lowercasing is a simple normalization step. It makes Python and python the same token, but it also erases information that may matter for names or code. Choose a rule and apply it consistently to training and new text.

import re

def tokenize(text):
    return re.findall(r"[a-z']+", text.lower())

print(tokenize("Python, Python! Text."))

One rule for every document

Share one vocabulary

Build the vocabulary from the training documents, not separately for each new document. Sort the tokens so the dimension order is deterministic. Tokens outside the vocabulary need an explicit policy, such as ignore or unknown.

docs = ["python text", "text models"]
vocab = sorted({word for doc in docs for word in doc.split()})
index = {word: i for i, word in enumerate(vocab)}
print(index)

Stable dimensions

Count into vectors

A count vector is easy to inspect: each position says how many times one vocabulary token appeared. Normalize by document length when long documents should not automatically look more similar to everything.

def vector(text, index):
    counts = [0] * len(index)
    for word in text.split():
        if word in index:
            counts[index[word]] += 1
    return counts

index = {"python": 0, "text": 1}
print(vector("python text text", index))

A vector you can audit

Compare with cosine

Cosine similarity measures the angle between vectors, so it ignores magnitude. That is useful when one document is simply longer. It still cannot tell you whether two sentences mean the same thing if they use different words.

import math

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    norm_a = math.sqrt(sum(x * x for x in a))
    norm_b = math.sqrt(sum(y * y for y in b))
    if not norm_a or not norm_b:
        return 0.0
    return dot / (norm_a * norm_b)

print(round(cosine([1, 1], [2, 2]), 3))
print(round(cosine([1, 0], [0, 1]), 3))

Direction, not length

Know what the bag misses

A bag-of-words model gives 'dog bites man' and 'man bites dog' the same vector. Character n-grams can help with misspellings, and word embeddings can capture distributional similarity, but neither removes the need to evaluate on your actual task.

def bag(text):
    return sorted(text.lower().split())

print(bag("dog bites man") == bag("man bites dog"))

Order is gone

Your practice checklist

Common questions

Is a token always a word?

No. Production tokenizers may split punctuation, subwords, bytes, or Unicode code points. A word token is a useful teaching simplification.

Do I need a neural embedding to compare text?

No. Bag-of-words and TF-IDF are strong baselines for many search and classification tasks. Embeddings help when lexical overlap is not enough.

Why does cosine return 0.0?

The two vectors have no shared nonzero dimensions, or one vector is all zeros. The guard makes that case explicit.

Practice tokens and vectors

12 questions on tokenization, vocabularies, counts, cosine similarity, and lexical limits.

Open practice

Keep reading: