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.
  • Writing systems — conlang script: a language samples a script (alphabet / syllabary / impure abjad / featural) from its phonology, the tool generates deterministic SVG glyphs as a derived view, and renders a script chart plus the lexicon written out (the recall deck shows glyphs too).
  • Data packs — optional, pinned PHOIBLE (inventories + features) and WALS (typology) ground sampling in real data (conlang data fetch), with a graceful fallback to the built-in presets when absent.
  • Feature-driven phonology — natural classes (obstruent, nasal, …) usable in onset/coda constraints, and feature-based sound-change rules ([-sonorant, -continuant] > [+continuant] / V_V) in conlang evolve; a changed vector resolves back to a symbol via the feature layer.
  • Place structure — each predicate carries an ordered list of semantic roles (Lexeme.places): verbs 1–3 places, the copula THEME, ATTRIBUTE, adpositions THEME + a spatial role. A ## Place structure view lists them, and clauses are role-labelled (water.ERG.AGENT give.3SG fire.ABS.PATIENT) — orthogonal to case.
  • Semantics — the logical operators (negation as a preverbal particle, quantifiers as NP modifiers, coordination with or/nor) and information structure (topic / focus markers), all closed function-word classes that add a word without reordering anything; a ## Semantics view lists the inventories and worked clauses.

Next:

  • Formal semantics (truth-conditional model theory) — deferred.

License

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

Metadata

Release files for conlanggen 0.9.0

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.9.0
File Size Uploaded
conlanggen-0.9.0.tar.gz 176.7 kB Details

Built distribution (wheel)

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

Total release size: 265.4 kB

Release files / conlanggen-0.9.0.tar.gz

Download URL conlanggen-0.9.0.tar.gz
Size 176.7 kB
Tags Source
SHA-256 checksum
How to use checksums
277dd96470c04b6d4ac2b4e8e5ce3415ef6844d6252ff31601c91964cdbef145
BLAKE2b-256 checksum
How to use checksums
500de37b338fe5168cffcc4289bd54c333900a22c11c480a1750f1a9c81f178f
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 11, 2026.

Transparency log

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

Download URL conlanggen-0.9.0-py3-none-any.whl
Size 88.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cf98578f232ae3db6bc4a45f9cc1af5aff609d1fe14e786bf0458bed43161598
BLAKE2b-256 checksum
How to use checksums
f0025db997be5bf56d9182928acf88df80904559f4141cb1bef711279382f225
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 11, 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

This release

0.9.0 This release

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

0.4.2

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