Python library for parsing, manipulating, and emitting JVM class files
Project description
pytecode
pytecode is a Python 3.14+ library for parsing, inspecting, editing, validating, and emitting JVM class files and JAR archives.
It is built for Python tooling that needs direct access to Java bytecode: classfile readers and writers, archive rewriters, transformation pipelines, control-flow analysis, descriptor utilities, hierarchy-aware frame computation, and verification-oriented workflows.
Why pytecode?
- Parse
.classfiles into typed Python dataclasses. - Edit classes, fields, methods, and bytecode through a mutable symbolic model.
- Rewrite JAR files while preserving non-class resources and ZIP metadata.
- Recompute
max_stack,max_locals, andStackMapTablewhen requested. - Validate parsed classfiles and edited models before emission.
- Work with descriptors, signatures, labels, symbolic operands, constant pools, and debug-info policies.
Installation
Install from PyPI:
pip install pytecode
Or with uv:
uv add pytecode
pytecode requires Python 3.14+.
Quick start
Parse and roundtrip a class file
from pathlib import Path
from pytecode import ClassReader, ClassWriter
reader = ClassReader.from_file("HelloWorld.class")
classfile = reader.class_info
print(classfile.major_version)
print(classfile.methods_count)
Path("HelloWorld-copy.class").write_bytes(ClassWriter.write(classfile))
Lift to the editable model
from pathlib import Path
from pytecode import ClassModel
model = ClassModel.from_bytes(Path("HelloWorld.class").read_bytes())
print(model.name)
updated_bytes = model.to_bytes()
Path("HelloWorld-updated.class").write_bytes(updated_bytes)
Use recompute_frames=True when an edit changes control flow or stack/local layout.
JAR rewriting example
JarFile.rewrite() can apply in-place transforms to matching classes and methods:
from pytecode import JarFile
from pytecode.classfile.constants import MethodAccessFlag
from pytecode.edit.model import ClassModel, MethodModel
from pytecode.transforms import (
class_named,
method_is_public,
method_is_static,
method_name_matches,
on_methods,
pipeline,
)
def make_final(method: MethodModel, _owner: ClassModel) -> None:
method.access_flags |= MethodAccessFlag.FINAL
JarFile("input.jar").rewrite(
"output.jar",
transform=pipeline(
on_methods(
make_final,
where=method_name_matches(r"main") & method_is_public() & method_is_static(),
owner=class_named("HelloWorld"),
)
),
)
Transforms must mutate models in place and return None. For code-shape changes, pass recompute_frames=True. For an ASM-like lift path that omits debug metadata, pass skip_debug=True.
Public surface
Top-level exports:
pytecode.ClassReaderandpytecode.ClassWriterfor raw classfile parsing and emission.pytecode.JarFilefor archive reads, mutation, and safe rewrite-to-disk.pytecode.ClassModelfor mutable editing with symbolic references.
Supported submodules:
pytecode.transformsfor composable class, field, method, and code transforms.pytecode.edit.labelsfor label-aware bytecode editing helpers.pytecode.edit.operandsfor symbolic operand wrappers.pytecode.analysisfor CFG construction, frame simulation, and recomputation helpers.pytecode.analysis.verifyfor structural validation and diagnostics.pytecode.analysis.hierarchyfor type and override resolution helpers.pytecode.classfile.descriptorsfor JVM descriptors and generic signatures.pytecode.edit.constant_pool_builderfor deterministic constant-pool construction.pytecode.classfile.modified_utf8for JVM Modified UTF-8 encoding and decoding.pytecode.edit.debug_infofor explicit debug-info preservation and stripping policies.
Documentation
- Development docs overview: docs/OVERVIEW.md
- Hosted API reference: https://smithtrenton.github.io/pytecode/
Development
Create a local environment with development tools:
uv sync --extra dev
Common checks:
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv run pytest -q
uv run python tools/generate_api_docs.py --check
Generate local API reference HTML with:
uv run python tools/generate_api_docs.py
Build source and wheel distributions locally:
uv build
Profile isolated JAR-processing stages without run.py's output overhead:
uv run python tools/profile_jar_pipeline.py path/to/jar.jar
uv run python tools/profile_jar_pipeline.py path/to/jar.jar --stages class-parse model-lift model-lower
uv run python tools/profile_jar_pipeline.py path/to/dir/with/jars --stages model-lift model-lower --summary-json output/profiles/common-libs/summary.json
When making runtime-performance changes, prefer checking both a focused jar such as 225.jar and the wider common-jar corpus so regressions and wins are not judged from a single artifact. A single jar defaults to all stages; directories and multi-jar runs default to model-lift and model-lower.
The oracle-marked CFG tests lazily cache ASM 9.7.1 test jars under .pytest_cache/pytecode-oracle and also honor manually seeded jars in tests/resources/oracle/lib. If java, javac, or the ASM jars are unavailable, that suite skips without failing the rest of the test run.
Release automation
PyPI releases are published from GitHub Actions by pushing an immutable v<version> tag that matches project.version in pyproject.toml. The same workflow can also be started manually for an existing tag by supplying a tag input. In both cases, the workflow checks out the tagged commit, reruns validation, builds both sdist and wheel with uv build, publishes from the protected pypi environment via PyPI Trusted Publishing, and then creates or updates a GitHub Release for the same tag with the built distributions attached.
Release procedure:
# 1) bump project.version in pyproject.toml
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv run pytest -q
uv run python tools/generate_api_docs.py --check
git commit -am "Bump version to X.Y.Z"
git push origin master
git tag vX.Y.Z
git push origin vX.Y.Z
The release workflow rejects tags that do not match project.version. Treat release tags as immutable: if a tag or published artifact is wrong, bump to a new version and publish a new tag instead of force-pushing the old one. For an existing tag, you can rerun the workflow directly or start it manually from Actions by providing the tag name. The workflow is safe to rerun for the same tag: PyPI uploads skip files that already exist, and the GitHub Release step updates the existing release assets in place if the release was already created.
Repository utilities
run.py is a manual smoke-test helper that parses a JAR file, writes pretty-printed parsed class structures under <jar parent>/output/<jar stem>/parsed/, and writes class-model-derived rewritten .class files plus copied resources under <jar parent>/output/<jar stem>/rewritten/.
Example:
uv run python ./run.py ./path/to/input.jar
The script prints read, parse, lift, write, and rewrite timings plus class and resource counts to stdout.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 pytecode-0.0.3.tar.gz.
File metadata
- Download URL: pytecode-0.0.3.tar.gz
- Upload date:
- Size: 199.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad449750d4e8f2402d0383cf9bf8450ded030535f3c40f1bec86221fed0272cf
|
|
| MD5 |
308134c789fdb230a4a7f53f3b8e1872
|
|
| BLAKE2b-256 |
a26dbf4c0b3c969e1e15e165678a21ec40e1d27c679299975287e28dca22daad
|
Provenance
The following attestation bundles were made for pytecode-0.0.3.tar.gz:
Publisher:
release.yml on smithtrenton/pytecode
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytecode-0.0.3.tar.gz -
Subject digest:
ad449750d4e8f2402d0383cf9bf8450ded030535f3c40f1bec86221fed0272cf - Sigstore transparency entry: 1210077197
- Sigstore integration time:
-
Permalink:
smithtrenton/pytecode@f66ae70eb953256913994ea34c513f11eb631a9f -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/smithtrenton
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f66ae70eb953256913994ea34c513f11eb631a9f -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytecode-0.0.3-py3-none-any.whl.
File metadata
- Download URL: pytecode-0.0.3-py3-none-any.whl
- Upload date:
- Size: 115.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b397d6f8edc969f09f232e0a78148b48d435650e1c86be6cc02e60dae014c631
|
|
| MD5 |
4909c55ca733445159e28d2a12a167de
|
|
| BLAKE2b-256 |
9f19f8c27551dc8de2e3ee4662e18828ebe3e6521ca3a7b78fb4031d4530cc6a
|
Provenance
The following attestation bundles were made for pytecode-0.0.3-py3-none-any.whl:
Publisher:
release.yml on smithtrenton/pytecode
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytecode-0.0.3-py3-none-any.whl -
Subject digest:
b397d6f8edc969f09f232e0a78148b48d435650e1c86be6cc02e60dae014c631 - Sigstore transparency entry: 1210077223
- Sigstore integration time:
-
Permalink:
smithtrenton/pytecode@f66ae70eb953256913994ea34c513f11eb631a9f -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/smithtrenton
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f66ae70eb953256913994ea34c513f11eb631a9f -
Trigger Event:
push
-
Statement type: