Python MOD

Python MOD

This mod lets you use Python to connect to the Python MOD API

3 downloads

Python MOD

Write Minecraft mods in Python.

Python MOD is a Fabric mod that embeds a real Python 3 runtime into Minecraft and exposes a sandboxed API for registering content, hooking game events and building screens. Drop a .pymod file into a folder, restart, and your Python is running inside the game — no Java, no Gradle, no recompiling.

  • Minecraft 1.21.1 · Fabric Loader 0.16+ · Fabric API
  • Runtime: GraalPy 24.2.2 (CPython 3.11 compatible)
  • API level: 1
  • Licence: Available License — © 2026 kscm · Project Owner: kscm

Hello, world

hello_world.pymod is a zip with two files:

hello_world.pymod
├── pymod.json
└── src/
    └── main.py

pymod.json

{
  "id": "hello_world",
  "name": "Hello World",
  "version": "1.0.0",
  "api_version": 1,
  "entrypoint": "src/main.py",
  "permissions": []
}

src/main.py

import pythonmod
from pythonmod import registry, events

registry.block("hello_block", hardness=1.0, sounds="wood", name="Hello Block")

@events.player_join
def greet(player):
    player.send_message("Hello from Python!")

Put it in .minecraft/mods/pythonmods/, start the game, press P.


What the API covers

pythonmod.registry — content

Function Registers
block(id, hardness, resistance, requires_tool, luminance, sounds, group, item, texture, name) A full-cube block, optionally with a block item
item(id, max_stack, group, texture, name) A plain item
food(id, nutrition, saturation, always_edible, max_stack, group, texture, name) An edible item

Everything is namespaced under your module id, so block("ruby_ore") in a package with "id": "gems" becomes gems:ruby_ore. Two unrelated packages can both define ruby_ore and never collide.

Models and English names are generated into a PythonMOD Generated resource pack, so new content is usable before you have drawn a single texture.

pythonmod.events — game hooks

Event Arguments
server_tick (server)
server_started / server_stopping (server)
player_join / player_leave (player)
block_break (player, pos, block)
block_use (player, pos)
item_use (player)
client_tick (client)
from pythonmod import events

@events.block_break
def on_break(player, pos, block):
    player.send_actionbar("broke %s at %d,%d,%d" % (block.id(), pos.x(), pos.y(), pos.z()))

An unrecognised event name raises at subscription time. A hook that silently never fires is the most expensive kind of typo, so it is not allowed to happen.

pythonmod.gui — screens

from pythonmod import gui

(gui.screen("Ore Scanner")
    .size(280, 160)
    .label("Radius", 20, 34)
    .slider("blocks", 20, 48, 240, value=16, min=4, max=64, on_change=set_radius)
    .checkbox("Include deepslate", 20, 80, checked=True, on_change=set_deepslate)
    .button("Scan", 20, 110, 240, 20, on_click=scan)
    .open())

Coordinates are relative to the panel and the panel is centred for you. Light and dark palettes are built in and follow the player's setting — a script never picks colours by hand.

On a dedicated server there is no client, so open() returns False and logs a warning instead of raising. Guard with gui.available() when a package is meant to run on both sides.

pythonmod.xmlgui — declarative HUD (XML)

Declare an in-game HUD panel in gui/*.xml — no Python screen code required. Each module's gui/ folder is scanned automatically at load time.

gui/hud.xml

<gui>
  <screen id="hud_demo" show_page="hud" x="12" y="12">
    <label id="title" text="Python MOD · XML HUD" x="0" y="0"
           style="color:#1b1f24" style_dark="color:#eceff3"/>
    <divider x="0" y="20" w="180"/>
    <button id="mine" text="Mine +1" x="0" y="30" w="160" h="20"
            style="bg:#1565c0;color:#ffffff" style_dark="bg:#5fa8ff;color:#0b0e12"
            click_script="on_click.py" follow_script="after.py"/>
    <checkbox id="auto" text="Auto mine" x="0" y="58" checked="true"
              style="color:#1b1f24" style_dark="color:#eceff3"
              click_script="on_toggle.py"/>
    <slider id="speed" text="Speed" x="0" y="84" w="180" value="0.5" min="0" max="1"/>
    <text_field id="status" placeholder="Status: ready" x="0" y="110" w="180" h="20"/>
  </screen>
</gui>
Attribute On Meaning
style / style_dark widget CSS-like key:value pairs (color, bg, border). The active light/dark theme picks which one
click_script button, checkbox Python file run when the widget is clicked
follow_script button Python file run right after click_script completes
show_page screen Where to show it: hud (in-game overlay). screen / main_menu are reserved
x / y screen, widget Coordinates in GUI pixels; the screen is the panel anchor, a widget is relative to it

The scripts run in an isolated context — same permissions and sandbox as the module, but a crash disables only that script, never the whole module. They can import pythonmod like any other code. A clicked button runs click_script then follow_script; the same path is reachable from Python:

from pythonmod import xmlgui
xmlgui.trigger("hud_demo", "mine")   # runs on_click.py then after.py

See examples/xml_hud_demo/ for a complete, runnable panel (button, checkbox, slider and text field with light/dark styles and Python scripts).


The manifest

Field Required Notes
id yes 2–64 chars of a-z, 0-9, _. Becomes your registry namespace
version yes Free-form
api_version yes Must be 1. See Attribution below
name no Defaults to id
description no Shown in the manager screen
authors no Array of strings
entrypoint no Defaults to src/main.py
environment no *, client or server
permissions no See the table below

Permissions

Permission Risk Grants
file_read low Reading inside your own data folder
file_write medium Writing inside your own data folder
network high Outbound sockets
thread high Creating threads
native critical Native/JNI access — effectively no sandbox
process critical Spawning processes — effectively no sandbox

The sandbox confines file access to pythonmod/data/<your-id>/ regardless of what is granted. pythonmod.data_path("state.json") builds a legal path; anything outside that tree is refused before it reaches the filesystem.

Packages asking for native or process produce a loud warning in the log and a red entry in the manager screen. That is deliberate: those two permissions hand a script the same power a Java mod has.


In-game manager

Press P (rebindable) for:

  • what loaded, what failed, and the exact reason
  • what each package registered, and how long it took
  • the permissions it was granted, colour-coded by risk
  • the last few lines it printed
  • a light / dark / auto theme switch, saved to the config

Configuration

.minecraft/config/pythonmod.json

Key Default Meaning
enabled true Master switch
autoDownloadRuntime true Fetch GraalPy on first launch
mavenRepository Maven Central Mirror to download from
verifyChecksums true Verify every downloaded jar
theme "auto" auto, light or dark
callbackTimeoutMs 5000 A callback exceeding this disables its package
verboseLogging false Extra diagnostics

How it works

preLaunch   RuntimeProvisioner   downloads GraalPy, verifies SHA-1,
                                 injects it into the Knot class loader
   ↓
main        PythonEngine         one shared Engine
            SandboxPolicy        one Context per package, with its own
                                 permission set and virtual filesystem
            PyModLoader          scan → validate → unpack → run
            PyBridge             the single Java object Python can reach
   ↓
runtime     PyEventBridge        Fabric callbacks fan out to Python hooks
            PyGuiBridge          screen descriptions, rendered client-side
            XmlHudRenderer       XML-declared HUD panels, scanned from gui/*.xml
            XmlGuiBridge         fires a widget's click/follow scripts

The runtime is not bundled. GraalPy is roughly 200 MB — larger than most modpacks. It is downloaded once on first launch, checksum-verified, and cached in .minecraft/pythonmod/runtime/. If the download fails the game still starts; Python support is simply reported as unavailable.

One context per package, not one shared context. A polyglot context cannot hold two different permission sets, so a shared one would grant every package the union of everyone's permissions — one package asking for network would silently hand network access to all the others. Separate contexts also mean a crashing package can be disabled on its own.

Guest exceptions never escape. Every call into Python goes through PythonEngine.invoke, which catches the guest exception, prints the Python traceback, and disables the offending package. A typo in a script is not allowed to take the server down.


Building

Requires JDK 21. A local Temurin 21 lives in .tooling/jdk-21; point org.gradle.java.home in gradle.properties at your own if you prefer.

./gradlew build          # builds the mod jar
./gradlew packExamples   # packs examples/ into build/pymods/*.pymod

Attribution

Python MOD is distributed under the Available License (see LICENSE), © 2026 kscm, Project Owner kscm.

Section 6 requires software using this API to state the original project name and version. Python MOD does that for you:

  • api_version is mandatory in every manifest — a package without it is refused rather than silently loaded
  • the loader prints <package> v<version> — Powered by Python MOD v<x.y.z> (API <n>) when the package starts
  • the same string is available as pythonmod.ATTRIBUTION
  • gradlew packExamples writes ATTRIBUTION.txt into every archive

Declaring api_version is therefore all an author has to do.

Section 3 — using this project's source code requires authorisation from the Project Owner, and any such use must state the original project name, team and version.

Section 7 — "Python MOD" is used as the sole project name; no translated name is claimed or authorised.

Not affiliated with Mojang Studios or Microsoft.

No gallery available for this project.