DEV Community

Cover image for 8 Powerful Python Techniques for Building Custom Languages and Domain-Specific Interpreters
Aarav Joshi
Aarav Joshi

Posted on

8 Powerful Python Techniques for Building Custom Languages and Domain-Specific Interpreters

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Creating tailored languages and interpreters in Python allows me to solve specialized problems with elegant, readable tools. When building domain-specific tools, I focus on techniques that maintain Python's clarity while extending its capabilities. Here are eight methods I regularly use, each with practical applications.

Text command parsing turns natural language into actions. I often use regex with dataclasses to process user inputs cleanly. This approach works well for chatbots and CLI tools where intuitive commands matter.

Enter fullscreen mode Exit fullscreen mode


python
import re
from dataclasses import dataclass

@dataclass
class Command:
verb: str
subject: str
modifiers: dict

def interpret(input_text):
cmd_pattern = r"^(?P\w+)\s+(?P\w+)(?:\s+using\s+(?P.*))?$"
match = re.match(cmd_pattern, input_text)
if not match:
return None

mods = {}
if mod_str := match.group("mods"):
    pairs = [p.split(":") for p in mod_str.split(";")]
    mods = {k.strip(): v.strip() for k,v in pairs}

return Command(
    verb=match.group("verb").lower(),
    subject=match.group("subject").lower(),
    modifiers=mods
)
Enter fullscreen mode Exit fullscreen mode

Example usage

user_cmd = interpret("resize image using width:800; height:600")
print(f"Action: {user_cmd.verb}, Object: {user_cmd.subject}, Settings: {user_cmd.modifiers}")


Operator overloading creates intuitive domain objects. By defining special methods like `__add__` or `__mul__`, I build expressive APIs for scientific computing. This technique makes complex operations feel native.

Enter fullscreen mode Exit fullscreen mode


python
class ChemicalCompound:
def init(self, elements):
self.composition = elements

def __add__(self, other):
    new_comp = {}
    for elem, count in {**self.composition, **other.composition}.items():
        new_comp[elem] = self.composition.get(elem,0) + other.composition.get(elem,0)
    return ChemicalCompound(new_comp)

def __repr__(self):
    return "+".join(f"{count}{elem}" for elem, count in self.composition.items())
Enter fullscreen mode Exit fullscreen mode

Chemical reaction simulation

water = ChemicalCompound({"H":2, "O":1})
oxygen = ChemicalCompound({"O":2})
reaction = water + oxygen
print(reaction) # 2H+3O


AST transformations modify code behavior during compilation. I use Python's `ast` module to inject domain logic directly into the parse tree. This is powerful for adding custom optimizations.

Enter fullscreen mode Exit fullscreen mode


python
import ast

class LogInjector(ast.NodeTransformer):
def visit_FunctionDef(self, node):
log_stmt = ast.Expr(value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[ast.Constant(value=f"Calling {node.name}")],
keywords=[]
))
node.body.insert(0, log_stmt)
return node

Adds logging to functions automatically

source_code = """
def calculate(a, b):
return a * b
"""
tree = ast.parse(source_code)
modified = LogInjector().visit(tree)
exec(compile(modified, "", "exec"))
calculate(3, 4) # Prints "Calling calculate"


Parser combinators handle complex grammars elegantly. Libraries like `parsy` let me construct recursive parsers through composition. I find this ideal for SQL-like mini-languages.

Enter fullscreen mode Exit fullscreen mode


python
from parsy import string, regex, seq

Config file parser

key = regex(r"[a-zA-Z_][\w]*")
value = regex(r"[^\n]+")
assignment = seq(
key << string("="),
value
).combine(lambda k, v: (k, v.strip()))

config_parser = assignment.sep_by(regex(r"\s*"))

Parsing key-value pairs

config_data = config_parser.parse("""
color = blue
size = large
shape = circle
""")
print(dict(config_data)) # {'color':'blue','size':'large','shape':'circle'}


Symbol tables manage execution contexts. I implement custom environments for safe evaluation, which is crucial when processing untrusted inputs.

Enter fullscreen mode Exit fullscreen mode


python
class SafeEnvironment:
def init(self):
self.variables = {}
self.allowed_functions = {"min": min, "max": max}

def set(self, name, value):
    self.variables[name] = value

def run(self, expr):
    return eval(expr, {"__builtins__": None}, {**self.variables, **self.allowed_functions})
Enter fullscreen mode Exit fullscreen mode

Restricted evaluation

env = SafeEnvironment()
env.set("x", 10)
env.set("y", 20)
result = env.run("min(x, y) + 5")
print(result) # 15


Metaclasses shape class behavior at definition time. I use them to enforce domain rules automatically, such as validation for financial models.

Enter fullscreen mode Exit fullscreen mode


python
class FieldValidator(type):
def new(cls, name, bases, dct):
fields = [k for k, v in dct.items() if isinstance(v, Field)]
dct['fields'] = fields
return super().
new_(cls, name, bases, dct)

class Field:
def init(self, min_val, max_val):
self.min = min_val
self.max = max_val

class Trade(metaclass=FieldValidator):
amount = Field(1, 10000)

def __init__(self, amount):
    if not (self._fields[0].min <= amount <= self._fields[0].max):
        raise ValueError("Invalid trade amount")
    self.amount = amount
Enter fullscreen mode Exit fullscreen mode

Usage

try:
t = Trade(15000) # Raises ValueError
except ValueError as e:
print(e)


Recursive descent parsers handle nested structures. When I need full control over parsing, I implement token-by-token processing.

Enter fullscreen mode Exit fullscreen mode


python
class MathParser:
def init(self, expression):
self.tokens = iter(expression.replace(" ", ""))
self.current = next(self.tokens, None)

def advance(self):
    self.current = next(self.tokens, None)

def parse(self):
    return self.expr()

def expr(self):
    result = self.term()
    while self.current in ('+', '-'):
        op = self.current
        self.advance()
        term = self.term()
        result = result + term if op == '+' else result - term
    return result

def term(self):
    result = self.factor()
    while self.current in ('*', '/'):
        op = self.current
        self.advance()
        fac = self.factor()
        result = result * fac if op == '*' else result / fac
    return result

def factor(self):
    if self.current == '(':
        self.advance()
        result = self.expr()
        if self.current != ')':
            raise SyntaxError("Mismatched parentheses")
        self.advance()
        return result
    else:
        return self.number()

def number(self):
    num_str = ''
    while self.current and self.current.isdigit():
        num_str += self.current
        self.advance()
    return int(num_str)
Enter fullscreen mode Exit fullscreen mode

Calculate expression

calc = MathParser("(3+2)*4")
print(calc.parse()) # 20


Decorators extend functions for domain tasks. I wrap core logic with context managers to handle resources like database connections automatically.

Enter fullscreen mode Exit fullscreen mode


python
def database_transaction(func):
def wrapper(*args, **kwargs):
print("Opening database connection")
result = func(*args, **kwargs)
print("Committing transaction")
return result
return wrapper

@database_transaction
def save_record(data):
print(f"Persisting {data}")

Automated transaction handling

save_record({"id": 101, "status": "active"})


These techniques form a versatile toolkit for building specialized languages. Each approach balances expressiveness with Python's inherent readability. When I design domain-specific tools, I start with the simplest method that solves the problem, gradually adopting more advanced techniques as requirements evolve. The real power comes from combining these approaches - like using parser combinators with AST transformations or decorators with operator overloading. This flexibility lets me create solutions that feel like natural extensions of Python rather than foreign constructs.
Enter fullscreen mode Exit fullscreen mode

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)