This guide walks you through creating, developing, and publishing a package for the T language.
You can create a new package interactively using the
t init --package command.
$ t init --package advanced-stats
Initializing new T package...
Author [User]: Alice
License [EUPL-1.2]: EUPL-1.2
✓ Package 'advanced-stats' created successfully!When running t init, you will be prompted to select an
AI Agent Context Level. This generates two essential
files in your package root:
These files are designed to be read by LLMs (like Antigravity, Claude, or ChatGPT) at the start of a session to give them immediate, high-fidelity context about your package and the specific version of T you are using.
This creates a standard directory structure:
main.t).test-advanced-stats.t).index.md).Dependencies are declared in DESCRIPTION.toml. To add a
dependency on another T package (e.g., math or a git
repository):
[dependencies]
# Example: depend on a git repository
my-lib = { git = "https://github.com/user/my-lib", tag = "v1.0.0" }You can also declare system-level tools and LaTeX packages required for your package development or documentation.
Under [additional-tools], you can add any package from
Nixpkgs. These tools will be available in your nix develop
shell:
[additional-tools]
# Tools for building, documenting, or testing your package
packages = ["git", "jq", "gawk", "pandoc"]If your documentation requires LaTeX (e.g., for formulas), use the
[latex] section. T provides texlive based on
scheme-small. You only need to list additional
packages:
[latex]
# LaTeX packages for math formulas or advanced formatting
packages = ["amsmath", "blindtext", "physics"]After modifying dependencies or updating the
[additional-tools] or [latex] sections, run
t update to sync your flake.nix and lock
file:
$ t update
Syncing 1 dependency(ies) from DESCRIPTION.toml → flake.nix...
Running nix flake update...This regenerates flake.nix so new dependencies appear as
proper flake inputs, then locks them. After updating, re-enter the
development shell:
$ nix developWrite your T code in src/. For example,
src/stats_helpers.t:
-- src/stats_helpers.t
-- Public by default — importers can use this
weighted_mean = \(x, w) sum(x .* w) / sum(w)
--# Internal helper, not for public use.
--# @private
_validate_weights = \(w) {
assert(length(w) > 0)
}
All top-level bindings in your package are public by
default. To hide an internal helper, add @private
to its T-Doc block.
You can test your code interactively in the REPL:
$ t repl
T> import "src/stats_helpers.t"
T> weighted_mean([1, 2, 3], [0.5, 0.3, 0.2])
1.7T has a built-in test runner. Tests are .t files in the
tests/ directory.
Example tests/test-mean.t:
import "src/stats.t"
-- Using specialized expect_* functions provides rich diagnostic diffs when tests fail:
assert(expect_equal(stats.mean([1, 2, 3]), 2.0))
assert(expect_equal(stats.mean([-1, -1]), -1.0))
Run all tests with:
$ t test
# Structured JSON output for agents and automation:
$ t test --json
# JUnit XML output for CI/CD:
$ t test --format junit
# Run only specific tests:
$ t test --only "stats" # run tests matching "stats"
$ t test --not "slow" # skip tests matching "slow"
# Stop on first failure:
$ t test --failfast
# List discovered tests without running:
$ t test --list
# Mark tests exceeding 30s as failed:
$ t test --timeout 30
# Generate coverage summary (requires instrumented build):
$ t test --coverageCreate tests/.tignore to automatically exclude test
files (one pattern per line):
# tests/.tignore
slow_integration.t
*_benchmark.t
legacy/
Share expensive setup (like loading large datasets) across tests
using chain():
fixture = pipeline {
data = node(command = read_csv("data/large.csv"), serializer = ^csv)
}
test_a = pipeline { ... }
test_b = pipeline { ... }
build_pipeline(chain(fixture, parallel(test_a, test_b)))
Each node runs in an isolated Nix sandbox. The fixture’s output is
available to downstream test nodes via the dependency DAG — no redundant
read_csv() wrapper needed. See the API reference for a full
example.
In the REPL, t_test() returns a DataFrame with test
results:
results = t_test()
results |> filter($status == "failed")
-- Filter from the REPL
results = t_test(only = ["arithmetic"])
results = t_test(not = ["slow"])
expect_* Functions with assert()?While a plain boolean check like
assert(colnames(df) == ["a", "b", "c"]) works, it only
evaluates to true or false. When it fails,
assert produces a generic error
(AssertionError: expression evaluated to false), providing
no detail on what differed.
By contrast, expect_* functions (such as
expect_equal, expect_colnames,
expect_nrow, expect_type, etc.) perform deep
structural comparisons and provide rich diagnostic diffs:
-- Plain assert: fails with unhelpful generic "expression evaluated to false"
assert(colnames(df) == ["a", "b", "c"])
-- Recommended: produces exact structural diff on failure (e.g. expected "b" at index 2, got "x")
assert(expect_colnames(df, ["a", "b", "c"]))
assert(expect_equal(colnames(df), ["a", "b", "c"]))
expect_* functions also return first-class
Expect values (Expect_pass,
Expect_stop msg, Expect_hold msg) that allow
soft failure, NA handling, or programmatic inspection before passing to
assert().
Since pipeline nodes are compiled and executed via Nix, tests that
build or run pipelines might fail or block in sandboxed environments or
systems lacking Nix. You can conditionally skip the execution of
specific pipeline nodes using the noop parameter with any T
expression.
Because noop accepts T expressions (evaluated at
runtime), you can pass conditions referencing environment variables:
import my_package
p = pipeline {
heavy_node = node(
command = <{ run_heavy_nix_job() }>,
# Skip execution if not running in CI
noop = (get(env("CI"), "") == "")
)
}
res = build_pipeline(p)
# Because errors are first-class values in T, skipped nodes propagate VError values cleanly.
# You can check if the node was skipped using expect_error or custom checks:
assert(expect_error(read_node(res.heavy_node), class = "TypeError", message = "was skipped"))
T packages use T-Doc, a comment-based documentation system. Documentation lives in source files close to the code and is generated into Markdown.
Use --# comments above your functions to document
them.
--# Calculate the square of a number.
--#
--# @param x :: Integer
--# The input number.
--#
--# @return :: Integer
--# The squared result.
--#
--# @example
--# square(4)
--# -- 16
--#
--# @export
fn square(x) {
x * x
}
Supported Tags: -
@param <name> :: <type> <description>:
Document a parameter. -
@return :: <type> <description>: Document the
return value. - @example: Start a code example block. -
@seealso <func1>, <func2>: Link to related
functions. - @family <name>: Group related functions
together. - @private: Mark the function as private — it
will not be visible to importers. - @export: Explicitly
mark as public (this is the default, so usually not needed).
To generate the documentation files in
docs/reference/:
$ t doc --parse --generateThis will: 1. Scan your src/ directory for
--# blocks. 2. Generate Markdown files for each function in
docs/reference/. 3. Generate a
docs/reference/index.md listing all exported functions.
You can view your documentation locally using:
$ t docsThis opens docs/index.md (or README.md) in
your system viewer. You can link to your reference documentation from
there.
Before publishing, run t doctor to check your package
for common issues:
$ t doctor
✓ Everything looks good!It checks for:
DESCRIPTION.toml,
flake.nix).When you are ready to release a version:
DESCRIPTION.toml has the correct
version.CHANGELOG.md with release notes for that
version.t publish.$ t publish
Preparing to publish version 0.1.0...
✓ Validation complete.
Proceed to tag and push v0.1.0? [y/N] y
✓ Tag v0.1.0 pushed to remote.This will run your tests, verify the changelog, and push a git tag to your repository.
Once published, other packages or projects can depend on yours by
adding it to their DESCRIPTION.toml or
tproject.toml:
[dependencies]
advanced-stats = { git = "https://github.com/user/advanced-stats", tag = "v0.1.0" }Then in their T code, they can import your package:
-- Import everything (all public functions)
import advanced_stats
-- Import only specific functions
import advanced_stats[weighted_mean]
-- Import with aliases
import advanced_stats[wmean=weighted_mean]
Functions marked with @private in your package are not
visible to importers.
Now that you know how to build packages, explore how to ensure your work is reproducible and understand T’s underlying architecture: