t-linter ๐โจ
Intelligent syntax highlighting, validation, and formatting for Python template strings (PEP 750).
๐ฃ ๐ผ Maintainer update: Open to opportunities. ๐ koxudaxi.dev
๐ Documentation
- ๐ฆ Installation - PyPI, VSCode Extension, Build from source
- ๐ Check Command - CLI validation & output formats
- ๐งน Format Command - Canonical formatting for supported templates
- ๐ฅ๏ธ LSP Server - Editor integration (VSCode, Claude Code, Codex, Neovim, etc.)
- ๐งช Interpolation Type Checking - Optional type checker-backed diagnostics
- ๐๏ธ SQL Catalog Cache - PostgreSQL-backed psycopg parameter narrowing
- โ๏ธ Configuration - pyproject.toml & ignore files
- ๐ Supported Languages - HTML, T-HTML, TDOM, SQL, JS, CSS, JSON, YAML, TOML
Overview
t-linter validates and formats embedded languages inside Python template strings (PEP 750). Built with Rust and Tree-sitter for speed, it ships as a single binary that works as both a CLI tool and an LSP server.
t-linter checkโ validate template string syntaxt-linter formatโ canonically reformat supported template literalst-linter sql prepareโ cache PostgreSQL parameter metadata for psycopg SQL type narrowingt-linter lspโ start the Language Server Protocol server for editor integration
Features
- ๐ Linting - Detect syntax errors in embedded HTML, JSON, YAML, TOML, CSS, JavaScript, SQL
- ๐งน Formatting - Canonical formatting for HTML, T-HTML, TDOM, JSON, YAML, TOML templates
- ๐จ Syntax Highlighting - Smart highlighting via LSP semantic tokens
- ๐ง Type-based Detection - Understands
Annotated[Template, "html"], type aliases, and marker classes withtstring_language - ๐งช Interpolation Type Checking - Optional LSP diagnostics for JSON, YAML, TOML, psycopg SQL, and TDOM interpolations through Ty, Pyright, or Pyrefly
- ๐๏ธ SQL Catalog Cache - Narrows psycopg SQL parameters from PostgreSQL metadata, even when the editor session has no live database
- ๐ JSON Schema Binding - Checks JSON template keys and static value shapes against
TypedDictor dataclass models withJson(schema=...) - ๐งฉ Callee Inference - Detects backend languages from helpers such as
tdom.html(...) - ๐ Fast - Single Rust binary with Tree-sitter parsers
Supported Languages
| Language | Annotation | Check | Format | Highlight |
|---|---|---|---|---|
| HTML | "html" |
โ | โ | โ |
| T-HTML | "thtml" |
โ | โ | โ |
| TDOM | "tdom" |
โ | โ | โ |
| JSON | "json" |
โ | โ | โ |
| YAML | "yaml", "yml" |
โ | โ | โ |
| TOML | "toml" |
โ | โ | โ |
| CSS | "css" |
โ | โ | โ |
| JavaScript | "javascript", "js" |
โ | โ | โ |
| SQL | "sql" |
โ | โ | โ |
For HTML, T-HTML, TDOM, JSON, YAML, and TOML, t-linter uses dedicated Rust backends (tstring-* crates) for strict validation and canonical formatting. CSS, JavaScript, and SQL use Tree-sitter for syntax validation and highlighting.
Installation
PyPI (Recommended)
pip install t-linter
Or add to your project's dependencies:
# Using uv (recommended)
uv add t-linter
# Or using pip with requirements.txt
echo "t-linter" >> requirements.txt
pip install -r requirements.txt
This provides the t-linter CLI tool and LSP server.
VSCode Extension
If you use VSCode, install the extension for seamless editor integration:
- Open VSCode โ Extensions (Ctrl+Shift+X / Cmd+Shift+X)
- Search for "t-linter" โ Install "T-Linter" by koxudaxi
- On Linux x64, macOS x64/arm64, and Windows x64, the extension bundles the
t-linterbinary, so no separate PyPI install is required. - On unsupported platforms, or if you want to override the bundled binary, install
t-lintervia PyPI (see above) and sett-linter.serverPathto the full executable path. - Choose one save-time formatting mode:
- Ruff coexistence mode keeps Ruff as the Python formatter and runs t-linter through
source.fixAll.t-linter:{ "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.t-linter": "explicit" } } }
- t-linter formatter mode keeps the existing formatter-only workflow:
{ "[python]": { "editor.defaultFormatter": "koxudaxi.t-linter", "editor.formatOnSave": true } }
Set"t-linter.format.runRuffPipeline": trueif you want this formatter mode to run Ruff fixAll, import organization, Ruff formatting, and then t-linter in one composed formatting transaction. VSCode only allows one default formatter per language, which is why t-linter also exposes a save-time code action lane for template-string formatting.
- Ruff coexistence mode keeps Ruff as the Python formatter and runs t-linter through
- If semantic highlighting conflicts with another Python extension, set the Python language server to
"None"in your workspace settings:{ "python.languageServer": "None" }
If you need those features, you can keep the Python language server enabled. t-linter will still provide template-string diagnostics, code actions, and formatting, though semantic highlighting may conflict.
If you use an external t-linter binary and it is not in your PATH, set t-linter.serverPath in VSCode settings to the full path of the executable.
โ Install from VSCode Marketplace
Build from Source
git clone https://github.com/koxudaxi/t-linter
cd t-linter
cargo install --path crates/t-linter
Usage
Check
Validate template strings for syntax errors:
# Check a single file
t-linter check file.py
# Check a directory
t-linter check src/
# Output formats: human (default), json, github, sarif
t-linter check file.py --format json
t-linter check file.py --format github # GitHub Actions annotations
t-linter check file.py --format sarif # SARIF 2.1.0
# Exit with error code if issues found (useful for CI)
t-linter check file.py --error-on-issues
# Apply or preview suggested edits
t-linter check file.py --fix
t-linter check file.py --diff
Example output:
example.py:4:47: error[embedded-parse-error] Expected a JSON value. (language=json)
1 files scanned, 1 templates scanned, 1 diagnostics, 0 failed files
JSON templates can also be bound to a Python schema marker so t-linter check
validates required keys, unknown keys, and static scalar shapes:
from typing import Annotated, NotRequired, TypedDict
from string.templatelib import Template
from json_tstring import Json
class Order(TypedDict):
id: int
name: str
note: NotRequired[str]
payload: Annotated[Template, Json(schema=Order)] = (
t'{{"id": "abc", "nme": "Ada"}}'
)
Json(schema=Order) declares the JSON template language and binds the schema.
payload: Json[Order] = t"..." is also accepted as a shorthand. Plain string
metadata such as Annotated[Template, "json"] remains supported for
language-only detection, but new code should not combine it with Json(...);
t-linter reports redundant or conflicting language metadata. t-linter reads the
annotation text only; the template is still a normal Python template string.
Exit codes:
| Code | Meaning |
|---|---|
0 |
Run completed successfully |
1 |
Issues were found and --error-on-issues was set |
2 |
Operational failure such as an unreadable file |
Format
Rewrite supported template literals (HTML, T-HTML, TDOM, JSON, YAML, TOML) in place:
# Format a single file
t-linter format file.py
# Format a directory
t-linter format src/
# Check whether formatting would change any files (for CI)
t-linter format --check file.py
# Override the formatter line length
t-linter format --line-length 100 file.py
# Format stdin
cat file.py | t-linter format --stdin-filename file.py -
Templates in unsupported languages (CSS, JavaScript, SQL) are left unchanged.
For CI pipelines that also use Ruff, run the tools explicitly in sequence:
ruff check --fix . && ruff format . && t-linter format .
ruff check . && ruff format --check . && t-linter format --check .
Stats
Count template strings without running diagnostics:
t-linter stats .
t-linter stats . --format json
SQL Catalog Cache
For psycopg SQL templates, t-linter can cache PostgreSQL describe metadata and use it later for LSP interpolation type diagnostics:
[tool.t-linter.sql]
library = "psycopg"
database-url = "env:DATABASE_URL"
search-path = "public"
DATABASE_URL=postgresql://postgres:postgres@localhost/app t-linter sql prepare .
DATABASE_URL=postgresql://postgres:postgres@localhost/app t-linter sql prepare --check .
Commit .t-linter/sql-cache/ with the code. When PostgreSQL is reachable,
--check reports stale or missing cache entries with exit code 2; when the
configured database URL resolves but the database is unreachable and a committed
cache exists, it trusts that cache and exits successfully. The LSP reads the
cache, narrows plain parameters such as {user_id} from PostgreSQL types, and
reports diagnostics on the original interpolation expression.
Set T_LINTER_SQL_PYTHON if python3 on PATH does not have psycopg
installed. See SQL Catalog Cache for the full
workflow.
LSP Server
Start the Language Server Protocol server for editor integration:
t-linter lsp
# Start the LSP server with the composed Ruff pipeline -> t-linter document formatting
t-linter lsp --ruff-pipeline
The LSP server provides:
- Semantic Tokens โ syntax highlighting for embedded languages
- Diagnostics โ real-time validation with 250ms debouncing
- Interpolation Type Checking โ optional JSON, YAML, TOML, psycopg SQL interpolation value diagnostics and TDOM component prop interpolation diagnostics through Ty, Pyright, or Pyrefly
- Document Formatting โ full document and range formatting
- Code Actions โ
source.fixAll.t-linterfor document-level rewrites andrefactor.rewrite.t-linterfor single-template selection rewrites
Interpolation value type checking is opt-in and applies to JSON, YAML, TOML, psycopg SQL templates and TDOM component props. Enable it from an LSP client with initialization options:
{
"typeChecking": {
"enabled": true,
"checker": "pyright",
"command": "/path/to/pyright-langserver",
"args": ["--stdio"]
}
}
When command and args are omitted, t-linter uses the selected checker defaults: ty server, pyright-langserver --stdio, or pyrefly lsp. It searches active virtual environments, workspace .venv/venv, uv projects, and then the checker on PATH.
See Interpolation Type Checking for the architecture, behavior, and implementation details.
Claude Code
Add t-linter as an LSP server in your project's .claude/settings.json:
{
"lsp": {
"t-linter": {
"command": "t-linter",
"args": ["lsp"],
"languages": ["python"]
}
}
}
Claude Code will then use t-linter's diagnostics when editing Python files containing template strings.
Codex
Add the LSP configuration to your project's codex.json or start t-linter's LSP server as part of your development environment. The t-linter check and t-linter format commands can also be used directly in Codex's sandbox:
t-linter check src/
t-linter format --check src/
Neovim
vim.lsp.start({
name = "t-linter",
cmd = { "t-linter", "lsp" },
filetypes = { "python" },
})
Other Editors
Any editor with LSP support can use t-linter. Configure the LSP client to start t-linter lsp as the server command for Python files.
Configuration
Configuration via pyproject.toml:
[tool.t-linter]
line-length = 80
extend-exclude = ["generated", "vendor"]
ignore-file = ".t-linterignore"
| Key | Description |
|---|---|
line-length |
Formatter print width (applies to HTML, T-HTML, and TDOM only; JSON, YAML, and TOML use fixed formatting rules) |
exclude |
Override the built-in default excludes |
extend-exclude |
Add more exclude patterns on top of the defaults |
ignore-file |
Path to a gitignore-style ignore file, relative to the project root |
By default, t-linter also reads .t-linterignore from the project root if it exists.
Quick Start Example
from typing import Annotated
from string.templatelib import Template
def render_html(template: Annotated[Template, "html"]) -> None:
pass
def run_sql(template: Annotated[Template, "sql"]) -> None:
pass
type css = Annotated[Template, "css"]
type yaml_config = Annotated[Template, "yaml"]
type toml_config = Annotated[Template, "toml"]
def load_styles(template: css) -> None:
pass
def load_yaml(template: yaml_config) -> None:
pass
def load_toml(template: toml_config) -> None:
pass
title = "t-linter"
heading = "Template strings with syntax highlighting"
content = "Interpolations stay as normal Python expressions."
render_html(t"""
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1 style="color: #007acc">{heading}</h1>
<p>{content}</p>
</body>
</html>
""")
start_date = "2026-01-01"
run_sql(t"""
SELECT u.name, u.email, p.title
FROM users u
JOIN posts p ON u.id = p.author_id
WHERE u.created_at > {start_date}
ORDER BY u.name
""")
padding = 24
load_styles(t"""
.container {{
max-width: 1200px;
margin: 0 auto;
padding: {padding}px;
}}
""")
app_name = "demo-app"
load_yaml(t"""
app:
name: {app_name}
debug: true
""")
project_name = "demo-project"
version = "0.1.0"
load_toml(t"""
[project]
name = "{project_name}"
version = "{version}"
""")
Use {{ and }} when the embedded language needs literal braces, such as CSS
or JSON objects.
For html, <title>{value}</title> is allowed and treated as escaped text.
<script>, <style>, and <textarea> still reject interpolations for safety.
Roadmap
Planned Features
- โ Language Server Protocol (LSP) - Fully implemented
- โ Syntax Highlighting - Supports HTML, T-HTML, TDOM, SQL, JavaScript, CSS, JSON, YAML, TOML
- โ
Type Alias and Marker Support - Recognizes
type html = Annotated[Template, "html"]and marker classes withtstring_language - โ
Linting (
checkcommand) - Validate template strings for syntax errors - โ
Formatting (
formatcommand) - Canonical formatting for HTML, T-HTML, TDOM, JSON, YAML, TOML - โ
Statistics (
statscommand) - Analyze template string usage across codebases - ๐ Cross-file Type Resolution - Track type aliases across module boundaries
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file t_linter-0.10.0.tar.gz.
File metadata
- Download URL: t_linter-0.10.0.tar.gz
- Upload date:
- Size: 216.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
98bb2616a5b272a146ca7199b3a2c1e3f141e8a62cc7892abd9d2d7fa0456bc9
|
|
| MD5 |
3108bac8e6e88545acfc30df667ed61f
|
|
| BLAKE2b-256 |
913ff87a7180df1a9922d7099d978be354d9431a5cf7c613ce04c11c2ea5d418
|
File details
Details for the file t_linter-0.10.0-py3-none-win_amd64.whl.
File metadata
- Download URL: t_linter-0.10.0-py3-none-win_amd64.whl
- Upload date:
- Size: 5.1 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7739ccd9cef72b45d464d7d741517efc6c0d0ea17863e4002a4f90343bf7ea4d
|
|
| MD5 |
2a9680d18d976d44f2455afbec74ba2b
|
|
| BLAKE2b-256 |
5454f7a456b3fdf7c9b16968ae87e427761f1bc83d70edfb7712fb205833e4f6
|
File details
Details for the file t_linter-0.10.0-py3-none-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: t_linter-0.10.0-py3-none-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 5.7 MB
- Tags: Python 3, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37b30c26ab4cfe3952b6d75eb632a3217602087a20ec3b85ee8ec4e07ab370a1
|
|
| MD5 |
9d5fb981d68630736b3b4e944a7c97a5
|
|
| BLAKE2b-256 |
9503341b1110f8e89d368ec1b31fa4c8080fafc2c9e487d4b6b92bbf9669ff77
|
File details
Details for the file t_linter-0.10.0-py3-none-manylinux_2_28_aarch64.whl.
File metadata
- Download URL: t_linter-0.10.0-py3-none-manylinux_2_28_aarch64.whl
- Upload date:
- Size: 5.7 MB
- Tags: Python 3, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b111571ddbf5fd331ce558046c9bc5890cce2da6fd3b0d6811cf3ec2137fc830
|
|
| MD5 |
22d42a74bf8f9908c20628ba2730b5c7
|
|
| BLAKE2b-256 |
b9da780fd0a695134f61ee2ffca9d6aac7b6b5dc161a3953f4d30622852cfded
|
File details
Details for the file t_linter-0.10.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: t_linter-0.10.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 5.5 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6e6329feafab47091bee26902ac47e4438f9bf82d86f12a07d63dcb58f5ddd3
|
|
| MD5 |
8271d72e41e9a14102b25b32070efcfb
|
|
| BLAKE2b-256 |
548dd85175a194a837d3e21f5f5d6ce776529f9c25756d035dcfabb9db2c7253
|