textX: build a DSL in Python from one grammar string
Domain-Specific Languages and parsers in Python made easy http://textx.github.io/textX/
At a glance
- What is it?
- textX is a Python meta-language for domain specific languages, inspired by Xtext and implemented on the Arpeggio PEG parser, so a single grammar string yields both a parser and a meta-model of Python classes. It fits teams that already speak Python and want a parser without a compiler toolchain, and it does not fit projects that need static analysis or an IDE-grade language server out of the box.
- Who is it for?
- Adopt textX when your team writes Python daily and needs a parser for a config format, DSL or simple language without adopting a JVM toolchain. Do not adopt it when you need type checking, static analysis or a maintained language server, because textX-LS is a separate project and textX-ninja is explicitly unmaintained.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 52 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One grammar, two outputs: parser and meta-model
textX is described as a meta-language for building domain specific languages in Python, inspired by Xtext. The core promise is that from a single language description, which the documentation calls a grammar, textX builds two things: a parser and a meta-model, in other words an abstract syntax.
That distinction is what makes it a meta-language rather than a parser library. You are not writing a grammar for one parser and separately hand coding the object model. You describe the language once, and the shape of the object graph is derived from the same description. Rules you name in the grammar become classes you instantiate, and attributes you assign in the grammar become attributes on those classes.
The audience is Python developers who need to read a textual format. The README frames it as helping you build your own language or build support for an already existing textual language or file format. The repository ships examples for Entity, IBM Rhapsody, StateMachine, drawing, expression, hello_world, json, pyFlies, robot and workflow, which is a good indication of the range: from a teaching example to a real model interchange format.
The implementation is pure Python, using the Arpeggio PEG parser. The stated consequences are no grammar ambiguities, unlimited lookahead and an interpreter style of work.
The drawing grammar from the README, line by line
The README's quick intro is a complete working example, and reading it tells you more about the grammar syntax than any summary would. The grammar for a small drawing language looks like this:
grammar = """
Model: commands*=DrawCommand;
DrawCommand: MoveCommand | ShapeCommand;
ShapeCommand: LineTo | Circle;
MoveCommand: MoveTo | MoveBy;
MoveTo: 'move' 'to' position=Point;
MoveBy: 'move' 'by' vector=Point;
Circle: 'circle' radius=INT;
LineTo: 'line' 'to' point=Point;
Point: x=INT ',' y=INT;
"""Several Xtext idioms are visible here. A rule name followed by a colon and an expression defines a rule. Alternatives are separated with a pipe. Assignment with the equals sign binds a parsed value to an attribute, so position=Point gives the MoveTo rule a position attribute. The star on commands*=DrawCommand marks it as a list attribute, which is how the model ends up with model.commands rather than a single command. A literal keyword in single quotes matches that exact word.
Arpeggio supplies the built-in terminals, which is why INT appears without a definition. The grammar is small enough to hold in your head, and that is precisely the point of the library: you write this and you get a working language.
Custom classes alongside dynamically generated ones
Not every rule should become an anonymous generated class. The example supplies its own Point class, with an initializer taking parent, x and y, and a string form:
class Point:
def __init__(self, parent, x, y):
self.parent = parent
self.x = x
self.y = y
def __str__(self):
return "{},{}".format(self.x, self.y)The class is then handed to the meta-model constructor so that the Point rule instantiates your class instead of a generated one:
mm = metamodel_from_str(grammar, classes=[Point])The generated classes carry a parent reference, which is how you can walk upward in a tree, and the supplied Point follows the same convention so it composes with the generated ones. The example goes further and gives Point an __add__ method so that adding two points yields a new Point with the parent taken from the left operand. That is ordinary Python operator overloading, which is one of the practical advantages of targeting a dynamically typed object model instead of compiling to a typed backend.
The cost of the dynamic approach is that nothing checks your grammar against your code at type check time. If a rule is renamed and the interpreter still refers to the old attribute name, the failure appears at runtime as an attribute error, not at compile time.
Parsing, interpreting and walking the model
Parsing a document is one call on the meta-model, and the result is a plain Python object graph:
model_str = """
move to 5, 10
line to 10, 10
line to 20, 20
move by 5, -7
circle 10
line to 10, 10
"""
# Meta-model knows how to parse and instantiate models.
model = mm.model_from_str(model_str)The README is explicit about what you get at this point: instances of dynamically created classes with attributes following the grammar. There is no separate compile step, no serialization format for the model and no code generation to regenerate. The interpretation loop is ordinary Python that inspects the class name of each command:
def cname(o):
return o.__class__.__name__The example then branches on MoveTo, MoveBy and Circle, tracks a position value, and prints a running trace of the drawing commands. For searching the model there is a helper that walks from the root and collects every instance of a given type:
# Collect all points starting from the root of the model
points = get_children_of_type("Point", model)
for point in points:
print('Point: {}'.format(point))That helper is the kind of small utility that usually ends up reimplemented in every DSL project. Having it in the library, along with the parent links on every node, means traversal code does not need to know anything about your grammar.
PEG semantics: what changes when you leave Xtext
textX follows the syntax and semantics of Xtext but differs in some places, and the documentation links to a comparison page for the details. The single most consequential difference is the parsing engine. Xtext builds an ANTLR-style parser, while textX uses Arpeggio, a PEG parser.
The README lists the properties that follow from that choice: no grammar ambiguities, unlimited lookahead and an interpreter style of work. The first two are the practical wins for a hand written grammar. A PEG parser resolves conflicts by ordered choice rather than by reporting ambiguity, so your grammar will not fail to build because two rules could match the same text, it will silently take the first alternative that matches. That is a genuine ergonomic gain, but it is also a debugging hazard: an ordering mistake in your grammar produces wrong parses instead of a build error.
Unlimited lookahead means the parser can look as far ahead as a rule needs, which lets you write rules that distinguish tokens by context without restructuring the grammar. The interpreter style of work means parsing speed is not the goal, and a parser that walks the input repeatedly will be slower than a table driven one on large documents.
The pure Python implementation is the other side of the same coin. You get no compile step and no external runtime, which makes textX easy to embed in an application, but you also do not get the static analysis tooling that comes with a compiled language infrastructure.
Running the test suite with just and uv
The repository uses a justfile with uv for a repeatable developer workflow. The lint recipe runs ruff over the package, the functional tests and the examples:
uv run --no-default-groups --group test ruff check {{ flags }} textx/ tests/functional examples/The test recipe runs the functional test directory with pytest, and the coverage recipe runs coverage with the textx package as the source and then enforces a threshold:
uv run --no-default-groups --group test coverage run --source textx -m pytest tests/functional
uv run --no-default-groups --group test coverage report --fail-under 90That 90 percent coverage gate is the most useful number in the repository for an evaluator, because it tells you the maintainers hold the functional suite to a coverage floor rather than letting it drift. The types recipe runs mypy over the package, and the check recipe chains check-format, lint, types and coverage together.
For the package itself, the pyproject.toml pins requires-python to 3.9 or newer and lists Python 3.9 through 3.14 in the classifiers, with a single runtime dependency on Arpeggio at version 3.0.0 or newer plus importlib-metadata for Python versions below 3.10. The build backend is flit_core. Note the difference between that floor and the README statement that it is tested for 3.8 and newer, which is a leftover from an earlier metadata version.
The published version is 4.4.0 and the package is named textX on PyPI.
Editor support that lives in separate repositories
textX itself is a library, so the tooling around it is a set of sibling projects, and the README is candid that they are in progress.
textX-LS provides Language Server Protocol support and VS Code integration for any textX based language, and it is described as being about to supersede two earlier efforts: textX-languageserver, which also targeted the Language Server Protocol, and textX-extensions, which provided syntax highlighting and a code outline. For visualising a textX language there is viewX, a separate VS Code extension for building visualizers. Vim users have textx.vim, and Emacs users have textx-mode, which is also available through MELPA.
One entry in that list is a warning. The textX-ninja project is described as currently unmaintained, and it is listed under support in IDE and editors, so a reader who picks it up inherits abandoned code.
There is also a playground at textx.github.io/textx-playground with a dropdown of examples, which is the fastest way to feel out a grammar before installing anything. For general questions the project asks for GitHub Discussions, and for bugs the GitHub issue tracker.
The documentation is at textx.github.io/textX/stable, and the repository ships a docker directory and a serve-docs.sh script for building the site locally.
MIT terms, a citation request and a maintenance timeline
The licence is MIT, declared in the pyproject.toml metadata and stated in the README, with the text in LICENSE.txt at the repository root. MIT is permissive, so a grammar you write and the models it produces carry no obligation back to the project.
There is one request attached. If you use textX in a research project, the README asks that you cite the paper by Dejanović, Vaderna, Milosavljević and Vuković, published in Knowledge-Based Systems, volume 115, pages 1 to 4, with a DOI link. That is a request rather than a licence condition, but if you are publishing academic work the citation is expected.
On maintenance, the repository is not archived and the last push was on 2026-09-01. The release list shows 4.4.0 on 2026-07-08, 4.3.0 on 2025-11-25 and 4.2.3, a bugfix release, on 2025-10-09. The version scheme has been on a 4.x line for a while and the changelog lives in the repository as CHANGELOG.md.
Because the last push is on 2026-09-01, some months before now, describe the project as recently pushed rather than actively developed. What you can say concretely is that 4.4.0 shipped in July 2026 and the repository has an active CI workflow with a build badge, a coverage badge and a docs badge.
Editorial conclusion
Adopt textX when your team writes Python daily and needs a parser for a config format, DSL or simple language without adopting a JVM toolchain. Do not adopt it when you need type checking, static analysis or a maintained language server, because textX-LS is a separate project and textX-ninja is explicitly unmaintained. Verify first that the grammar you write is unambiguous under a PEG parser, that the Python version on your machine satisfies requires-python of 3.9 or newer, and that the MIT licence and its citation request both sit comfortably with your project.
Frequently asked questions
What is textX and what does a grammar produce?
textX is a meta-language for building domain specific languages in Python. From a single grammar string it builds a parser and a meta-model, so the parsed result is a plain Python object graph of dynamically created classes.
Which Python versions does textX support?
The pyproject.toml sets requires-python to 3.9 or newer and lists classifiers for Python 3.9 through 3.14. The README says it is tested for 3.8 and newer, which reflects older metadata.
How is textX different from Xtext?
textX follows Xtext syntax and semantics but differs in places, and it is implemented in Python on the Arpeggio PEG parser rather than a generated ANTLR parser, which the README lists as giving no grammar ambiguities, unlimited lookahead and interpreter style work.
Can I supply my own Python classes to a textX grammar?
Yes. You pass them to the meta-model constructor, for example metamodel_from_str(grammar, classes=[Point]), and the matching rule instantiates your class instead of a generated one.
Does textX have IDE support?
Support lives in separate projects. textX-LS provides Language Server Protocol and VS Code support, viewX builds visualizers, and there is a vim plugin and an Emacs mode, while the textX-ninja project is explicitly described as currently unmaintained.
How do I run the textX tests?
The justfile runs the functional suite with pytest under uv, for example `uv run --no-default-groups --group test pytest tests/functional`, and the coverage recipe enforces a 90 percent threshold over the textx package.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/textx-textx)Was this useful?