Skip to main content

t-linter 🐍✨

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

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.

→ 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:

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-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"
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 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

Metadata

Release files for t-linter 0.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for t-linter 0.10.0
File Size Uploaded
t_linter-0.10.0.tar.gz 216.1 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for t-linter 0.10.0
File
t_linter-0.10.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
t_linter-0.10.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
t_linter-0.10.0-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details
t_linter-0.10.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details

Total release size: 22.2 MB

Release files / t_linter-0.10.0.tar.gz

Download URL t_linter-0.10.0.tar.gz
Size 216.1 kB
Tags Source
SHA-256 checksum
How to use checksums
98bb2616a5b272a146ca7199b3a2c1e3f141e8a62cc7892abd9d2d7fa0456bc9
BLAKE2b-256 checksum
How to use checksums
913ff87a7180df1a9922d7099d978be354d9431a5cf7c613ce04c11c2ea5d418
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / t_linter-0.10.0-py3-none-win_amd64.whl

Download URL t_linter-0.10.0-py3-none-win_amd64.whl
Size 5.1 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
7739ccd9cef72b45d464d7d741517efc6c0d0ea17863e4002a4f90343bf7ea4d
BLAKE2b-256 checksum
How to use checksums
5454f7a456b3fdf7c9b16968ae87e427761f1bc83d70edfb7712fb205833e4f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / t_linter-0.10.0-py3-none-manylinux_2_28_x86_64.whl

Download URL t_linter-0.10.0-py3-none-manylinux_2_28_x86_64.whl
Size 5.7 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
37b30c26ab4cfe3952b6d75eb632a3217602087a20ec3b85ee8ec4e07ab370a1
BLAKE2b-256 checksum
How to use checksums
9503341b1110f8e89d368ec1b31fa4c8080fafc2c9e487d4b6b92bbf9669ff77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / t_linter-0.10.0-py3-none-manylinux_2_28_aarch64.whl

Download URL t_linter-0.10.0-py3-none-manylinux_2_28_aarch64.whl
Size 5.7 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
b111571ddbf5fd331ce558046c9bc5890cce2da6fd3b0d6811cf3ec2137fc830
BLAKE2b-256 checksum
How to use checksums
b9da780fd0a695134f61ee2ffca9d6aac7b6b5dc161a3953f4d30622852cfded
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / t_linter-0.10.0-py3-none-macosx_11_0_arm64.whl

Download URL t_linter-0.10.0-py3-none-macosx_11_0_arm64.whl
Size 5.5 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
c6e6329feafab47091bee26902ac47e4438f9bf82d86f12a07d63dcb58f5ddd3
BLAKE2b-256 checksum
How to use checksums
548dd85175a194a837d3e21f5f5d6ce776529f9c25756d035dcfabb9db2c7253
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

0.10.0 This release

5 release files

0.9.1

5 release files

0.9.0

5 release files

0.8.1

5 release files

0.8.0

5 release files

0.7.0

5 release files

0.6.2

5 release files

0.6.1

5 release files

0.6.0

5 release files

0.5.2

5 release files

0.5.1

5 release files

0.5.0

5 release files

0.4.0

5 release files

0.3.0

5 release files

0.2.1

5 release files

0.2.0

5 release files

0.1.0

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page