Skip to the content.

TAPL is an extensible typed programming language that compiles to Python. It gives you a type system powerful enough to catch bugs that most type systems can’t – while keeping the full Python ecosystem at your fingertips.

Here’s what makes it different:

Installation

TAPL requires Python 3.9 or higher. It has no third-party dependencies – only the Python standard library.

pip install tapl-lang

Verify the installation:

tapl --help

Important:

Hello World

Create a file called hello_world.tapl:

language pythonlike

print('Hello World!')

Every TAPL file starts with a language directive that tells the compiler which grammar to use. The built-in pythonlike language gives you a typed, Python-like syntax.

Run it:

tapl hello_world.tapl
# Output: Hello World!

Behind the scenes, TAPL generates two Python files from your source:

TAPL runs the type-checker first. If it finds problems, you get error messages and the runtime code never executes. If everything checks out, TAPL considers the runtime code safe and runs it.

Tip: You can always open the generated .py files to see exactly what TAPL produced. This is handy for debugging when something doesn’t behave as expected.

Language Basics

If you know Python, you already know most of TAPL. Variables, functions, classes, collections, if/for/while, try/except/finally, and imports all work the way you’d expect. You can import other .tapl files the same way you import Python modules, and the imported file is also compiled and type-checked.

Here’s a quick example that shows the familiar syntax:

language pythonlike

class Dog:
    def __init__(self, name: Str):
        self.name = name

    def bark(self) -> Str:
        return self.name + ' says Woof! Woof!'

my_dog = Dog('Buddy')
print(my_dog.bark())

The differences are small but important:

CamelCase Type Names

TAPL uses Int, Str, Bool, Float instead of Python’s lowercase int, str, bool, float:

language pythonlike

x: Int = 42
name: Str = 'hello'
pi: Float = 3.14

Parameterized Types Use Function-Call Syntax

Where Python writes list[int], TAPL writes List(Int). Nesting works the same way: List(List(Int)) for a list of lists.

The ! Operator: Classes vs. Instances

Python type checkers use type[Dog] to distinguish the class from an instance. TAPL takes a different approach with the ! operator:

def greet_dog(dog: Dog!) -> Str:
    return 'Hello, ' + dog.name + '!'

def make_dog(factory: Dog, name: Str) -> Dog!:
    return factory(name)

greet_dog takes an instance (a dog you already created). make_dog takes the class itself and uses it as a factory to create a new instance.

Type Errors

Here’s TAPL’s type checker in action. Create a file called type_error.tapl:

language pythonlike

def one() -> Str:
    return 0

TAPL catches this at compile time – the runtime code never executes:

Return type mismatch: expected Str, got Int.

Since the type-checker is itself generated Python (type_error1.py), you can open it, step through it with a debugger, and see exactly how this error is raised.

Dependent Types with Matrices

Most type systems can only check things like “this is an integer” or “this is a list of strings.” TAPL goes further: it can check properties that depend on actual values. Imagine catching a dimension mismatch in matrix multiplication before your code even runs.

This section uses two special TAPL operators. Here’s what they do:

Defining a Dimension-Parameterized Matrix

The Matrix(rows, cols) function creates a class whose type is tagged with its dimensions:

language pythonlike

def Matrix(rows, cols):

    class Matrix_:
        class_name = ^'Matrix({},{})'.format(rows, cols)

        def __init__(self):
            self.rows = rows
            self.cols = cols
            self.num_rows = <rows:Int>
            self.num_cols = <cols:Int>
            self.values = <[]:List(List(Int))>
            for i in range(self.num_rows):
                columns = <[]:List(Int)>
                for j in range(self.num_cols):
                    columns.append(0)
                self.values.append(columns)

        def __repr__(self):
            return str(self.values)

    return Matrix_

A few things to note:

Type-Safe Function Signatures

Now you can write functions where the compiler checks dimensions for you:

def accept_matrix_2_3(matrix: Matrix(^2, ^3)!):
    pass

Matrix(^2, ^3)! means “an instance of a 2x3 matrix.” The ^2 and ^3 make the dimensions visible to the type-checker.

You can also write functions that are generic over dimensions:

def add(rows, cols):
    def add_(a: Matrix(rows, cols)!, b: Matrix(rows, cols)!):
        result = Matrix(rows, cols)()
        for i in range(result.num_rows):
            for j in range(result.num_cols):
                result.values[i][j] = a.values[i][j] + b.values[i][j]
        return result
    return add_

Both matrices must have the same dimensions. If you try to add a 2x2 and a 2x3, the compiler will reject it.

Matrix multiplication enforces that the inner dimensions match:

def multiply(m, n, p):
    def multiply_(a: Matrix(m, n)!, b: Matrix(n, p)!):
        result = Matrix(m, p)()
        for i in range(a.num_rows):
            for j in range(b.num_cols):
                for k in range(a.num_cols):
                    result.values[i][j] = result.values[i][j] + a.values[i][k] * b.values[k][j]
        return result
    return multiply_

The first matrix is m by n, the second is n by p, and the result is m by p. The shared n is enforced at compile time.

Using Matrices

def main():
    matrix_2_2 = Matrix(^2, ^2)()
    matrix_2_2.values = [[1, 2], [3, 4]]
    matrix_2_3 = Matrix(^2, ^3)()
    matrix_2_3.values = [[1, 2, 3], [4, 5, 6]]

    accept_matrix_2_3(matrix_2_3)

    print(add(^2, ^2)(matrix_2_2, matrix_2_2))
    print(multiply(^2, ^2, ^3)(matrix_2_2, matrix_2_3))

Run it:

tapl matrix.tapl

See the full working code in matrix.tapl.

Extending the Language

TAPL lets you add your own syntax. You define new grammars by extending existing ones, and TAPL handles both runtime code generation and type-checking for your new syntax automatically.

Here’s a practical example. Deeply nested function calls are hard to read:

print(round(abs(-2.5)))

A pipe operator (|>) would let you write this left-to-right instead. TAPL includes pipeweaver as an example of how to create your own language grammar:

Note: The example below uses language pipeweaver, not pythonlike. This is a custom grammar built on top of pythonlike language.

language pipeweaver

def double(i: Int) -> Int:
    return i * 2

def square(i: Int) -> Int:
    return i * i

3 |> double |> square |> print
3 |> square |> double |> print

Run it:

tapl pipe.tapl

Behind the scenes, TAPL generates standard Python with nested calls:

def double(i):
    return i * 2

def square(i):
    return i * i

print(square(double(3)))
print(double(square(3)))

The pipeweaver language is implemented by subclassing the base grammar and adding custom parsing rules. You can see how it works in the pipeweaver source code. The same approach lets you build any DSL on top of TAPL.

What’s Supported

The goal is to make pythonlike as close to Python as possible. Here’s what works today:

Basics: variables, functions, classes, if/elif/else, for, while, try/except/finally

Types: Int, Str, Bool, Float, NoneType, List(T), Set(T), Dict(K, V), union types (A | B), intersection types (A & B)

Collections: lists, sets, dictionaries (including indexing, append, add, remove, del)

Other: imports between .tapl files, language directive for choosing grammars, custom language extensions

Not yet supported: some parts of the Python standard library, decorators, async/await, comprehensions, *args/**kwargs. TAPL is experimental and the set of supported features grows with every commit.

What’s Next?

Explore TAPL further:

The name TAPL comes from Benjamin C. Pierce’s book Types and Programming Languages, which inspired the project.