I built a tiny GPT from scratch to show my kids how chatbots really work A developer with over 30 years of coding experience built PyLLM, a from-scratch GPT implementation in pure Python and numpy that includes its own autograd engine, tokenizer, attention layers, optimizer, training loop and sampling code, with no PyTorch or TensorFlow. The package ships with a pre-trained two-block, 59,739-parameter model trained on 149 Pokémon names, and offers a three-rung ladder (bigram, MLP, GPT) each trained for 2,000 steps, with the bigram model reaching a final loss of 2.3501 in about a second. The project is aimed at showing how chatbots work, with numerical gradient checks validating every operation and layer. What actually happens inside a GPT? Everyone talks about "attention" and "transformers" and "training", but if you had to build one with nothing but arrays of numbers, where would you even start? I've been coding for over 30 years and I couldn't have told you. So I built a small one in Python and called it PyLLM. There's no PyTorch and no TensorFlow, just numpy for the array maths. The autograd engine, the layers, attention, the tokenizer, the optimiser, the training loop and the sampling are all plain Python you can read. pip install fromscratch-pyllm PyLLM ships with a tiny model that's already trained, so it starts writing straight away: bash $ pyllm --seed 0 --max-new-tokens 40 machop machoke machamp bellsprout weepin It wrote that one letter at a time: it guessed m , then a , then c , and fed each guess back in as the start of the next one. weepin gets cut off because I only gave it 40 characters. A GPT does exactly this, just with far more practice. A model can't read letters. First the text gets chopped into tokens, a bit like snapping a sentence into Lego bricks, and every brick gets a number. The model only ever sees the numbers: php $ pyllm tokenize --text "Pika" 'P' - I have not seen that character yet 'i' - 9 'k' - 11 'a' - 1 Here every character is its own brick. The alphabet was learned from lower-case Pokémon names, so a capital P simply isn't in it. There's also a BPE tokenizer that learns bigger bricks like "ing" , which is closer to what the big models use. Learning means asking every number in the model: if I nudge you up a little, does the answer get better or worse, and by how much? That's a gradient, and working it out automatically is called autograd. The docs explain it with breadcrumbs. Every time PyLLM's Tensor does some maths, it drops a breadcrumb saying who its parents were and how to pass the blame back to them. Then backward follows the trail in reverse: python from pyllm.autograd import Tensor a = Tensor 2.0 b = Tensor 3.0 c = a b + a c = 2 3 + 2 = 8 c.backward print c.data, a.grad, b.grad 8. 4. 2. a was used twice, once multiplied by 3 and once on its own, so its blame adds up to 4. Each op is a few lines: work out the answer, then leave a small backward recipe behind. How do you know the breadcrumbs are right? You check them the slow, obvious way: nudge each number a tiny bit and see how far the output moves. PyLLM's numerical grad does exactly that, and the tests use it on every op and layer. Everything else in PyLLM sits on top of this one idea. When you read "The trophy didn't fit in the case because it was too big", you glance back to work out what "it" means. Attention is the model doing the same thing. Each token asks a question a query , every earlier token holds up a label a key , and the good matches hand over their information a value . A causal mask stops anyone peeking at letters that haven't been written yet. A few attention "heads" side by side, plus a bit of private thinking the feed-forward part , make one Transformer block. Stack a few blocks and you've got a GPT. The bundled Pokémon model has two blocks and 59,739 numbers in total. A real chatbot has billions. Training is a guessing game. Guess the next letter, find out how surprised you should have been the loss , follow the breadcrumbs to share out the blame, nudge every number a little, and repeat. PyLLM gives you a ladder of three models, each one fixing a weakness of the one below. I trained each for 2,000 steps on the bundled list of 149 Pokémon names: bash $ pyllm train --model bigram --steps 2000 --seed 0 --out bigram.npz $ pyllm train --model mlp --steps 2000 --seed 0 --out mlp.npz $ pyllm train --model gpt --steps 2000 --seed 0 --out gpt.npz | Model | What it looks at | Final loss | Time on my run | |---|---|---|---| | Bigram | just the last letter | 2.3501 | about 1 second | | MLP | the last 3 letters | 0.7192 | about 2 seconds | | GPT | the last 16 letters, with attention | 0.2996 | about 4 minutes | Then I asked each one for names with the same seed pyllm --checkpoint