Every test suite I write ends up needing throwaway names. Faker is great for "John Smith" and "Acme Inc", but the moment I'm seeding a game's NPC table or a fantasy-app fixture, real-world names look wrong next to a dwarf cleric. So I went down the rabbit hole of procedural name generation. You can get believable fantasy names with nothing but the standard library.
Here's the whole idea in a few lines.
The trick: stitch syllables, don't store names
A lookup table of pre-written names runs dry fast and feels repetitive. Instead you keep small pools of sound fragments per race and assemble a name from one start + optional middle + ending:
import random
ELF_START = ["Ae", "Fae", "Lael", "Cael", "Syl"]
ELF_MID = ["", "la", "ri", "va", "thy"]
ELF_END = ["riel", "wyn", "thas", "lor", "ndil"]
def elf_name(rnd=random.random):
pick = lambda pool: pool[int(rnd() * len(pool))]
return (pick(ELF_START) + pick(ELF_MID) + pick(ELF_END)).capitalize()
print(elf_name()) # -> 'Faewyn'
Five fragments per slot already gives you 5 x 5 x 5 = 125 combinations that read like elf names, because the constraint lives in the fragments, not in a giant list. Swap the pools and an orc reads like an orc:
ORC_START = ["Gr", "Mog", "Rok", "Dur", "Kra"]
# harsh consonant clusters, short endings -> 'Grukgor', 'Rokmash'
A couple of design notes that matter once you actually use this:
- Keep
generate()pure. Pass the RNG in as a callable (rnd=random.Random(7).random) so you can seed it. Reproducible names mean your test fixtures don't churn the diff every run. - Capitalize at the end, not per-fragment, or you get
FaeWyn. - An empty string in the middle pool is a cheap way to vary name length without a separate branch.
If you just want names, not a project
I packaged the full version as a pip install: seven races (human, elf, dwarf, orc, halfling, tiefling, dragonborn), masculine/feminine/any, still zero dependencies.
pip install dnd-name-generator
$ dnd-name-generator -r dwarf -g masculine -n 5
Thorin
Durgrim
Balek
Khazdin
Gimnor
Or from code:
from dnd_name_generator import generate, generate_many
generate("Tiefling", "Feminine") # -> 'Kallieth'
generate_many(3, race="Orc") # -> ['Grishnak', 'Moguk', 'Rokgor']
It's MIT and generate() takes the same rnd callable, so you can seed it for tests. Source and docs are on PyPI: https://pypi.org/project/dnd-name-generator/
Next time a fixture needs a half-orc barbarian instead of another "Test User 3", you've got options that don't pull in a single transitive dependency.
Top comments (0)