Skip to main content

bethkit.py

License Python PyPI

Python bindings for bethkit, a Rust library for reading and writing Bethesda plugin and archive files. Python provides typed views and editing workflows; Bethkit remains responsible for binary formats, schema interpretation, and lossless writing.

Python 2.1.0 uses Bethkit 0.6.0, pinned together with its release artifacts in native-source.json. This version adds an explicit inline-text encoding choice for translations such as the German USSEP ESP. Older native binaries do not provide the required encoding override. See BUILDING.md for source builds and release gates.

Features

  • Plugin reading - open .esp/.esm/.esl files by path or from in-memory bytes; iterate groups and records; look up records by FormID or EditorID; inspect sub-records
  • Plugin writing - build new plugins from scratch with PluginWriter, WritableGroup, and WritableRecord
  • BSA / BA2 archives - open and extract entries from BSA (TES4/SSE) and BA2 (GNRL/DX10) archives; write new archives with BsaWriter, Ba2GnrlWriter, and Ba2Dx10Writer
  • String tables - read, edit, and write .STRINGS/.DLSTRINGS/.ILSTRINGS localisation files; apply translation sets with LocalizationSet
  • Typed records - schema-generated, immutable models for all 134 Skyrim SE record definitions, with nested Quest stages, dialogue responses, and perk effects; models load only when requested.
  • Semantic editing - edit typed fields through structural addresses, validate records, and patch existing plugins while preserving untouched record bytes.
  • Localization - enumerate inline and external text by position, isolate changes to shared string IDs, and save a plugin together with its tables as a new output bundle.
  • Load-order utilities - LoadOrder, GlobalFormId, and PluginCache for winning-override lookups and EditorID search across multiple plugins

Requirements

Requirement Version
Python ≥ 3.10
pydantic ≥ 2.5.3, < 3

Release wheels bundle the matching native library with the Skyrim SE schema only. Access the embedded package through SchemaCatalog.embedded() and catalog.package(Game.SKYRIM_SE), or open an explicit .bkschema file as in the examples below. Local development requires the pinned native build. Generated types also check the exact schema payload hash; an ABI-compatible library alone does not make a different schema compatible.

Schema definitions originate in xEdit and are exported and published by Modding-Forge/xDump. See SCHEMA.md for coverage, provenance, generation, and current limitations.

Installation

uv add bethkit

or

pip install bethkit

Quick Start

Reading a plugin

from pathlib import Path
from bethkit import Plugin, Game

with Plugin.open(Path("Ordinator - Perks of Skyrim.esp"), Game.SKYRIM_SE) as plugin:
    print("Masters:", plugin.masters)
    print("Kind:", plugin.kind)

    for group in plugin:
        for child in group:
            if hasattr(child, "form_id"):
                print(f"  0x{child.form_id:08X}  {child.editor_id}")

Reading and editing a typed quest

from pathlib import Path

from bethkit import Game, Plugin, PluginPatcher, SchemaPackage, SemanticContext
from bethkit.records.skyrim_se import QuestRecord

with SchemaPackage.open(Path("skyrim_se.bkschema")) as package:
    with SemanticContext(package) as context:
        with Plugin.open(Path("MyMod.esp"), Game.SKYRIM_SE) as plugin:
            record = plugin.find_record(0x800)
            if record is None:
                raise LookupError("Quest 0x800 was not found")
            quest = QuestRecord.from_record(
                record, context, localized=plugin.is_localized
            )
            for stage in quest.stages:
                for entry in stage.log_entries:
                    print(entry.log_entry)
            with context.edit(record, localized=plugin.is_localized) as editor:
                editor.set(quest.field("editor_id"), "MyRenamedQuest")
                with editor.finish() as replacement:
                    with PluginPatcher(plugin) as patcher:
                        patcher.replace_record(record.form_id, replacement)
                        patcher.write_to_file(Path("MyMod-edited.esp"))

The generated view is read-only and remains usable after its source handles close. Edits use the native editor, including schema validation and callbacks. Structural changes require fresh addresses from editor.snapshot(); ordinary scalar or text-length changes retain their positions. The generic RecordView and SemanticContext.snapshot() remain available for inspectors and unknown fields.

Editing localized text safely

from pathlib import Path

from bethkit import (
    Game, LocalizationEditor, LocalizationSet, Plugin,
    SchemaPackage, SemanticContext,
)

source = Path("MyLocalizedMod.esp")
with SchemaPackage.open(Path("skyrim_se.bkschema")) as package:
    with SemanticContext(package) as context:
        with Plugin.open(source, Game.SKYRIM_SE) as plugin:
            with LocalizationSet.open(source, "english") as tables:
                with LocalizationEditor(plugin, context, tables) as editor:
                    for string in editor.iter_strings():
                        if string.text == "Old text":
                            editor.replace(string, "New text")
                    editor.save_bundle(
                        Path("edited-bundle"), source.name, language="english"
                    )

For an inline-string plugin, omit tables. A string's identity contains its plugin and structural position, never its text or external table ID. Replacing one external string allocates a fresh ID so other users of a shared ID are unchanged. Saving creates a new directory containing the plugin, its Strings/ files, and a checksum manifest; it never overwrites an installed Data directory.

Bethkit normally uses each schema field's declared encoding for inline text. Some translations, including the German USSEP ESP, store UTF-8 text where the Skyrim SE schema declares Windows-1252. For these plugins, create SemanticContext(package, inline_encoding="utf-8") and use that same context for reading and editing. The override applies only to schema-localizable text stored inline; technical fields keep their schema codec, and external string tables have their own encoding option. It requires a native Bethkit build that supports inline encoding overrides.

Building a plugin from scratch

from pathlib import Path
from bethkit import Game, PluginWriter, WritableGroup, WritableRecord

with PluginWriter(Game.SKYRIM_SE) as writer:
    with WritableGroup.new(b"NPC_") as group:
        rec = WritableRecord.new(b"NPC_", form_id=0x000D62)
        rec.add_subrecord(b"EDID", b"MyNPC\x00")
        group.add_record(rec)
        writer.add_group(group)
    writer.write_to_file(Path("MyMod.esp"))

Extracting from an archive

from pathlib import Path
from bethkit import Archive

with Archive.open(Path("Skyrim - Meshes.bsa")) as arc:
    data = arc.extract("meshes/actors/character/character assets/skeleton.nif")
    if data:
        Path("skeleton.nif").write_bytes(data)

Load-order and FormID resolution

from pathlib import Path
from bethkit import Game, Plugin, PluginCache, PluginKind, LoadOrder

lo = LoadOrder()
lo.push("Skyrim.esm", PluginKind.FULL)
lo.push("MyMod.esp", PluginKind.FULL)

cache = PluginCache()
cache.add("Skyrim.esm", Plugin.open(Path("Skyrim.esm"), Game.SKYRIM_SE))

hit = cache.find_by_editor_id("ArmorIronCuirass")
if hit:
    print(hit.global_form_id)   # Skyrim.esm:0x012E49
    print(hit.record.editor_id)

Development

git clone https://github.com/Modding-Forge/bethkit.py
cd bethkit.py
uv sync --extra dev
uv run pytest

Linting and type-checking:

uv run ruff check src/ tests/ scripts/
uv run ruff format --check src/ tests/ scripts/
uv run pyright src/ tests/ scripts/

Unit tests mock the native boundary. A release-quality check must also set BETHKIT_LIB and BETHKIT_SCHEMA and run pytest --require-native; an explicitly configured but incompatible library is an error, not a skipped test. See BUILDING.md for the pinned workflow and SCHEMA.md for deterministic model generation.

Contributions follow the Python style conventions. Maintained files are limited to 400 lines, enforced by local commit/push hooks, CI, and the build backend; individual generated-file exceptions are recorded explicitly in file-length-policy.json. Install the local guards with uv run python scripts/install_git_hooks.py after setting up the development environment.

  • bethkit - the underlying Rust library; bethkit.py wraps its C ABI

License

Apache-2.0 - see LICENSE.

Metadata

Release files for bethkit 2.1.0

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

Built distributions (wheels)

Table of built distributions (wheels) for bethkit 2.1.0
File Interpreter ABI Platform
bethkit-2.1.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
bethkit-2.1.0-py3-none-manylinux_2_34_x86_64.whl Python 3 none Linux glibc 2.34+ x86-64 Details

Total release size: 5.9 MB

Release files / bethkit-2.1.0-py3-none-win_amd64.whl

Download URL bethkit-2.1.0-py3-none-win_amd64.whl
Size 2.9 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
e831306ab970b6f548c18b2978ae1c28ed5ab063f0342cb8108d5b3c9c0145a6
BLAKE2b-256 checksum
How to use checksums
2ec74fb216372bff32dc3b0c29d1551ae189125f65309a3532ae3c1aca7028d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / bethkit-2.1.0-py3-none-manylinux_2_34_x86_64.whl

Download URL bethkit-2.1.0-py3-none-manylinux_2_34_x86_64.whl
Size 3.0 MB
Tags Linux glibc 2.34+ x86-64 Python 3
SHA-256 checksum
How to use checksums
6c15564fbcd03ec1b03b5fff3a378cb62a67a283c8a82ae40f099b1ec08ac400
BLAKE2b-256 checksum
How to use checksums
0aa6224e237821f88775e15d89f9ecb0f09a77b62fabe04e1ba2d46d7edfaaf4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

2.3.0

2 release files

2.2.0

2 release files

This release

2.1.0 This release

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 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