diff --git a/.gitignore b/.gitignore
index c093af014652307f1bb2fc0f0a4a4d3c66d51132..b61206a4a5e2b58be1c92cfe751177e41a387591 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 *.pyc
+*.pyd
 
 .DS_Store
 
@@ -6,6 +7,8 @@ Cargo.lock
 
 /.vscode/settings.json
 
+/.eggs
+/.venv
 /.idea
 
 /ndk
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 40733f41dfc9d5e6c85dd7413bc84ab28e725546..9b52b50d959e6f0514e5985f51ee0341fd4453b2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -79,6 +79,17 @@ test-rust:
     - rustc --version
     - cargo test
 
+test-pyo3:
+  stage: test
+  parallel:
+    matrix:
+      - RUST_VERSION: ["1.74.0"]
+  script:
+    - rustup toolchain install $RUST_VERSION
+    - rustup override set $RUST_VERSION
+    - rustc --version
+    - python setup.py test
+
 deploy-netlify-preview:
   stage: deploy
   script:
diff --git a/README.md b/README.md
index cf6459a7b91135c31a4f9c199049aaa4548ed9a2..044f1c28ccccf783699497713f8a6448e3be5254 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,12 @@ cargo build
 pip install .
 ```
 
+or
+
+```bash
+python setup.py install
+```
+
 ### WASM for Node.js
 
 ```bash
diff --git a/frontends/libretro/src/lib.rs b/frontends/libretro/src/lib.rs
index cb8d838fce9cf50f6eafd31e3293570cbbc4e8d8..c1540645f8b814c5e03dd0e7bb467df9d9a2550b 100644
--- a/frontends/libretro/src/lib.rs
+++ b/frontends/libretro/src/lib.rs
@@ -385,7 +385,7 @@ pub unsafe extern "C" fn retro_load_game(game: *const RetroGameInfo) -> bool {
     instance.set_mode(mode);
     instance.reset();
     instance.load(true);
-    instance.load_cartridge(rom);
+    instance.load_cartridge(rom).unwrap();
     update_vars();
     true
 }
diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index f7852aa299139b204b140cb6e19bece953af8ca4..1c073ab5b14fcf2d1e91a89429e79d1d82f7f29d 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -8,6 +8,7 @@ pub mod test;
 use audio::Audio;
 use boytacean::{
     devices::{printer::PrinterDevice, stdout::StdoutDevice},
+    error::Error,
     gb::{AudioProvider, GameBoy, GameBoyMode},
     info::Info,
     pad::PadKey,
@@ -225,7 +226,7 @@ impl Emulator {
         ));
     }
 
-    pub fn load_rom(&mut self, path: Option<&str>) {
+    pub fn load_rom(&mut self, path: Option<&str>) -> Result<(), Error> {
         let rom_path: &str = path.unwrap_or(&self.rom_path);
         let ram_path = replace_ext(rom_path, "sav").unwrap_or_else(|| "invalid".to_string());
         let rom = self.system.load_rom_file(
@@ -235,7 +236,7 @@ impl Emulator {
             } else {
                 None
             },
-        );
+        )?;
         println!(
             "========= Cartridge =========\n{}\n=============================",
             rom
@@ -253,12 +254,14 @@ impl Emulator {
             .to_str()
             .unwrap()
             .to_string();
+        Ok(())
     }
 
-    pub fn reset(&mut self) {
+    pub fn reset(&mut self) -> Result<(), Error> {
         self.system.reset();
         self.system.load(true);
-        self.load_rom(None);
+        self.load_rom(None)?;
+        Ok(())
     }
 
     pub fn apply_cheats(&mut self, cheats: &Vec<String>) {
@@ -418,7 +421,7 @@ impl Emulator {
                     Event::KeyDown {
                         keycode: Some(Keycode::R),
                         ..
-                    } => self.reset(),
+                    } => self.reset().unwrap(),
                     Event::KeyDown {
                         keycode: Some(Keycode::B),
                         ..
@@ -528,7 +531,7 @@ impl Emulator {
                         }
                         self.system.reset();
                         self.system.load(true);
-                        self.load_rom(Some(&filename));
+                        self.load_rom(Some(&filename)).unwrap();
                     }
                     _ => (),
                 }
@@ -989,7 +992,7 @@ fn main() {
     };
     let mut emulator = Emulator::new(game_boy, options);
     emulator.start(SCREEN_SCALE);
-    emulator.load_rom(Some(&args.rom_path));
+    emulator.load_rom(Some(&args.rom_path)).unwrap();
     emulator.apply_cheats(&args.cheats);
     emulator.toggle_palette();
 
diff --git a/setup.py b/setup.py
index 51b98c155e20cfa25c9120d3f5219e2f4b8db58d..f49970180c57bcf2ca1f41327841f1ea0fa1d4fb 100644
--- a/setup.py
+++ b/setup.py
@@ -35,6 +35,7 @@ setuptools.setup(
     keywords="gameboy emulator rust",
     url="https://boytacean.joao.me",
     packages=["boytacean"],
+    test_suite="boytacean.test",
     package_dir={"": os.path.normpath("src/python")},
     rust_extensions=[
         setuptools_rust.RustExtension(
diff --git a/src/gb.rs b/src/gb.rs
index 6386b2283d4f2b95f8b95357a6cc10c621ec509e..260828968485b9a31b6631a9ff1978154158653d 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -426,9 +426,20 @@ impl GameBoy {
         let rom = self.rom().clone();
         self.reset();
         self.load(true);
-        self.load_cartridge(rom);
+        self.load_cartridge(rom).unwrap();
     }
 
+    /// Advance the clock of the system by one tick, this will
+    /// usually imply executing one CPU instruction and advancing
+    /// all the other components of the system by the required
+    /// amount of cycles.
+    ///
+    /// This method takes into account the current speed of the
+    /// system (single or double) and will execute the required
+    /// amount of cycles in the other components of the system
+    /// accordingly.
+    ///
+    /// The amount of cycles executed by the CPU is returned.
     pub fn clock(&mut self) -> u16 {
         let cycles = self.cpu_clock() as u16;
         let cycles_n = cycles / self.multiplier() as u16;
@@ -1081,12 +1092,16 @@ impl GameBoy {
         Ok(())
     }
 
-    pub fn load_cartridge(&mut self, rom: Cartridge) -> &mut Cartridge {
+    pub fn load_cartridge(&mut self, rom: Cartridge) -> Result<&mut Cartridge, Error> {
         self.mmu().set_rom(rom);
-        self.mmu().rom()
+        Ok(self.mmu().rom())
     }
 
-    pub fn load_rom(&mut self, data: &[u8], ram_data: Option<&[u8]>) -> &mut Cartridge {
+    pub fn load_rom(
+        &mut self,
+        data: &[u8],
+        ram_data: Option<&[u8]>,
+    ) -> Result<&mut Cartridge, Error> {
         let mut rom = Cartridge::from_data(data);
         if let Some(ram_data) = ram_data {
             rom.set_ram_data(ram_data)
@@ -1094,11 +1109,15 @@ impl GameBoy {
         self.load_cartridge(rom)
     }
 
-    pub fn load_rom_file(&mut self, path: &str, ram_path: Option<&str>) -> &mut Cartridge {
-        let data = read_file(path).unwrap();
+    pub fn load_rom_file(
+        &mut self,
+        path: &str,
+        ram_path: Option<&str>,
+    ) -> Result<&mut Cartridge, Error> {
+        let data = read_file(path)?;
         match ram_path {
             Some(ram_path) => {
-                let ram_data = read_file(ram_path).unwrap();
+                let ram_data = read_file(ram_path)?;
                 self.load_rom(&data, Some(&ram_data))
             }
             None => self.load_rom(&data, None),
@@ -1184,12 +1203,12 @@ impl GameBoy {
         }));
     }
 
-    pub fn load_rom_wa(&mut self, data: &[u8]) -> Cartridge {
-        let rom = self.load_rom(data, None);
+    pub fn load_rom_wa(&mut self, data: &[u8]) -> Result<Cartridge, String> {
+        let rom = self.load_rom(data, None).map_err(|e| e.to_string())?;
         rom.set_rumble_cb(|active| {
             rumble_callback(active);
         });
-        rom.clone()
+        Ok(rom.clone())
     }
 
     pub fn load_callbacks_wa(&mut self) {
diff --git a/src/py.rs b/src/py.rs
index 2e948c70aa8be704a8652e40690915c90b5fe23d..a9fb17fbe80e32cde3fafeab3d6e5e71f63a45c7 100644
--- a/src/py.rs
+++ b/src/py.rs
@@ -46,12 +46,18 @@ impl GameBoy {
             .map_err(PyErr::new::<PyException, _>)
     }
 
-    pub fn load_rom(&mut self, data: &[u8]) {
-        self.system.load_rom(data, None);
+    pub fn load_rom(&mut self, data: &[u8]) -> PyResult<()> {
+        self.system
+            .load_rom(data, None)
+            .map(|_| ())
+            .map_err(PyErr::new::<PyException, _>)
     }
 
-    pub fn load_rom_file(&mut self, path: &str) {
-        self.system.load_rom_file(path, None);
+    pub fn load_rom_file(&mut self, path: &str) -> PyResult<()> {
+        self.system
+            .load_rom_file(path, None)
+            .map(|_| ())
+            .map_err(PyErr::new::<PyException, _>)
     }
 
     pub fn read_memory(&mut self, addr: u16) -> u8 {
diff --git a/src/python/boytacean/gb.py b/src/python/boytacean/gb.py
index ecf2292cfab7e84d823a30814611a40b02aa5ba5..5e2b3e3308226aaa74d5eeaca038e557e0c9e40d 100644
--- a/src/python/boytacean/gb.py
+++ b/src/python/boytacean/gb.py
@@ -2,7 +2,11 @@ from enum import Enum
 from contextlib import contextmanager
 from typing import Any, Iterable, Union, cast
 
-from PIL.Image import Image, frombytes
+try:
+    from PIL.Image import Image, frombytes
+except ImportError:
+    Image = Any
+    frombytes = Any
 
 from .palettes import PALETTES
 from .video import VideoCapture
diff --git a/src/python/boytacean/test/__init__.py b/src/python/boytacean/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/python/boytacean/test/base.py b/src/python/boytacean/test/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..487f0230675e1aafecc8888c02e01a42f68a29d8
--- /dev/null
+++ b/src/python/boytacean/test/base.py
@@ -0,0 +1,21 @@
+import unittest
+
+from os.path import dirname, realpath, join
+
+from boytacean import GameBoy
+
+
+class BaseTest(unittest.TestCase):
+
+    def test_pocket(self):
+        CURRENT_DIR = dirname(realpath(__file__))
+        ROM_PATH = join(CURRENT_DIR, "../../../../res/roms/demo/pocket.gb")
+        FRAME_COUNT = 600
+        LOAD_GRAPHICS = False
+
+        gb = GameBoy(
+            apu_enabled=False, serial_enabled=False, load_graphics=LOAD_GRAPHICS
+        )
+        gb.load_rom(ROM_PATH)
+        for _ in range(FRAME_COUNT):
+            gb.next_frame()
diff --git a/src/test.rs b/src/test.rs
index ae82c98b135a2e1dae87966b4348e23adba530c4..6ebd856c258a4fdf40137dcd5eb7ef5e49cada4a 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -1,5 +1,6 @@
 use crate::{
     devices::buffer::BufferDevice,
+    error::Error,
     gb::{GameBoy, GameBoyMode},
     ppu::FRAME_BUFFER_SIZE,
 };
@@ -25,12 +26,16 @@ pub fn build_test(options: TestOptions) -> GameBoy {
     game_boy
 }
 
-pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -> GameBoy {
+pub fn run_test(
+    rom_path: &str,
+    max_cycles: Option<u64>,
+    options: TestOptions,
+) -> Result<GameBoy, Error> {
     let mut cycles = 0u64;
     let max_cycles = max_cycles.unwrap_or(u64::MAX);
 
     let mut game_boy = build_test(options);
-    game_boy.load_rom_file(rom_path, None);
+    game_boy.load_rom_file(rom_path, None)?;
 
     loop {
         cycles += game_boy.clock() as u64;
@@ -39,28 +44,32 @@ pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -
         }
     }
 
-    game_boy
+    Ok(game_boy)
 }
 
-pub fn run_step_test(rom_path: &str, addr: u16, options: TestOptions) -> GameBoy {
+pub fn run_step_test(rom_path: &str, addr: u16, options: TestOptions) -> Result<GameBoy, Error> {
     let mut game_boy = build_test(options);
-    game_boy.load_rom_file(rom_path, None);
+    game_boy.load_rom_file(rom_path, None)?;
     game_boy.step_to(addr);
-    game_boy
+    Ok(game_boy)
 }
 
-pub fn run_serial_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) -> String {
-    let mut game_boy = run_test(rom_path, max_cycles, options);
-    game_boy.serial().device().state()
+pub fn run_serial_test(
+    rom_path: &str,
+    max_cycles: Option<u64>,
+    options: TestOptions,
+) -> Result<String, Error> {
+    let mut game_boy = run_test(rom_path, max_cycles, options)?;
+    Ok(game_boy.serial().device().state())
 }
 
 pub fn run_image_test(
     rom_path: &str,
     max_cycles: Option<u64>,
     options: TestOptions,
-) -> [u8; FRAME_BUFFER_SIZE] {
-    let mut game_boy = run_test(rom_path, max_cycles, options);
-    *game_boy.frame_buffer()
+) -> Result<[u8; FRAME_BUFFER_SIZE], Error> {
+    let mut game_boy = run_test(rom_path, max_cycles, options)?;
+    Ok(*game_boy.frame_buffer())
 }
 
 #[cfg(test)]
@@ -75,7 +84,9 @@ mod tests {
             "res/roms/test/blargg/cpu/cpu_instrs.gb",
             0x0100,
             TestOptions::default(),
-        );
+        )
+        .unwrap();
+
         assert_eq!(result.cpu_i().pc(), 0x0100);
         assert_eq!(result.cpu_i().sp(), 0xfffe);
         assert_eq!(result.cpu_i().af(), 0x01b0);
@@ -96,7 +107,8 @@ mod tests {
             "res/roms/test/blargg/cpu/cpu_instrs.gb",
             Some(300000000),
             TestOptions::default(),
-        );
+        )
+        .unwrap();
         assert_eq!(result, "cpu_instrs\n\n01:ok  02:ok  03:ok  04:ok  05:ok  06:ok  07:ok  08:ok  09:ok  10:ok  11:ok  \n\nPassed all tests\n");
     }
 
@@ -106,7 +118,8 @@ mod tests {
             "res/roms/test/blargg/instr_timing/instr_timing.gb",
             Some(50000000),
             TestOptions::default(),
-        );
+        )
+        .unwrap();
         assert_eq!(result, "instr_timing\n\n\nPassed\n");
     }
 }