Skip to main content

conlang

A partial-spec constructed-language generator.

You describe only what you care about — phonology, syllable shapes, a handful of words — and the generator samples the rest from typologically-plausible defaults. Same spec plus same seed always produces byte-identical output, and sampled entries can be locked so they survive re-sampling.

spec (YAML) ──▶ generate ──▶ model (canonical JSON) ──▶ render ──▶ dictionary + grammar sketch
                                   │
                                   ├── lint   (machine-checkable invariants)
                                   └── bench  (plausibility scorecard vs. Kah)

The model JSON is the single source of truth; the dictionary, grammar sketch, and any future views are rendered from it.

Install

pip install conlanggen

The distribution is conlanggen, but the import package and the command are both conlang — import conlang, conlang generate.

Quickstart

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"

# Generate from the example spec, and render a Markdown dictionary.
.\.venv\Scripts\python.exe -m conlang.cli generate --spec spec\example.yaml --render

# Happy with it? Freeze it into an explicit spec so the values stick.
conlang accept --model out\veshtari.json

Output lands in out/: a model JSON and a rendered .md.

# Re-lint or re-render an existing model.
conlang lint --model out\veshtari.json
conlang render --model out\veshtari.json --out out\veshtari.md

Translating a fixed story

The toolkit ships a repeatable pipeline for translating the fixed 111-sentence story The Quiet Morning into a generated language: conlang new → grow → translate → validate-translation → extras. The runbook is docs/pipeline.md.

Writing a spec

Everything is optional. Omitted fields are sampled.

name: Veshtari
seed: 42

phonology:
  consonants: [p, t, k, m, n, s, l, r, v, sh, ch]
  vowels: [a, e, i, o, u]

syllables: [CV, CVC, V]   # C = consonant slot, V = vowel slot
word_count: 150

lexicon:                  # pin specific words; they are locked by default
  - gloss: water
    root: vela

Accepted keys: name, seed, phonology.{consonants,vowels,ipa,orthography}, syllables, concepts, word_count, lexicon, affixes, onsets, codas, forbidden.

An explicit, unsized lexicon is treated as the complete word list (that is what accept writes back); otherwise the built-in concept list is used and word_count trims or extends it.

If your syllables include CCV/CCVC, the generator samples a legal onset set unless you supply onsets yourself, and accept records whichever was used so a frozen spec regenerates identically.

IPA inventories

By default a symbol is its spelling (sh is written sh). Set phonology.ipa: true to declare the inventory phonemic: each symbol is an IPA phoneme, and the tool derives a romanised orthography for the dictionary.

name: Shibo
seed: 11

phonology:
  ipa: true
  consonants: [p, t, k, ʃ, tʃ, m, n, s, l]
  vowels: [a, e, i, o, u]
  orthography:        # optional; corrects or extends the built-in defaults
    tʃ: ch
    ʃ: sh

/ʃ/ spells sh and /tʃ/ spells ch from a built-in table; anything absent spells itself, so a plain Latin inventory is unchanged. orthography overrides the table (set an entry back to its own symbol to cancel a default), and the resolved map is stored on the inventory. The dictionary and word-form tables then show a spelling column beside the ipa one, and accept writes the map back so a frozen spec regenerates identically. Lint warns on a map entry that is not an inventory symbol, or two symbols that would spell the same thing.

Freezing a language (accept)

A partial spec samples whatever you leave out. Once you like the result, conlang accept writes it back as a fully-explicit spec: the phonology, syllable shapes, constraints, affixes, and every root become pinned entries. Regenerating from that spec reproduces the same language exactly — sampled values now survive re-sampling, and changing the seed can no longer disturb them.

conlang generate --spec spec\example.yaml --out out\veshtari.json
conlang accept  --model out\veshtari.json --out spec/veshtari.yaml
conlang generate --spec spec/veshtari.yaml --out out/veshtari.json

Two fields legitimately change across the round trip: origin becomes specified for every accepted entry (it is pinned now, not sampled), and spec_hash changes because the spec text is different. Everything linguistic is identical, and tests/test_invariants.py asserts exactly that.

Guarantees and lint rules

Reproducibility is the one hard guarantee: spec + seed ⇒ identical bytes. Everything else is reported by lint as errors or warnings, and a broken language is still generated (conlangers want to explore broken states).

rule severity meaning
schema error/warning schema version present and known
lexeme.root error lexeme has a non-empty root
phonotactics.lexeme error every root is a legal word
phonotactics.word error every inflected form is a legal word
lexicon.homophones warning two roots collide (Kah has polysemy too)
morphology.paradigm warning a word is missing a form for a category its class realises
morphology.class error a word names an inflection class the language does not define
lexicon.nonempty warning the lexicon is empty
orthography.unknown warning the spelling map names a symbol that is not in the inventory
orthography.collision warning two symbols would spell the same thing

Every rule above has a test in tests/test_lint.py, tests/test_lint_invariants.py, or tests/test_morphology.py.

Phonotactics

A word is legal when its C/V skeleton can be partitioned into the language's syllable templates and every resulting onset and coda is allowed. Two details matter more than they look:

  • Constraints are checked while syllabifying, not after. A word often has several valid partitions; deplango is de|plan|go with legal onsets, and also de|pla|ngo with an illegal one. Verdict must depend on the phonology, not on which partition the search happens to return first.
  • Onsets and codas are compared by segment, not by string. An onset of ny + m renders as "nym" — three characters, two segments. Matching strings would silently reject legal words in any language with a digraph.

When a template set admits onset clusters (CCV, CCVC), the generator samples a legal onset set and builds roots from it, so generated words are legal by construction rather than by luck. constraints.onsets in the model records what was sampled; an explicit onsets in the spec always wins.

Affixes carry allomorphs: conditioned surface forms. A single form cannot serve both consonant-final and vowel-final roots, because the root's coda plus a consonant-initial affix resyllabify into an onset cluster. When the primary form would be illegal, build_words uses the first alternative that is legal — which is what real languages do. It is a rare fallback (~2 in 8,500 affixed forms), but without it those words would be illegal and lint would fail the language.

The Kah benchmark

Kah (Kwesho's language) is hand-made, so the benchmark does not try to regenerate it. It does two things:

  1. Adapter fixture — parses the real Kwesho dictionary cache into the model schema, proving the schema can represent real data.
  2. Plausibility scorecard — compares a generated language against Kah.

The scorecard deliberately reports three separate numbers, because a word can fail for two unrelated reasons and collapsing them makes the report unreadable:

metric question
symbol coverage which Kah sounds does this language have?
usable sounds how many Kah headwords use only sounds this language has?
legal shapes of those, how many fit the phonotactics?

Scoring the built-in kah preset against Kah is circular, so the report says so explicitly when it detects it. Treat the scorecard as a lever, not a target: the presets are deliberately small, and a language that only allows CV syllables is a legitimate (if monotonous) language type, not a bug to tune away.

conlang bench --spec spec\example.yaml
# or point at a cache explicitly:
conlang bench --spec spec\example.yaml --kah C:\path\to\kah-lexicon.txt

Kah's dictionary is Kwesho's copyrighted work and is never vendored into this repository. Without --kah, the benchmark looks for the glossary at a per-user cache path (%LOCALAPPDATA%\conlang\kah-lexicon.txt on Windows, ~/.cache/conlang/kah-lexicon.txt elsewhere, or $XDG_CACHE_HOME/conlang/); override it with --kah or CONLANG_KAH_LEXICON.

On the reference machine, the adapter parses 8,153 entries with a 97.8% validity rate.

Evolving a language

A generated model can be the parent of a daughter, evolved by ordered sound changes. A rule is old > new (new empty deletes the segment), optionally conditioned by an environment after /. The environment names the context on each side of the target, with _ marking the target slot, # the word boundary, and V/C the vowel/consonant classes:

conlang generate --spec spec\example.yaml --out out\proto.json
conlang evolve --model out\proto.json --rule "a > e" --rule "p > f / V_V" --out out\daughter.json
conlang evolve --model out\proto.json --rule "t > d / #_" --rule "s > z / V_" --out out\voiced.json

V_V is "between vowels", #_ is word-initial, _# is word-final. Rules are applied left to right so a later rule sees an earlier one's output (feeding).

The daughter is a full model: its inventory gains any new sounds, its lexicon and affixes are rewritten, and its word forms are rebuilt. It records where it came from in lineage:

"lineage": { "parent": "proto", "rules": ["a > e", "p > f / V_V"] }

That is the whole point of the field: the derivation is data, so a daughter can itself be evolved, and accept keeps the chain across a round trip. Mergers introduced by a change show up as lexicon.homophones warnings, never errors.

Syntax

Each language samples a constituent order (SOV, SVO, or VSO, weighted toward the first two) and an alignment (accusative or ergative). Both are recorded in the model and survive accept. A transitive clause is linearised from the lexicon and inflected: the subject and object take case by alignment (accusative marks NOM/ACC; ergative marks ERG/ABS) and the verb agrees with the subject:

from conlang.syntax import sample_clause
words, gloss = sample_clause(language)
# e.g. (['pyople', 'klavragprekbra', 'blonyto'], 'water.ERG say.3SG fire.ABS')

Case and agreement are applied when the clause is built, not woven into every word's standing paradigm — a deliberate split, so a noun's form count does not double again for case.

Layout

src/conlang/
  model/        canonical pydantic schema (the source of truth)
  sampling/     deterministic RNG, inventory presets, concepts, defaults
  phonology/    tokenization, syllabification, legality checks
  lexicon/      seeded root generation, stable per-item seeding
  morphology/   affixes, inflection classes, and word-form assembly
  soundchange/  ordered sound changes; derive a daughter language
  syntax/       word order, alignment, simple clause linearisation
  render/       Markdown dictionary + grammar sketch
  lint/         machine-checkable invariants
  bench/        Kah adapter + scorecard
  cli.py        thin command-line view over the library

Development

.\.venv\Scripts\python.exe -m pytest
.\.venv\Scripts\python.exe -m ruff check .
.\.venv\Scripts\python.exe -m mypy src

Roadmap

Shipped so far:

  • Phonology — inventory, phonotactics, seeded word generation.
  • IPA inventories — declare the inventory phonemic (ipa: true) and the tool derives a romanised orthography alongside the phonemic forms.
  • Morphology — affix breadth, inflection classes, and stacking (a word can carry several categories at once).
  • Diachrony — conlang evolve: ordered sound changes derive a daughter language, now with environments (p > f / V_V, #, _), and the chain recorded in lineage.
  • Syntax — sampled word order and alignment, case marking with subject agreement, the predication tier (adpositions, noun phrases, intransitives, copula/existential), clause sequencing (parataxis and conjunctions), and aspect (progressive/perfect, stacking with tense).
  • Translation pipeline — conlang new / grow / translate / validate-translation / extras: build a generated language and translate the fixed story into it, including a grammar-match port from a sibling language.

Next:

  • Optional data packs: PHOIBLE (inventories) and WALS (typology) as CC-BY adapters behind the built-in defaults.
  • Semantics/pragmatics and writing systems (deferred from v1).

License

MIT (code). Kah dictionary and grammar content remains Kwesho's and is not part of this repository.

Metadata

Release files for conlanggen 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for conlanggen 0.4.2
File Size Uploaded
conlanggen-0.4.2.tar.gz 99.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for conlanggen 0.4.2
File Interpreter ABI Platform
conlanggen-0.4.2-py3-none-any.whl Python 3 none any Details

Total release size: 170.2 kB

Release files / conlanggen-0.4.2.tar.gz

Download URL conlanggen-0.4.2.tar.gz
Size 99.2 kB
Tags Source
SHA-256 checksum
How to use checksums
26bb7d2f1bc9f609cf9ff8a5d5b12822a268b2d3d4c3d186a6987b9b408727bf
BLAKE2b-256 checksum
How to use checksums
7823c00eefe8b2bba411b9ba86df984685e403a5a0184b1acd68d7052f826f11
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / conlanggen-0.4.2-py3-none-any.whl

Download URL conlanggen-0.4.2-py3-none-any.whl
Size 71.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5a7738b629f0ff8774df25f19b414e75ffa761bb60fa4da8d20c4ee537acd579
BLAKE2b-256 checksum
How to use checksums
ee091efa0dd093e8a4dbbc950263d2c7ec78f376ac705709b6e004b7203cd4b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

1.12.0

2 release files

1.11.1

2 release files

1.11.0

2 release files

1.10.0

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.3

2 release files

This release

0.4.2 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page