Skip to main content

Python library for parsing, manipulating, and emitting JVM class files

Project description

pytecode

pytecode is a Python 3.12+ 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 .class files into typed Python objects backed by the Rust engine.
  • 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, and StackMapTable when 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

Published releases ship prebuilt wheels for Windows, macOS, and Linux. If a matching wheel is unavailable, pip/uv falls back to a source build, which requires a working Rust toolchain.

pytecode requires Python 3.12+.

Quick start

Parse and roundtrip a class file

from pathlib import Path

from pytecode.classfile 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))

Edit through the Rust-owned model

from pathlib import Path

from pytecode.model 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)

Import FrameComputationMode from pytecode.archive and use to_bytes_with_options(frame_mode=FrameComputationMode.RECOMPUTE) 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.transforms import (
    PipelineBuilder,
    add_access_flags,
    class_named,
    method_is_public,
    method_is_static,
    method_name_matches,
)


pipeline = (
    PipelineBuilder()
    .on_methods(
        method_name_matches(r"main") & method_is_public() & method_is_static(),
        add_access_flags(int(MethodAccessFlag.FINAL)),
        owner_matcher=class_named("HelloWorld"),
    )
    .build()
)


JarFile("input.jar").rewrite(
    "output.jar",
    transform=pipeline.apply,
)

For code-shape changes, pass frame_mode=FrameComputationMode.RECOMPUTE. To strip debug metadata during rewrite, prefer debug_info=DebugInfoPolicy.STRIP.

JarFile.rewrite() uses the Rust archive layer for in-memory archive edits and Rust-native transforms. Python-callable transforms are also supported through the same public API when a workflow needs ClassModel-level mutation.

Public surface

Top-level exports:

  • pytecode.ClassReader / pytecode.ClassWriter for Rust-backed raw classfile parsing and emission.
  • pytecode.ClassModel for Rust-owned mutable editing.
  • pytecode.JarFile for Rust-backed archive reads and rewrite workflows.

Canonical semantic modules:

  • pytecode.classfile for raw Rust-backed classfile reading and writing.
  • pytecode.model for editable class models, typed code items, labels, and code metadata wrappers.

Supported submodules:

  • pytecode.transforms for Rust-backed class, field, and method transforms.
  • pytecode.analysis for Rust-backed verification entry points.
  • pytecode.analysis.verify for structural validation and diagnostics.
  • pytecode.analysis.hierarchy for type and override resolution helpers.
  • pytecode.classfile.attributes for typed raw attribute dataclasses.
  • pytecode.classfile.bytecode for opcode and array-type enums.
  • pytecode.classfile.constants for access flags, opcodes, and verifier constants.
  • pytecode.model for editable class, field, method, and code models.

Documentation

Development

Create a local environment with development tools:

uv sync --extra dev

uv sync --extra dev now builds the local editable package through maturin, so a working Rust toolchain is required alongside Python.

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
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

Workspace smoke and comparison commands:

cargo run --release -p pytecode-cli -- bench-smoke --iterations 5
uv run python tools\benchmark_jar_pipeline.py crates\pytecode-engine\fixtures\jars\byte-buddy-1.17.5.jar --iterations 5
uv run python tools\compare_rust_python_benchmarks.py --jar crates\pytecode-engine\fixtures\jars\byte-buddy-1.17.5.jar --iterations 5 --output output\benchmarks\rust-vs-python-byte-buddy.json
cargo run -p pytecode-cli -- class-summary --path crates\pytecode-engine\fixtures\classes\HelloWorld\HelloWorld.class
cargo run -p pytecode-cli -- rewrite-smoke --jar input.jar --output output.jar --class-name HelloWorld
cargo run -p pytecode-cli -- patch-jar --jar input.jar --output output.jar --rules rules.json
uv run python examples\patch_jar.py --jar input.jar --output output.jar --rules rules.json
cargo run -p pytecode-cli -- deobfuscate analyze --jar path\to\obfuscated-client.jar
cargo run -p pytecode-cli -- deobfuscate rewrite --jar path\to\obfuscated-client.jar --output output\obfuscated-client-cleaned.jar
uv run python examples\deobfuscate.py analyze --jar path\to\obfuscated-client.jar
uv run python examples\deobfuscate.py rewrite --jar path\to\obfuscated-client.jar --output output\obfuscated-client-python-cleaned.jar

patch-jar is the first real config-driven consumer of the Rust archive + transform stack. It reads a JSON rule file, rewrites matching classes in a JAR, and prints a JSON report with per-rule match/change counts.

examples\patch_jar.py mirrors that workflow from Python and accepts the same rules.json shape, but applies it through the Python API.

Example rules.json:

{
  "options": {
    "debug_info": "preserve",
    "frame_mode": "preserve"
  },
  "rules": [
    {
      "name": "finalize-main",
      "kind": "method",
      "owner": {
        "name": "HelloWorld"
      },
      "matcher": {
        "name": "main",
        "access_all": ["public", "static"],
        "has_code": true
      },
      "action": {
        "type": "add-access-flags",
        "flags": ["final"]
      }
    }
  ]
}

The first version supports:

  • class rules for access-flag edits, set-super-class, add-interface, and remove-interface
  • field rules for access-flag edits, rename, and remove
  • method rules for access-flag edits, rename, and remove
  • method code_actions for string replacement, method-call redirection, field-access redirection, opcode-based instruction removal, single-instruction replacement, before/after insertion, contiguous sequence replacement/removal, and grouped sequence action blocks

Example sequence rewrite inside code_actions:

{
  "type": "replace-sequence",
  "pattern": [
    { "ldc_string": "Hello from fixture" },
    {
      "method_owner": "java/io/PrintStream",
      "method_name": "println",
      "method_descriptor": "(Ljava/lang/String;)V"
    }
  ],
  "replacement": [
    { "type": "ldc-string", "value": "patched via sequence action" },
    {
      "type": "method",
      "opcode": 182,
      "owner": "java/io/PrintStream",
      "name": "print",
      "descriptor": "(Ljava/lang/String;)V"
    }
  ]
}

Sequence patterns use matcher objects (opcode, opcode_any, method_owner, method_name, method_descriptor, field_owner, field_name, field_descriptor, ldc_string, var_slot, type_descriptor, and the is_* category flags). Replacement items support symbolic instructions like raw, ldc-string, field, method, type, var, and iinc, plus label-based control-flow items.

Replacement items can also express symbolic control flow with label, branch, lookup-switch, and table-switch. Branch and switch targets must reference labels declared inside the same replacement block, so malformed control-flow edits fail fast during plan loading instead of producing broken classfiles.

Example grouped rewrite that patches one instruction and brackets another with inserted NOPs:

[
  {
    "type": "replace-insn",
    "matcher": { "ldc_string": "Hello from fixture" },
    "replacement": [
      { "type": "ldc-string", "value": "patched via replace-insn" }
    ]
  },
  {
    "type": "sequence",
    "actions": [
      {
        "type": "insert-before",
        "matcher": {
          "method_owner": "java/io/PrintStream",
          "method_name": "println",
          "method_descriptor": "(Ljava/lang/String;)V"
        },
        "items": [{ "type": "raw", "opcode": 0 }]
      },
      {
        "type": "insert-after",
        "matcher": {
          "method_owner": "java/io/PrintStream",
          "method_name": "println",
          "method_descriptor": "(Ljava/lang/String;)V"
        },
        "items": [{ "type": "raw", "opcode": 0 }]
      }
    ]
  }
]

deobfuscate is the higher-level workflow tool built on top of the same archive/model stack. It is aimed at jars like injected-client, where the goal is to inspect obfuscation signals first and then apply safe cleanup passes without hand-authoring JSON rules.

deobfuscate analyze currently reports:

  • suspicious short-name class counts and samples
  • package concentration (<root> versus named packages)
  • compilercontrol.json JIT exclusion hints
  • hotspot classes by size/method count
  • classes with readable string constants that can anchor reverse-engineering work

deobfuscate rewrite currently applies conservative bytecode cleanup:

  • remove nop instructions
  • remove unconditional goto instructions that already target the immediate fallthrough label
  • collapse unconditional goto chains to their terminal target

Use patch-jar when you already know the exact bytecode rewrite you want and want a declarative rule file. Use deobfuscate when you want a product-shaped inspection/cleanup pass over an obfuscated jar, especially injected-client.

If you want the same workflow from the Python side, examples\deobfuscate.py provides the same analyze/rewrite flow on top of pytecode.JarFile, ClassModel, and Rust-backed Python transforms.

Rust-owned fixtures now live under crates\pytecode-engine\fixtures\. Rust crate tests only read those copied fixture sources and do not invoke Python. The Rust harness lazily compiles crates\pytecode-engine\fixtures\java\*.java into target\pytecode-rust-javac\... and only reruns javac when the source bytes, required --release, or javac identity changes.

bench-smoke now reports isolated-stage timing samples with median+spread summaries instead of only one accumulated elapsed total. tools\benchmark_jar_pipeline.py mirrors that isolated-stage reporting for the wrapper-inclusive Python path, and tools\compare_rust_python_benchmarks.py frames the result as native Rust timings versus Python wrapper overhead on the same jar.

Small Rust examples live under crates\pytecode-engine\examples\finalize_main.rs and crates\pytecode-archive\examples\rewrite_jar.rs to show direct transform and archive rewrite workflows without going through the CLI.

Workspace layout:

  • pytecode-engine for classfile parsing, editing, transforms, analysis, and validation
  • pytecode-archive for JAR handling
  • pytecode-cli for CLI workflows and benchmarks
  • pytecode-python for PyO3 bindings and wheel packaging

Generate local API reference HTML with:

uv run python tools/generate_api_docs.py

Build source and wheel distributions locally:

uv build

uv build produces both an sdist and a platform wheel for the current machine.

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

Compare native Rust vs Python-via-Rust stage timings with the Python extension rebuilt in release mode by default:

uv run python tools/bench_full_comparison.py
uv run python tools/bench_full_comparison.py --extension-build installed

When making runtime-performance changes, prefer checking both a focused jar such as crates\pytecode-engine\fixtures\jars\byte-buddy-1.17.5.jar and the wider common-jar corpus so regressions and wins are not judged from a single artifact. Byte Buddy is the default focused Rust benchmark fixture because it is a common JVM library, carries a much larger class corpus than the old 225.jar fixture, and also includes newer multi-release classes than Guava. A single jar defaults to all stages; directories and multi-jar runs default to model-lift and model-lower.

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 the source distribution plus platform wheels, 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
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

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

pytecode-0.1.0.tar.gz (7.9 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pytecode-0.1.0-cp312-abi3-win_arm64.whl (1.6 MB view details)

Uploaded CPython 3.12+Windows ARM64

pytecode-0.1.0-cp312-abi3-win_amd64.whl (1.7 MB view details)

Uploaded CPython 3.12+Windows x86-64

pytecode-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.9 MB view details)

Uploaded CPython 3.12+manylinux: glibc 2.17+ x86-64

pytecode-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.8 MB view details)

Uploaded CPython 3.12+manylinux: glibc 2.17+ ARM64

pytecode-0.1.0-cp312-abi3-macosx_11_0_arm64.whl (1.7 MB view details)

Uploaded CPython 3.12+macOS 11.0+ ARM64

pytecode-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl (1.8 MB view details)

Uploaded CPython 3.12+macOS 10.12+ x86-64

File details

Details for the file pytecode-0.1.0.tar.gz.

File metadata

  • Download URL: pytecode-0.1.0.tar.gz
  • Upload date:
  • Size: 7.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytecode-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2f7f1a6009685ad748a9624d25bdd14f3214cf79c9e94fde03461dbb91cd4e33
MD5 21e9fb46ec3b73a62d5b82224b5c197b
BLAKE2b-256 3f1b79fd63cd530ff047caca468fa6140fcf51d3719aed0fa5c2e797db1b9ec8

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytecode-0.1.0.tar.gz:

Publisher: release.yml on smithtrenton/pytecode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytecode-0.1.0-cp312-abi3-win_arm64.whl.

File metadata

  • Download URL: pytecode-0.1.0-cp312-abi3-win_arm64.whl
  • Upload date:
  • Size: 1.6 MB
  • Tags: CPython 3.12+, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytecode-0.1.0-cp312-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 54e3f25e0eb2d88c751d66713a7f6148b043e6b9c508f37cb9f01e81cda57799
MD5 917110342de7d3a707e6f7d39d6393f5
BLAKE2b-256 c573b0ff54dc08245862fcca2a3716ed3bf4564d85a03b91bf0a4e79ca35634f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytecode-0.1.0-cp312-abi3-win_arm64.whl:

Publisher: release.yml on smithtrenton/pytecode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytecode-0.1.0-cp312-abi3-win_amd64.whl.

File metadata

  • Download URL: pytecode-0.1.0-cp312-abi3-win_amd64.whl
  • Upload date:
  • Size: 1.7 MB
  • Tags: CPython 3.12+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytecode-0.1.0-cp312-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 437d49e3716cd91a4b378b35483b81ca25979ae8f81f43bb007fcd0a0e3476c3
MD5 d2ea3a521319c3be91fc63996e00e715
BLAKE2b-256 c478db40a7cbc3c648b2c72c4b2e156addb82ae394599ceea51f1cf1cc3e375f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytecode-0.1.0-cp312-abi3-win_amd64.whl:

Publisher: release.yml on smithtrenton/pytecode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytecode-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pytecode-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1f87773f074379c72a1230fe2b5e6772945b38447dcdf6a4bc58f955a57bfa0f
MD5 7fd7220d7ff44c9017b3badc54ca9f71
BLAKE2b-256 f23821146657b09f305a4f410b65c1a70a92abb07eb898f25920dd056abea790

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytecode-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on smithtrenton/pytecode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytecode-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pytecode-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 cabd96d213944267d1ace79f01c0c917e87b02daf754fef44cc67ce882dae4cf
MD5 738d93811dc17afbad336f58ff224c7e
BLAKE2b-256 4714567900c7c0297533d489c84ce4d5954f416ff5116677ab89e12b24217920

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytecode-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on smithtrenton/pytecode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytecode-0.1.0-cp312-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pytecode-0.1.0-cp312-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 761319f6bd74f74101d63bb3a187f447595a571e1e5a03f7609550d7ac71f5a2
MD5 59db26fd01df9202dbe70465e5644d84
BLAKE2b-256 d68a08f2d62d4ccfdbdff36bf36cbbdb7d8fa7f440503c8ebe367cc2c07c9650

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytecode-0.1.0-cp312-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on smithtrenton/pytecode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytecode-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pytecode-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 b1949e5a9ba685f253b065690013d7cfd7e4c04fdc1cfda87871b434455928e1
MD5 089cccbcd22f233e26fb5818e5b63a1f
BLAKE2b-256 f240059944bb2d093dd097863fdbe58a24e68e90840fdca55cc9735643a6f906

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytecode-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on smithtrenton/pytecode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page