Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>PyriteNew to Visual Studio Code? Get it now.
Pyrite

Pyrite

daniel driechlinger

|
16 installs
| (0) | Free
Read Python as if it were Java: mirrors a Python project into a read-only, Java-flavored view for Java developers.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Pyrite: translate Python to Java

Marketplace version Installs Build MIT License

Issues & Bugs: danielonet/pyrite/issues · Changelog: CHANGELOG.md

Pyrite

Read Python as if it were Java.

Developers today, Java developers included, increasingly write code with AI. A Java developer dropped into a Python codebase no longer has to close the language gap before contributing, because the AI can write the Python. The real risk is the opposite: shipping code you cannot read. That is vibe coding, and it leaves the developer unable to review, reason about, or take responsibility for what the application does.

Pyrite closes that gap on the reading side, so a Java developer on a Python project can practise Supervised Development instead of vibe coding: read the AI's output at a developer level, understand the design, review changes, and steer the application deliberately.

The name is the disclaimer. Pyrite is fool's gold: it looks like Java but is not, and it is never meant to compile. It is a faithful reading view of the Python underneath.

Pyrite mirrors every .py file in your workspace into a .java-view/ folder laid out as a standard Java project: production code in src/main/java, test code (test_*.py, *_test.py, conftest.py, anything under test/tests) in src/test/java. Every package starts with the project's name (the workspace folder, or pyrite.basePackage, optionally behind pyrite.basePackagePrefix) followed by the Python folder path, and each file is named after its Java class, as Java requires. Each module is translated into a Java-flavored reading view. The view keeps Python's names, order, comments and docstrings, but uses Java syntax: braces, semicolons, explicit types, for (var x : xs), throw new ..., streams instead of comprehensions, and so on. It is meant to be read, not compiled.

my-project/                          my-project/.java-view/src/
├── main.py                          ├── main/java/my_project/
├── inventory/                       │   ├── Main.java                  package my_project;
│   ├── models.py            ==>     │   └── inventory/
│   └── services/                    │       ├── Models.java            package my_project.inventory;
│       └── order_service.py         │       └── services/
└── tests/                           │           └── OrderService.java  package my_project.inventory.services;
    └── test_order_service.py        └── test/java/my_project/tests/
                                         └── TestOrderService.java      package my_project.tests;

Imports of the project's own modules carry the same prefix and point at the generated class (from .models import Order -> import my_project.inventory.Models.Order;).

Features

  • Generate Java view for the workspace – one command translates the whole tree.
  • Live sync – saving a Python file re-translates just that file (pyrite.watch).
  • Status bar report – the Pyrite logo sits in the status bar; hovering it (or clicking it) shows how many classes, methods, files and fields the Java view holds, a health bar, and the engine in use. Failures, Python syntax errors and warnings are links: clicking one lists every message in the Output panel. Buttons regenerate or delete the view, or open the Pyrite settings.
  • Two-way navigation – Ctrl+Alt+J jumps from a Python line to the matching Java line and back, using a per-file line map.
  • Go to Definition – F12 / Ctrl+Click / right-click on a class, method or field name inside the Java view jumps to where it's declared, even in another mirrored file.
  • Two work modes – rules (default: deterministic, offline, instant) and hybrid (the rules plus a small LLM running on your own machine for the few functions the rules cannot translate). Switch with the pyrite.engine setting. See Work modes.
  • CLI for CI or quick checks: npx pyrite <project-root>.

See architecture/ for design notes, including the hybrid engine and the earlier cloud LLM engine that the local one replaced.

The rules engine

The rules engine is Pyrite's core and the default mode. It is a deterministic, line-oriented translator built on a real Python parser (tree-sitter): the same Python always gives the same Java view, it runs offline with no API key, and it translates a whole project in seconds. It works on the structure it recognises (classes, functions, control flow, common builtins, type hints), which covers the large majority of ordinary Python.

What it maps:

Python Java view
module order_service.py public final class OrderService { ... } in OrderService.java
class Foo(Base): public static class Foo extends Base {
class OrderService: inside order_service.py the module's class itself, public class OrderService { ... }, with the module's constants and functions as its static members (Java forbids a nested class named like its enclosing class)
@dataclass fields, self.x = ... in __init__ field declarations with inferred types
def f(self, a: int, b="x") -> bool: public boolean f(int a, String b /* = "x" */) {
__init__, __str__, __eq__, __len__ ... constructor, toString(), equals(), size() ...
if __name__ == "__main__": public static void main(String[] args) {
for i in range(n) / for k, v in d.items() for (int i = 0; i < n; i++) / entrySet() loop
try/except X as e/finally, raise X(...) try/catch (X e)/finally, throw new X(...)
with open(p) as f: try (var f = open(p)) {
[f(x) for x in xs if c] xs.stream().filter(x -> c).map(x -> f(x)).toList()
sum, sorted(key=..), any/all, next, round(x, 2), map/filter, isinstance(x, (A, B)), open(p, "w"), ... .mapToDouble(..).sum(), .sorted(Comparator.comparing(..)), .anyMatch(..), it.next(), Math.round(x * 100.0) / 100.0, .map(..)/.filter(..), x instanceof A \|\| .., new PrintWriter(p), ...
value = a or default Objects.requireNonNullElse(a, default) (in a condition a or b stays a \|\| b)
@property def total(self) and order.total total() and order.total(); a trivial getter/setter pair in Lombok style becomes @Getter/@Setter on a field named after the property, read as order.getTotal()
x = repo.order(1) where order has a -> Order hint, for line in self.lines with lines: List[OrderLine] Order x = ..., for (OrderLine line : this.lines) - types follow hints on functions, methods (also from other files and base classes), fields and parameters
f"Hi {name}, {total:.2f}" "Hi " + name + ", " + String.format("%.2f", total)
x if c else y, and/or/not, None/True/False c ? x : y, &&/\|\|/!, null/true/false
if items and not done: (truthiness) if (Py.truthy(items) && !done) - a value that is not a boolean goes through the runtime library
xs[1:-1], f()[-1], (a, b), zip(a, b) xs.subList(1, xs.size() - 1), f().getLast(), Tuple.of(a, b), Tuple.zip(a, b)
os.path.join(a, b), sys.argv, re.match(p, s), math.floor(x) Os.path.join(a, b), Sys.argv, Re.match(p, s), (long) Math.floor(x)
a name assigned in an if/try branch and used after it declared before the block (String label = null;), since a Java local lives only in its block
def f(text): return text.strip().split(",") (no hint) f(String text): an unhinted parameter used only as a string, list or dict gets that type
s.split(), s.partition("="), xs.pop(), s[0] on a string Py.split(s), Py.partition(s, "="), Py.pop(xs), String.valueOf(s.charAt(0))
Optional[T], list[int], dict[str, Any] T /* nullable */, List<Integer>, Map<String, Object>
ValueError, KeyError, ... IllegalArgumentException, NoSuchElementException, ...
docstrings / # comments Javadoc / // comments
inventory/__init__.py with only a docstring / __all__ nothing: Java packages are plain folders
inventory/__init__.py with real content (constants, re-exports) public final class InventoryInit in inventory/InventoryInit.java, Javadoc names the original __init__.py; the Init suffix hints at __init__.py and keeps it apart from a sibling inventory/inventory.py (Inventory.java)

Anything without a clean equivalent is kept and annotated with a /* ... */ comment rather than dropped, so the view is always complete.

A file with a Python syntax error is still translated, but its view starts with a // WARNING block naming the error, and a file the engine cannot handle at all gets a TRANSLATION FAILED placeholder rather than an outdated view. Syntax is checked with a real Python parser (tree-sitter, WebAssembly, offline).

The small LLM

Some Python does not translate well line by line, for example a comprehension with two for clauses, or an assignment expression (if (n := len(xs)) > 3). For those, Pyrite can ask a small language model running locally to rewrite just that one function. The model gets the Python function (with line numbers) and the rules engine's draft, and returns a Java-flavored version that keeps every statement, name and comment. It runs through Ollama, so nothing leaves your machine and no API key is involved.

Pyrite does not include an LLM server. For now you install and run one yourself (Ollama is the supported one) and point Pyrite at it; see Running a local LLM with Ollama. If none is running, hybrid mode simply behaves like rules and tells you why.

Work modes: rules and hybrid

Set the mode with pyrite.engine:

Mode What runs Needs
rules (default) Rules engine only nothing
hybrid Rules engine for everything, plus the local LLM for the functions the rules cannot handle and for the imports the JDK index cannot resolve a running Ollama and a downloaded model

How hybrid works, per file:

  1. The rules engine translates the whole file first, exactly as in rules mode.
  2. Pyrite splits the file into functions and methods and looks for ones the rules failed on: a warning was raised inside it, a statement stayed untranslated, or it uses a construct the rules are known to get wrong (nested comprehension, :=).
  3. Only those functions are sent to the model, one request per function, each with its Python source and the rules' draft. Files and functions the rules handled never touch the model, so most files make no request at all (pyrite.ollama.maxFunctionsPerFile caps the rest).
  4. The answer replaces just that function in the Java view. The line map and Go to Definition are kept, and a // Rewritten by <model> (<reason>); check against the Python. line marks it, so you always know which parts came from the model.
  5. If the model is unreachable, too slow, or returns something unusable, the rules output for that function stays and a warning is recorded (click the warning count in the status bar report to see it). Answers are cached in .java-view/.pyrite/ollama-cache.json, so only functions that changed are sent again.

Model output is not deterministic or guaranteed correct: treat a rewritten function as a reading aid and check it against the Python, which is why it is marked.

Real Java imports

Python's imports (import logging, from typing import Dict) name modules, not classes javac could find. Pyrite replaces them with explicit imports of the real classes the view uses, in both modes:

// Python-shaped                           // resolved (default)
import java.util.*;                        import java.util.List;
import java.util.stream.*;                 import java.util.Map;
import logging;                            import java.util.logging.Logger;
import typing.Dict;
import app.models.Order;                   import lombok.Getter;
import app.utils.formatting.money;
                                           import app.Models.Order;
                                           import static app.utils.Formatting.money;

                                           // python: import logging
                                           // python: from typing import Dict
  1. Every type the Java code uses is looked up in an index of the real JDK (every public class of the java.se modules, generated from JDK 21): List becomes java.util.List, Logger becomes java.util.logging.Logger. java.lang needs no import.
  2. Imports of your own modules point at the classes Pyrite generates: a class is nested in its module's class (app.Models.Order), and a function or constant becomes a static import. A class used without a Python import (from a type hint, say) is found too.
  3. The Lombok annotations Pyrite writes (@Getter, @Data, ...) import lombok.*.
  4. In hybrid mode, only the names left over go to the local model, in one request per file (cached like the function rewrites). Its answer is checked: a JDK class must exist in the JDK, and an import must be for a name the code really uses; anything else is dropped with a warning. A library class it suggests is kept and marked // suggested by <model>; not in the JDK, needs this library on the classpath.

Python imports with no Java counterpart stay as // python: ... comments (so Ctrl+Alt+J still jumps to them), and names nobody could resolve are listed in a // No Java import found for: ... comment. Steps 1-3 need no model, so they run in rules mode and when Ollama is not running. Turn it off with pyrite.resolveImports: false (CLI: --no-java-imports) to keep the Python-shaped lines, which read like the Python but are not Java.

The rest of the view is still a reading view (logging.getLogger(__name__) is not Java), so the imports are what is real today, not the whole file.

Runtime library

Some Python has no one-line Java equivalent: truthiness, slices with negative or out-of-range bounds, tuples, re with its Python syntax and results. The views call a small Java library for those instead of guessing, and Pyrite writes its sources into the view project (src/main/java/pyrite/runtime/, imported like any other class):

Class For
Py Py.truthy(x), Py.slice(xs, lo, hi[, step]), Py.at(xs, -2), Py.len, Py.repr, Python's str methods (split, strip(chars), partition, splitlines, isdigit, ...) and pop / remove; NotImplemented, Ellipsis
Tuple Tuple.of(a, b), Tuple.zip(...), Tuple.enumerate(...): an immutable List that prints and compares like Python
Os, Os.path, Sys, Re, Time, Warnings the commonly used part of os, os.path, sys, re (Python pattern syntax, match anchored at the start, findall returning groups, sub with \1 or a function), time, warnings (with the warning categories as classes)
@Property, @PropertySetter mark methods that were a Python @property / setter

Only the module members the library implements are rewritten (os.path.join -> Os.path.join); anything else keeps its Python spelling (os.stat(p)), so what is missing stays visible. math maps to java.lang.Math.

Running a local LLM with Ollama

The default rules engine needs nothing. The optional hybrid engine adds a small model running on your own machine, used only for the functions the rules cannot translate (nested comprehensions, assignment expressions :=, and anything the rules flag with a warning or an untranslated marker). Generators, async/await, lambdas and the rest stay rule-based. If Ollama is off or fails, Pyrite keeps the rules output for that function and adds a warning, so it never blocks you.

1. Install Ollama (ollama.com/download)

# Linux
curl -fsSL https://ollama.com/install.sh | sh
# macOS
brew install ollama          # or download the app
# Windows: download the installer from ollama.com/download

On Linux the installer sets up a background service on http://localhost:11434. On macOS and Windows the app starts the server; otherwise run ollama serve.

2. Download a model. Pick a small code model; a bigger one is slower:

ollama pull qwen2.5-coder:7b     # Pyrite's default; good at code, ~4.7 GB
ollama pull qwen2.5-coder:3b     # smaller and faster, for weaker machines
ollama list                      # shows the exact names you can use

3. Check it works

curl http://localhost:11434/api/tags       # should list your models
ollama run qwen2.5-coder:7b "say hi"

4. Turn it on in VS Code (settings.json):

"pyrite.engine": "hybrid",
"pyrite.ollama.model": "qwen2.5-coder:7b"

pyrite.ollama.model must match a name from ollama list exactly, tag included. Then run Pyrite: Generate Java View for Workspace. A function rewritten by the model has a // Rewritten by <model> (<reason>); check against the Python. line above it, and the answers are cached in .java-view/.pyrite/ollama-cache.json, so only changed functions are sent again. From the command line:

node out/cli.js my-project --engine hybrid --ollama-model qwen2.5-coder:7b

Troubleshooting

Symptom Cause and fix
404 on /api/chat, or a warning that the model "is not installed" The model in pyrite.ollama.model is not pulled. Run ollama pull <name> or set the setting to a name from ollama list.
Warning "Could not reach Ollama" The server is not running, or pyrite.ollama.url is wrong. Start it with ollama serve.
Warning "did not answer within ...s" Too slow. On a CPU-only machine the first request also loads the model and can take minutes. Use a smaller model, raise pyrite.ollama.timeoutSeconds, or lower pyrite.ollama.maxFunctionsPerFile.
Where are Ollama's logs? Linux service: journalctl -u ollama -f. Also ollama ps shows what is loaded and whether it runs on CPU or GPU.

Every warning is listed in the Output panel: click the warning count in the status bar report.

Testing

npm test runs the unit tests, a snapshot of the sample project (src/test/snapshots/, refresh with UPDATE_SNAPSHOTS=1 npm test after an intended change) and a corpus run over the first 300 files of the local Python standard library: the engine must not throw, must emit balanced braces, must stay fast, and every class and function the real parser sees must come out as a symbol. Point it elsewhere with PYRITE_CORPUS=dir1:dir2, widen it with PYRITE_CORPUS_ALL=1. CI runs the full standard library on every push to main.

Getting started (development)

On a fresh Debian/Ubuntu machine, scripts/setup-env.sh installs Node.js and npm through apt (adding the NodeSource repository when apt's Node is older than 20) and runs npm install. scripts/build-and-install.sh builds a .vsix, installs Node 20 through nvm if it is missing, and installs the extension into your local VS Code.

git clone https://github.com/danielonet/pyrite.git && cd pyrite
npm install
npm run compile
npm test                       # unit tests (node --test)
npm run cli -- sample-python-project   # writes sample-python-project/.java-view/

Open this folder in VS Code and press F5 to launch an Extension Development Host; open sample-python-project there and run Pyrite: Generate Java View for Workspace.

To build an installable package: npx @vscode/vsce package (produces a .vsix).

To publish a release to the Visual Studio Marketplace: npm run publish:marketplace (runs scripts/publish-marketplace.sh - builds, tests, packages and publishes). It reads the publisher id and Marketplace personal access token from a plain text file outside this repo (~/.config/pyrite/marketplace.env by default, never committed); the script prints a template and instructions the first time it's run. Use --dry-run to build and package without publishing.

Settings

Setting Default Meaning
pyrite.engine rules work mode: rules, or hybrid (rules + a local LLM for hard functions, see Work modes)
pyrite.ollama.url http://localhost:11434 Ollama server used by the hybrid engine
pyrite.ollama.model qwen2.5-coder:7b model name, exactly as ollama list shows it
pyrite.ollama.timeoutSeconds 300 give up on one request after this long and keep the rules output
pyrite.ollama.maxFunctionsPerFile 10 at most this many functions per file go to the model
pyrite.resolveImports true replace Python's imports with real Java ones, in both modes (see Real Java imports)
pyrite.outputFolder .java-view where the mirror is written (relative to the workspace root), as src/main/java and src/test/java
pyrite.basePackage (empty) package every view starts with, e.g. com.acme.shop; empty uses the workspace folder's name (my-project -> my_project)
pyrite.basePackagePrefix (empty) package put in front of the project's name: com.acme + folder my-project -> com.acme.my_project
pyrite.exclude venv, node_modules, ... glob patterns to skip
pyrite.watch true re-translate on save
pyrite.javadoc docstringOnly Javadoc generation: always (every class/method, extrapolated when there's no docstring), docstringOnly (only where a docstring exists), or none (no Javadoc; docstrings kept as plain comments)
pyrite.javadocTestCode false Document test code too, using the same pyrite.javadoc rules as production code. When unchecked, test files (test_*.py, *_test.py, conftest.py, or anything under a test/tests folder) never get Javadoc
pyrite.lombok true Use Lombok-style annotations instead of spelling out boilerplate: a plain self.x = x __init__ becomes @AllArgsConstructor, a trivial __str__/__repr__ becomes @ToString, a trivial __eq__/__hash__ becomes @EqualsAndHashCode, a @property/@x.setter pair that just wraps a field becomes @Getter/@Setter on that field, and a dataclass-like class becomes @Data. Methods that don't match these simple shapes are left spelled out
pyrite.invalidJava stub Every view is parsed as Java. With stub, each generated statement that is not valid Java is replaced by if (true) throw new UnsupportedOperationException("TODO(pyrite): <python line>");, with the original text kept as a comment, so one bad statement cannot break the rest of the file (search the view for TODO(pyrite)). keep leaves the text alone and only counts those statements in the view's warnings
pyrite.lineWidth 120 Maximum line length of the generated Java. Longer lines wrap like IntelliJ IDEA and palantir-java-format by default: a signature or call that does not fit gets one parameter per line with an 8-space continuation indent, conditions break before &&/\|\|, concatenation before +, call chains before each .call(). Long string literals are never split. 0 turns wrapping off

The generated folder contains its own .gitignore so it is never committed.

Roadmap

  • Edit the Java view and sync back to Python is not implemented yet. The intended design (function-level, approval-gated, LLM-translated with the original Python as context) and its open questions are in architecture/phase-2.md.
  • A built-in LLM server is not planned for now: the model runs in your own Ollama.

Limitations

  • The rules engine is line-oriented. Deeply nested comprehensions, exotic decorators, metaclasses and dynamic attribute tricks are kept verbatim with a comment (nested comprehensions and := can be rewritten by the hybrid engine's local model).
  • Types are inferred from hints, defaults and simple literals only; unknown types show as Object/var.
  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft