diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index b1787c2..700eb71 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -12,12 +12,12 @@ jobs:
       PYTHONUNBUFFERED: '1'
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           submodules: 'true'
       
       - name: Python environment
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: '3.10'
 
@@ -42,9 +42,9 @@ jobs:
           python tests/invalid.py
     
       - name: Upload wheels
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
-          name: wheels
+          name: wheels-windows
           path: dist
 
   macos:
@@ -56,12 +56,12 @@ jobs:
       PYTHONUNBUFFERED: '1'
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           submodules: 'true'
       
       - name: Python environment
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: '3.10'
 
@@ -90,22 +90,22 @@ jobs:
           python tests/invalid.py
     
       - name: Upload wheels
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
-          name: wheels
+          name: wheels-macos
           path: dist
 
   linux:
     # Skip building pull requests from the same repository
     if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
-    runs-on: ubuntu-latest
-    container: quay.io/pypa/manylinux2014_x86_64
+    runs-on: ubuntu-24.04
+    container: quay.io/pypa/manylinux_2_28_x86_64
     env:
       # Disable output buffering in an attempt to get readable errors
       PYTHONUNBUFFERED: '1'
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           submodules: 'true'
       
@@ -116,7 +116,7 @@ jobs:
           export PATH="$PATH:$HOME/.cargo/bin"
           export PATH="/opt/python/cp38-cp38/bin:$PATH"
           pip install -r requirements.txt
-          python setup.py bdist_wheel --py-limited-api=cp37 --plat-name manylinux2014_x86_64
+          python setup.py bdist_wheel --py-limited-api=cp37 --plat-name manylinux_2_28_x86_64
           auditwheel show dist/*.whl
           pip install --force-reinstall dist/*.whl
           python -c "import icicle"
@@ -129,23 +129,24 @@ jobs:
           python tests/invalid.py
     
       - name: Upload wheels
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
-          name: wheels
+          name: wheels-linux
           path: dist
 
   release:
     if: ${{ startsWith(github.ref, 'refs/tags/') }}
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs: [windows, macos, linux]
     permissions:
       contents: write
       discussions: write
     steps:
       - name: Download wheels
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
-          name: wheels
+          pattern: wheels-*
+          merge-multiple: true
           path: dist
 
       - name: Publish to PyPI
diff --git a/Cargo.lock b/Cargo.lock
index 5ff517b..10ef78e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -406,7 +406,7 @@ dependencies = [
 
 [[package]]
 name = "icicle-python"
-version = "0.0.3"
+version = "0.0.4"
 dependencies = [
  "icicle-cpu",
  "icicle-vm",
diff --git a/Cargo.toml b/Cargo.toml
index d59bfb2..cdea8d6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "icicle-python"
-version = "0.0.3"
+version = "0.0.4"
 edition = "2021"
 
 [lib]
diff --git a/python/icicle/__init__.py b/python/icicle/__init__.py
index 5fb47c3..3e931e5 100644
--- a/python/icicle/__init__.py
+++ b/python/icicle/__init__.py
@@ -1,4 +1,137 @@
-from .icicle import *
+from typing import List, Dict, Tuple
+from enum import Enum
+
+class MemoryProtection(Enum):
+    NoAccess = ...
+    ReadOnly = ...
+    ReadWrite = ...
+    ExecuteOnly = ...
+    ExecuteRead = ...
+    ExecuteReadWrite = ...
+
+class MemoryExceptionCode(Enum):
+    Unallocated = ...
+    Unmapped = ...
+    UnmappedRegisters = ...
+    Uninitialized = ...
+    ReadViolation = ...
+    WriteViolation = ...
+    ExecViolation = ...
+    ReadWatch = ...
+    WriteWatch = ...
+    Unaligned = ...
+    OutOfMemory = ...
+    SelfModifyingCode = ...
+    AddressOverflow = ...
+    Unknown = ...
+
+class RunStatus(Enum):
+    Running = ...
+    InstructionLimit = ...
+    Breakpoint = ...
+    Interrupted = ...
+    Halt = ...
+    Killed = ...
+    Deadlock = ...
+    OutOfMemory = ...
+    Unimplemented = ...
+    UnhandledException = ...
+
+class ExceptionCode(Enum):
+    NoException = ...
+    InstructionLimit = ...
+    Halt = ...
+    Sleep = ...
+    Syscall = ...
+    CpuStateChanged = ...
+    DivisionException = ...
+    ReadUnmapped = ...
+    ReadPerm = ...
+    ReadUnaligned = ...
+    ReadWatch = ...
+    ReadUninitialized = ...
+    WriteUnmapped = ...
+    WritePerm = ...
+    WriteWatch = ...
+    WriteUnaligned = ...
+    ExecViolation = ...
+    SelfModifyingCode = ...
+    OutOfMemory = ...
+    AddressOverflow = ...
+    InvalidInstruction = ...
+    UnknownInterrupt = ...
+    UnknownCpuID = ...
+    InvalidOpSize = ...
+    InvalidFloatSize = ...
+    CodeNotTranslated = ...
+    ShadowStackOverflow = ...
+    ShadowStackInvalid = ...
+    InvalidTarget = ...
+    UnimplementedOp = ...
+    ExternalAddr = ...
+    Environment = ...
+    JitError = ...
+    InternalError = ...
+    UnmappedRegister = ...
+    UnknownError = ...
+
+class Icicle:
+    def __init__(self, architecture: str, *,
+                 jit = True,
+                 jit_mem = True,
+                 shadow_stack = True,
+                 recompilation = True,
+                 track_uninitialized = False,
+                 optimize_instructions = True,
+                 optimize_block = True,
+                 tracing = False,
+                 ) -> None: ...
+
+    @property
+    def exception_code(self) -> ExceptionCode: ...
+
+    @property
+    def exception_value(self) -> int: ...
+
+    icount: int
+
+    icount_limit: int
+
+    # TODO: API to get memory information?
+
+    def mem_map(self, address: int, size: int, protection: MemoryProtection): ...
+
+    def mem_unmap(self, address: int, size: int): ...
+
+    def mem_protect(self, address: int, size: int, protection: MemoryProtection): ...
+
+    def mem_read(self, address: int, size: int) -> bytes: ...
+
+    def mem_write(self, address: int, data: bytes) -> None: ...
+
+    def reg_list(self) -> Dict[str, Tuple[int, int]]: ...
+
+    def reg_offset(self, name: str) -> int: ...
+
+    def reg_size(self, name: str) -> int: ...
+
+    def reg_read(self, name: str) -> int: ...
+
+    def reg_write(self, name: str, value: int) -> None: ...
+
+    def reset(self): ...
+
+    def run(self) -> RunStatus: ...
+
+    def run_until(self, address: int) -> RunStatus: ...
+
+    def step(self, count: int) -> RunStatus: ...
+
+    def add_breakpoint(self, address: int) -> bool: ...
+
+    def remove_breakpoint(self, address: int) -> bool: ...
+
+def architectures() -> List[str]: ...
 
 class MemoryException(Exception):
     def __init__(self, message: str, code: MemoryExceptionCode):
@@ -19,3 +152,6 @@ def __ghidra_init():
     raise FileNotFoundError("Ghidra processor definitions not found")
 
 __ghidra_init()
+
+# NOTE: This overrides the stubs at runtime with the actual implementation
+from .icicle import *
\ No newline at end of file
diff --git a/python/icicle/icicle.pyi b/python/icicle/icicle.pyi
deleted file mode 100644
index fbb3900..0000000
--- a/python/icicle/icicle.pyi
+++ /dev/null
@@ -1,134 +0,0 @@
-from typing import List, Dict, Tuple
-from enum import Enum
-
-class MemoryProtection(Enum):
-    NoAccess = ...
-    ReadOnly = ...
-    ReadWrite = ...
-    ExecuteOnly = ...
-    ExecuteRead = ...
-    ExecuteReadWrite = ...
-
-class MemoryExceptionCode(Enum):
-    Unallocated = ...
-    Unmapped = ...
-    UnmappedRegisters = ...
-    Uninitialized = ...
-    ReadViolation = ...
-    WriteViolation = ...
-    ExecViolation = ...
-    ReadWatch = ...
-    WriteWatch = ...
-    Unaligned = ...
-    OutOfMemory = ...
-    SelfModifyingCode = ...
-    AddressOverflow = ...
-    Unknown = ...
-
-class RunStatus(Enum):
-    Running = ...
-    InstructionLimit = ...
-    Breakpoint = ...
-    Interrupted = ...
-    Halt = ...
-    Killed = ...
-    Deadlock = ...
-    OutOfMemory = ...
-    Unimplemented = ...
-    UnhandledException = ...
-
-class ExceptionCode(Enum):
-    NoException = ...
-    InstructionLimit = ...
-    Halt = ...
-    Sleep = ...
-    Syscall = ...
-    CpuStateChanged = ...
-    DivisionException = ...
-    ReadUnmapped = ...
-    ReadPerm = ...
-    ReadUnaligned = ...
-    ReadWatch = ...
-    ReadUninitialized = ...
-    WriteUnmapped = ...
-    WritePerm = ...
-    WriteWatch = ...
-    WriteUnaligned = ...
-    ExecViolation = ...
-    SelfModifyingCode = ...
-    OutOfMemory = ...
-    AddressOverflow = ...
-    InvalidInstruction = ...
-    UnknownInterrupt = ...
-    UnknownCpuID = ...
-    InvalidOpSize = ...
-    InvalidFloatSize = ...
-    CodeNotTranslated = ...
-    ShadowStackOverflow = ...
-    ShadowStackInvalid = ...
-    InvalidTarget = ...
-    UnimplementedOp = ...
-    ExternalAddr = ...
-    Environment = ...
-    JitError = ...
-    InternalError = ...
-    UnmappedRegister = ...
-    UnknownError = ...
-
-class Icicle:
-    def __init__(self, architecture: str, *,
-                 jit = True,
-                 jit_mem = True,
-                 shadow_stack = True,
-                 recompilation = True,
-                 track_uninitialized = False,
-                 optimize_instructions = True,
-                 optimize_block = True,
-                 tracing = False,
-                 ) -> None: ...
-
-    @property
-    def exception_code(self) -> ExceptionCode: ...
-
-    @property
-    def exception_value(self) -> int: ...
-
-    icount: int
-
-    icount_limit: int
-
-    # TODO: API to get memory information?
-
-    def mem_map(self, address: int, size: int, protection: MemoryProtection): ...
-
-    def mem_unmap(self, address: int, size: int): ...
-
-    def mem_protect(self, address: int, size: int, protection: MemoryProtection): ...
-
-    def mem_read(self, address: int, size: int) -> bytes: ...
-
-    def mem_write(self, address: int, data: bytes) -> None: ...
-
-    def reg_list(self) -> Dict[str, Tuple[int, int]]: ...
-
-    def reg_offset(self, name: str) -> int: ...
-
-    def reg_size(self, name: str) -> int: ...
-
-    def reg_read(self, name: str) -> int: ...
-
-    def reg_write(self, name: str, value: int) -> None: ...
-
-    def reset(self): ...
-
-    def run(self) -> RunStatus: ...
-
-    def run_until(self, address: int) -> RunStatus: ...
-
-    def step(self, count: int) -> RunStatus: ...
-
-    def add_breakpoint(self, address: int) -> bool: ...
-
-    def remove_breakpoint(self, address: int) -> bool: ...
-
-def architectures() -> List[str]: ...
diff --git a/python/icicle/py.typed b/python/icicle/py.typed
deleted file mode 100644
index e69de29..0000000