t-linter ๐Ÿโœจ

July 10, 2026 ยท View on GitHub

Intelligent syntax highlighting, validation, and formatting for Python template strings (PEP 750).

License: MIT PyPI VSCode Marketplace

๐Ÿ“ฃ ๐Ÿ’ผ Maintainer update: Open to opportunities. ๐Ÿ”— koxudaxi.dev

๐Ÿ“– Documentation

๐Ÿ‘‰ t-linter.koxudaxi.dev


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 syntax
  • t-linter format โ€” canonically reformat supported template literals
  • t-linter sql prepare โ€” cache PostgreSQL parameter metadata for psycopg SQL type narrowing
  • t-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 with tstring_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 TypedDict or dataclass models with Json(schema=...)
  • ๐Ÿงฉ Callee Inference - Detects backend languages from helpers such as tdom.html(...)
  • ๐Ÿš€ Fast - Single Rust binary with Tree-sitter parsers

Supported Languages

LanguageAnnotationCheckFormatHighlight
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

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.

โ†’ View on PyPI

VSCode Extension

If you use VSCode, install the extension for seamless editor integration:

  1. Open VSCode โ†’ Extensions (Ctrl+Shift+X / Cmd+Shift+X)
  2. Search for "t-linter" โ†’ Install "T-Linter" by koxudaxi
  3. On Linux x64, macOS x64/arm64, and Windows x64, the extension bundles the t-linter binary, so no separate PyPI install is required.
  4. On unsupported platforms, or if you want to override the bundled binary, install t-linter via PyPI (see above) and set t-linter.serverPath to the full executable path.
  5. 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": true if 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.
  6. 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:

CodeMeaning
0Run completed successfully
1Issues were found and --error-on-issues was set
2Operational 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-linter for document-level rewrites and refactor.rewrite.t-linter for 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"
KeyDescription
line-lengthFormatter print width (applies to HTML, T-HTML, and TDOM only; JSON, YAML, and TOML use fixed formatting rules)
excludeOverride the built-in default excludes
extend-excludeAdd more exclude patterns on top of the defaults
ignore-filePath 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 with tstring_language
  • โœ… Linting (check command) - Validate template strings for syntax errors
  • โœ… Formatting (format command) - Canonical formatting for HTML, T-HTML, TDOM, JSON, YAML, TOML
  • โœ… Statistics (stats command) - Analyze template string usage across codebases
  • ๐Ÿ“‹ Cross-file Type Resolution - Track type aliases across module boundaries