diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 470a41853895ef3fbaec66ebea29cb2a8e68490c..9c4a770dd2ca448d94c011b76cc3801404e9194f 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -48,6 +48,15 @@ typedef unsigned int size_t;
 #define API_DISP_RECT          0x16
 #define API_DISP_CIRC          0x17
 #define API_DISP_PIXEL         0x18
+
+#define API_FILE_OPEN          0x30
+#define API_FILE_CLOSE         0x31
+#define API_FILE_READ          0x32
+#define API_FILE_WRITE         0x34
+#define API_FILE_FLUSH         0x35
+#define API_FILE_SEEK          0x36 //NYI
+#define API_FILE_TELL          0x37 //NYI
+#define API_FILE_STAT          0x38
 /* clang-format on */
 
 typedef uint32_t api_int_id_t;
@@ -439,4 +448,68 @@ API(API_LIGHT_SENSOR_GET, int epic_light_sensor_get(uint16_t* value));
  */
 API(API_LIGHT_SENSOR_STOP, int epic_light_sensor_stop());
 
+/**
+ * File
+ * ====
+ * Except for :c:func:`epic_file_open`, which models C stdio's ``fopen``
+ * function, ``close``, ``read`` and ``write`` model `close(2)`_, `read(2)`_ and
+ * `write(2)`_.  All file-related functions return >= ``0`` on success and
+ * ``-Exyz`` on failure, with error codes from errno.h (``EIO``, ``EINVAL``
+ * etc.)
+ *
+ * .. _close(2): http://man7.org/linux/man-pages/man2/close.2.html
+ * .. _read(2): http://man7.org/linux/man-pages/man2/read.2.html
+ * .. _write(2): http://man7.org/linux/man-pages/man2/write.2.html
+ */
+
+/** */
+API(
+	API_FILE_OPEN,
+	int epic_file_open(const char* filename, const char* modeString)
+);
+
+/** */
+API(API_FILE_CLOSE, int epic_file_close(int fd));
+
+/** */
+API(API_FILE_READ, int epic_file_read(int fd, void* buf, size_t nbytes));
+
+/** */
+API(
+	API_FILE_WRITE,
+	int epic_file_write(int fd, const void* buf, size_t nbytes)
+);
+
+/** */
+API(API_FILE_FLUSH, int epic_file_flush(int fd));
+
+/** */
+enum epic_stat_type {
+	/** */
+	EPICSTAT_FILE,
+	/** */
+	EPICSTAT_DIR,
+};
+
+/** */
+typedef struct epic_stat_t {
+	/** */
+	enum epic_stat_type type;
+} epic_stat_t;
+
+/**
+ * stat path
+ *
+ * This does not follow posix convention, but rather takes
+ * a path as parameter. This aligns more with libff's API and
+ * also this has been implemented for python import support, which
+ * passes the filename as well.
+ *
+ * :param const char* filename: path to stat
+ * :param epic_stat_t* stat: pointer to result
+ *
+ * :return: `0` on success, negative on error
+ */
+API(API_FILE_STAT, int epic_file_stat(const char* path, epic_stat_t* stat));
+
 #endif /* _EPICARDIUM_H */
diff --git a/epicardium/modules/fatfs.c b/epicardium/modules/fatfs.c
index fc76bb5532640d8a2e3a2484e7880f20e15c49ef..649ba730374bbf2901c42373aa8f8b8575d903f8 100644
--- a/epicardium/modules/fatfs.c
+++ b/epicardium/modules/fatfs.c
@@ -2,24 +2,74 @@
  * support routines for FatFs
  */
 
-#include <stddef.h> //NULL
+#include <errno.h>
+#include <stddef.h>
 #include <stdio.h>
 #include <stdbool.h>
+#include <stdlib.h>
+
 #include <ff.h>
 
 #include <FreeRTOS.h>
 #include <semphr.h>
 
+#include "modules.h"
+#include "epicardium.h"
+
+#ifndef EPIC_FAT_STATIC_SEMAPHORE
+#define EPIC_FAT_STATIC_SEMAPHORE 0
+#endif
+
 static const TCHAR *rcstrings =
 	_T("OK\0DISK_ERR\0INT_ERR\0NOT_READY\0NO_FILE\0NO_PATH\0INVALID_NAME\0")
 	_T("DENIED\0EXIST\0INVALID_OBJECT\0WRITE_PROTECTED\0INVALID_DRIVE\0")
 	_T("NOT_ENABLED\0NO_FILESYSTEM\0MKFS_ABORTED\0TIMEOUT\0LOCKED\0")
 	_T("NOT_ENOUGH_CORE\0TOO_MANY_OPEN_FILES\0INVALID_PARAMETER\0");
 
+// this table converts from FRESULT to POSIX errno
+const int fresult_to_errno_table[20] = {
+	[FR_OK]                  = 0,
+	[FR_DISK_ERR]            = EIO,
+	[FR_INT_ERR]             = EIO,
+	[FR_NOT_READY]           = EBUSY,
+	[FR_NO_FILE]             = ENOENT,
+	[FR_NO_PATH]             = ENOENT,
+	[FR_INVALID_NAME]        = EINVAL,
+	[FR_DENIED]              = EACCES,
+	[FR_EXIST]               = EEXIST,
+	[FR_INVALID_OBJECT]      = EINVAL,
+	[FR_WRITE_PROTECTED]     = EROFS,
+	[FR_INVALID_DRIVE]       = ENODEV,
+	[FR_NOT_ENABLED]         = ENODEV,
+	[FR_NO_FILESYSTEM]       = ENODEV,
+	[FR_MKFS_ABORTED]        = EIO,
+	[FR_TIMEOUT]             = EIO,
+	[FR_LOCKED]              = EIO,
+	[FR_NOT_ENOUGH_CORE]     = ENOMEM,
+	[FR_TOO_MANY_OPEN_FILES] = EMFILE,
+	[FR_INVALID_PARAMETER]   = EINVAL,
+};
+
+enum FatObjectType { FO_Nil, FO_File, FO_Dir };
+struct FatObject {
+	enum FatObjectType type;
+	union {
+		FIL file;
+		DIR dir;
+	};
+};
+
 static bool mount(void);
+static int
+get_fat_object(int i, enum FatObjectType expected, struct FatObject **res);
 
 DIR dir;
 FATFS FatFs;
+static struct FatObject s_openedObjects[EPIC_FAT_MAX_OPENED];
+
+#if (EPIC_FAT_STATIC_SEMAPHORE == 1)
+StaticSemaphore_t xSemaphoreBuffer;
+#endif
 
 static volatile struct {
 	bool initiaized;
@@ -64,6 +114,7 @@ static bool mount()
 
 	return true;
 }
+
 /*------------------------------------------------------------------------*/
 /* Create a Synchronization Object */
 /*------------------------------------------------------------------------*/
@@ -79,7 +130,12 @@ static bool mount()
  */
 int ff_cre_syncobj(BYTE vol, FF_SYNC_t *sobj)
 {
+#if (EPIC_FAT_STATIC_SEMAPHORE == 1)
+	*sobj = xSemaphoreCreateMutexStatic(&xSemaphoreBuffer);
+#else
 	*sobj = xSemaphoreCreateMutex();
+#endif //EPIC_FAT_STATIC_SEMAPHORE
+
 	return (int)(*sobj != NULL);
 }
 
@@ -98,7 +154,6 @@ int ff_cre_syncobj(BYTE vol, FF_SYNC_t *sobj)
  */
 int ff_del_syncobj(FF_SYNC_t sobj)
 {
-	printf("%s\n", __PRETTY_FUNCTION__);
 	/* FreeRTOS */
 	vSemaphoreDelete(sobj);
 	return 1;
@@ -133,3 +188,158 @@ void ff_rel_grant(FF_SYNC_t sobj)
 	/* FreeRTOS */
 	xSemaphoreGive(sobj);
 }
+
+int get_fat_object(int i, enum FatObjectType expected, struct FatObject **res)
+{
+	if (i < 0 || i >= EPIC_FAT_MAX_OPENED) {
+		*res = NULL;
+		return EBADF;
+	}
+	if (s_openedObjects[i].type != expected) {
+		*res = NULL;
+		return EBADF;
+	}
+	*res = &s_openedObjects[i];
+	return 0;
+}
+
+int epic_file_open(const char *filename, const char *modeString)
+{
+	struct FatObject *o = NULL;
+	const char *mode_s  = modeString;
+	int i;
+	int mode = 0;
+
+	//find free object to use
+	for (i = 0; i < EPIC_FAT_MAX_OPENED; ++i) {
+		if (s_openedObjects[i].type == FO_Nil) {
+			break;
+		}
+	}
+	if (i == EPIC_FAT_MAX_OPENED) {
+		return -fresult_to_errno_table[FR_TOO_MANY_OPEN_FILES];
+	}
+	o = &s_openedObjects[i];
+
+	while (*mode_s) {
+		switch (*mode_s++) {
+		case 'r':
+			mode |= FA_READ;
+			break;
+		case 'w':
+			mode |= FA_WRITE | FA_CREATE_ALWAYS;
+			break;
+		case 'x':
+			mode |= FA_WRITE | FA_CREATE_NEW;
+			break;
+		case 'a':
+			mode |= FA_WRITE | FA_OPEN_ALWAYS;
+			break;
+		case '+':
+			mode |= FA_READ | FA_WRITE;
+			break;
+		}
+	}
+
+	int res = f_open(&o->file, filename, mode);
+	if (res != FR_OK) {
+		return -fresult_to_errno_table[res];
+	}
+	o->type = FO_File;
+
+	// for 'a' mode, we must begin at the end of the file
+	if ((mode & FA_OPEN_ALWAYS) != 0) {
+		f_lseek(&o->file, f_size(&o->file));
+	}
+
+	return i;
+}
+
+int epic_file_close(int fd)
+{
+	int res;
+	struct FatObject *o;
+	res = get_fat_object(fd, FO_File, &o);
+	if (res) {
+		return -res;
+	}
+
+	res = f_close(&o->file);
+	if (res != FR_OK) {
+		return -fresult_to_errno_table[res];
+	}
+
+	o->type = FO_Nil;
+	return 0;
+}
+
+int epic_file_read(int fd, void *buf, size_t nbytes)
+{
+	unsigned int nread = 0;
+
+	int res;
+	struct FatObject *o;
+	res = get_fat_object(fd, FO_File, &o);
+	if (res) {
+		return -res;
+	}
+
+	res = f_read(&o->file, buf, nbytes, &nread);
+	if (res != FR_OK) {
+		return -fresult_to_errno_table[res];
+	}
+
+	return nread;
+}
+
+int epic_file_write(int fd, const void *buf, size_t nbytes)
+{
+	unsigned int nwritten = 0;
+
+	int res;
+	struct FatObject *o;
+	res = get_fat_object(fd, FO_File, &o);
+	if (res) {
+		return -res;
+	}
+	res = f_write(&o->file, buf, nbytes, &nwritten);
+	if (res != FR_OK) {
+		return -fresult_to_errno_table[res];
+	}
+
+	return nwritten;
+}
+
+int epic_file_flush(int fd)
+{
+	int res;
+	struct FatObject *o;
+	res = get_fat_object(fd, FO_File, &o);
+	if (res) {
+		return -res;
+	}
+	res = f_sync(&o->file);
+	if (res != FR_OK) {
+		return -fresult_to_errno_table[res];
+	}
+
+	return 0;
+}
+
+int epic_file_stat(const char *filename, epic_stat_t *stat)
+{
+	int res;
+	FILINFO finfo;
+	res = f_stat(filename, &finfo);
+	if (res != FR_OK) {
+		return -fresult_to_errno_table[res];
+	}
+
+	if (finfo.fattrib & AM_DIR) {
+		stat->type = EPICSTAT_DIR;
+	} else {
+		stat->type = EPICSTAT_FILE;
+	}
+
+	return 0;
+}
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 214545f6029e5201db4f3518a1224bc8ad6f12bd..f02298e36b2e538237fc75bfe0710517c11e33ab 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -1,7 +1,10 @@
 #ifndef MODULES_H
 #define MODULES_H
 
-/* FatFS */
+/* ---------- FAT fs ------------------------------------------------------ */
+/* max no. of descriptors (file & directory) that can be open at a time */
+#define EPIC_FAT_MAX_OPENED 16
+#define EPIC_FAT_STATIC_SEMAPHORE 1
 void fatfs_init(void);
 
 /* ---------- Serial ------------------------------------------------------- */
diff --git a/pycardium/meson.build b/pycardium/meson.build
index 2a1f08939c5f69273186d28050f44fe1bbe3b8b1..cf5f5ba57ecdbdb4d321cd5d6bc08dfc151024ff 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -6,7 +6,9 @@ modsrc = files(
   'modules/sys_display.c',
   'modules/utime.c',
   'modules/vibra.c',
-  'modules/light_sensor.c'
+  'modules/light_sensor.c',
+  'modules/fat_file.c',
+  'modules/fat_reader_import.c',
 )
 
 #################################
diff --git a/pycardium/modules/fat_file.c b/pycardium/modules/fat_file.c
new file mode 100644
index 0000000000000000000000000000000000000000..e1d42f3efcc33c16f1425d23e061767d1eb6d5ad
--- /dev/null
+++ b/pycardium/modules/fat_file.c
@@ -0,0 +1,274 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013, 2014 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include "py/mpconfig.h"
+#include "py/runtime.h"
+#include "py/builtin.h"
+#include "py/stream.h"
+#include "py/mperrno.h"
+
+#include "epicardium.h"
+
+extern const mp_obj_type_t mp_type_fat_textio;
+#if MICROPY_PY_IO_FILEIO
+extern const mp_obj_type_t mp_type_fat_fileio;
+#endif
+
+typedef struct _pyb_file_obj_t {
+	mp_obj_base_t base;
+	int fd;
+} pyb_file_obj_t;
+
+STATIC void
+file_obj_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind)
+{
+	(void)kind;
+	mp_printf(
+		print,
+		"<io.%s %p>",
+		mp_obj_get_type_str(self_in),
+		MP_OBJ_TO_PTR(self_in)
+	);
+}
+
+STATIC mp_uint_t
+file_obj_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode)
+{
+	pyb_file_obj_t *self = MP_OBJ_TO_PTR(self_in);
+	int res              = epic_file_read(self->fd, buf, size);
+	if (res < 0) {
+		*errcode = -res;
+		return MP_STREAM_ERROR;
+	}
+	return res;
+}
+
+STATIC mp_uint_t
+file_obj_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode)
+{
+	pyb_file_obj_t *self = MP_OBJ_TO_PTR(self_in);
+	int res              = epic_file_write(self->fd, buf, size);
+	if (res < 0) {
+		*errcode = -res;
+		return MP_STREAM_ERROR;
+	}
+	return res;
+}
+
+STATIC mp_obj_t file_obj___exit__(size_t n_args, const mp_obj_t *args)
+{
+	(void)n_args;
+	return mp_stream_close(args[0]);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
+	file_obj___exit___obj, 4, 4, file_obj___exit__
+);
+
+STATIC mp_uint_t
+file_obj_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg, int *errcode)
+{
+	pyb_file_obj_t *self = MP_OBJ_TO_PTR(o_in);
+	int res;
+	switch (request) {
+	case MP_STREAM_FLUSH:
+		res = epic_file_flush(self->fd);
+		if (res < 0) {
+			*errcode = -res;
+			return MP_STREAM_ERROR;
+		}
+		return 0;
+	case MP_STREAM_CLOSE:
+		res = epic_file_close(self->fd);
+		if (res < 0) {
+			*errcode = -res;
+			return MP_STREAM_ERROR;
+		}
+		return 0;
+	}
+	//every valid case returns either success or error, so this is EINVAL land:
+	*errcode = MP_EINVAL;
+	return MP_STREAM_ERROR;
+	// if (request == MP_STREAM_SEEK) {
+	//     struct mp_stream_seek_t *s = (struct mp_stream_seek_t*)(uintptr_t)arg;
+
+	//     switch (s->whence) {
+	//         case 0: // SEEK_SET
+	//             f_lseek(&self->fp, s->offset);
+	//             break;
+
+	//         case 1: // SEEK_CUR
+	//             f_lseek(&self->fp, f_tell(&self->fp) + s->offset);
+	//             break;
+
+	//         case 2: // SEEK_END
+	//             f_lseek(&self->fp, f_size(&self->fp) + s->offset);
+	//             break;
+	//     }
+
+	//     s->offset = f_tell(&self->fp);
+	//     return 0;
+}
+
+// Note: encoding is ignored for now; it's also not a valid kwarg for CPython's FileIO,
+// but by adding it here we can use one single mp_arg_t array for open() and FileIO's constructor
+STATIC const mp_arg_t file_open_args[] = {
+	{ MP_QSTR_file,
+	  MP_ARG_OBJ | MP_ARG_REQUIRED,
+	  { .u_rom_obj = MP_ROM_PTR(&mp_const_none_obj) } },
+	{ MP_QSTR_mode, MP_ARG_OBJ, { .u_obj = MP_OBJ_NEW_QSTR(MP_QSTR_r) } },
+	{ MP_QSTR_encoding,
+	  MP_ARG_OBJ | MP_ARG_KW_ONLY,
+	  { .u_rom_obj = MP_ROM_PTR(&mp_const_none_obj) } },
+};
+#define FILE_OPEN_NUM_ARGS MP_ARRAY_SIZE(file_open_args)
+
+STATIC mp_obj_t file_open(const mp_obj_type_t *type, mp_arg_val_t *args)
+{
+	const char *modeString = mp_obj_str_get_str(args[1].u_obj);
+	const char *mode_s     = modeString;
+	// modes r w x a + are handled on epicardium side, binary / text
+	// is relevant for python type so look for these here
+	while (*mode_s) {
+		switch (*mode_s++) {
+#if MICROPY_PY_IO_FILEIO
+		case 'b':
+			type = &mp_type_fat_fileio;
+			break;
+#endif
+		case 't':
+			type = &mp_type_fat_textio;
+			break;
+		}
+	}
+
+	pyb_file_obj_t *o = m_new_obj_with_finaliser(pyb_file_obj_t);
+	o->base.type      = type;
+
+	const char *fname = mp_obj_str_get_str(args[0].u_obj);
+	int res           = epic_file_open(fname, modeString);
+	if (res < 0) {
+		m_del_obj(pyb_file_obj_t, o);
+		mp_raise_OSError(-res);
+	}
+	o->fd = res;
+
+	return MP_OBJ_FROM_PTR(o);
+}
+
+STATIC mp_obj_t file_obj_make_new(
+	const mp_obj_type_t *type,
+	size_t n_args,
+	size_t n_kw,
+	const mp_obj_t *args
+) {
+	mp_arg_val_t arg_vals[FILE_OPEN_NUM_ARGS];
+	mp_arg_parse_all_kw_array(
+		n_args,
+		n_kw,
+		args,
+		FILE_OPEN_NUM_ARGS,
+		file_open_args,
+		arg_vals
+	);
+	return file_open(type, arg_vals);
+}
+
+// TODO gc hook to close the file if not already closed
+
+STATIC const mp_rom_map_elem_t rawfile_locals_dict_table[] = {
+	{ MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_readline),
+	  MP_ROM_PTR(&mp_stream_unbuffered_readline_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_readlines),
+	  MP_ROM_PTR(&mp_stream_unbuffered_readlines_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_flush), MP_ROM_PTR(&mp_stream_flush_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&mp_stream_close_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_seek), MP_ROM_PTR(&mp_stream_seek_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_tell), MP_ROM_PTR(&mp_stream_tell_obj) },
+	{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mp_stream_close_obj) },
+	{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&mp_identity_obj) },
+	{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&file_obj___exit___obj) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(rawfile_locals_dict, rawfile_locals_dict_table);
+
+#if MICROPY_PY_IO_FILEIO
+STATIC const mp_stream_p_t fileio_stream_p = {
+	.read  = file_obj_read,
+	.write = file_obj_write,
+	.ioctl = file_obj_ioctl,
+};
+
+const mp_obj_type_t mp_type_fat_fileio = {
+	{ &mp_type_type },
+	.name        = MP_QSTR_FileIO,
+	.print       = file_obj_print,
+	.make_new    = file_obj_make_new,
+	.getiter     = mp_identity_getiter,
+	.iternext    = mp_stream_unbuffered_iter,
+	.protocol    = &fileio_stream_p,
+	.locals_dict = (mp_obj_dict_t *)&rawfile_locals_dict,
+};
+#endif
+
+STATIC const mp_stream_p_t textio_stream_p = {
+	.read    = file_obj_read,
+	.write   = file_obj_write,
+	.ioctl   = file_obj_ioctl,
+	.is_text = true,
+};
+
+const mp_obj_type_t mp_type_fat_textio = {
+	{ &mp_type_type },
+	.name        = MP_QSTR_TextIOWrapper,
+	.print       = file_obj_print,
+	.make_new    = file_obj_make_new,
+	.getiter     = mp_identity_getiter,
+	.iternext    = mp_stream_unbuffered_iter,
+	.protocol    = &textio_stream_p,
+	.locals_dict = (mp_obj_dict_t *)&rawfile_locals_dict,
+};
+
+// Factory function for I/O stream classes
+mp_obj_t mp_builtin_open(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs)
+{
+	// TODO: analyze buffering args and instantiate appropriate type
+	mp_arg_val_t arg_vals[FILE_OPEN_NUM_ARGS];
+	mp_arg_parse_all(
+		n_args,
+		args,
+		kwargs,
+		FILE_OPEN_NUM_ARGS,
+		file_open_args,
+		arg_vals
+	);
+	return file_open(&mp_type_fat_textio, arg_vals);
+}
+MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_open_obj, 1, mp_builtin_open);
diff --git a/pycardium/modules/fat_reader_import.c b/pycardium/modules/fat_reader_import.c
new file mode 100644
index 0000000000000000000000000000000000000000..bb35aec22ff8cd66c11c02924b8ce3eeb944b430
--- /dev/null
+++ b/pycardium/modules/fat_reader_import.c
@@ -0,0 +1,83 @@
+#include "epicardium.h"
+
+#include <py/runtime.h>
+#include <py/reader.h>
+#include <py/lexer.h>
+
+/** ported from picropython's posic implementation */
+
+typedef struct _mp_reader_epicfat_t {
+	bool close_fd;
+	int fd;
+	size_t len;
+	size_t pos;
+	byte buf[20];
+} mp_reader_epicfat_t;
+
+STATIC mp_uint_t mp_reader_epicfat_readbyte(void *data)
+{
+	mp_reader_epicfat_t *reader = (mp_reader_epicfat_t *)data;
+	if (reader->pos >= reader->len) {
+		if (reader->len == 0) {
+			return MP_READER_EOF;
+		} else {
+			int n = epic_file_read(
+				reader->fd, reader->buf, sizeof(reader->buf)
+			);
+			if (n <= 0) {
+				reader->len = 0;
+				return MP_READER_EOF;
+			}
+			reader->len = n;
+			reader->pos = 0;
+		}
+	}
+	return reader->buf[reader->pos++];
+}
+
+STATIC void mp_reader_epicfat_close(void *data)
+{
+	mp_reader_epicfat_t *reader = (mp_reader_epicfat_t *)data;
+	epic_file_close(reader->fd);
+	m_del_obj(mp_reader_epicfat_t, reader);
+}
+
+void mp_reader_new_file(mp_reader_t *reader, const char *filename)
+{
+	int fd = epic_file_open(filename, "r");
+	if (fd < 0) {
+		mp_raise_OSError(-fd);
+	}
+	mp_reader_epicfat_t *rp = m_new_obj(mp_reader_epicfat_t);
+	rp->fd                  = fd;
+	int n                   = epic_file_read(rp->fd, rp->buf, sizeof(rp->buf));
+	if (n < 0) {
+		epic_file_close(fd);
+	}
+	rp->len          = n;
+	rp->pos          = 0;
+	reader->data     = rp;
+	reader->readbyte = mp_reader_epicfat_readbyte;
+	reader->close    = mp_reader_epicfat_close;
+}
+
+mp_lexer_t *mp_lexer_new_from_file(const char *filename)
+{
+	mp_reader_t reader;
+	mp_reader_new_file(&reader, filename);
+	return mp_lexer_new(qstr_from_str(filename), reader);
+}
+
+mp_import_stat_t mp_import_stat(const char *path)
+{
+	struct epic_stat_t stat;
+
+	if (epic_file_stat(path, &stat) == 0) {
+		if (stat.type == EPICSTAT_FILE) {
+			return MP_IMPORT_STAT_FILE;
+		} else {
+			return MP_IMPORT_STAT_DIR;
+		}
+	}
+	return MP_IMPORT_STAT_NO_EXIST;
+}
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index 3dda54d667557f66d39978788fbde813f8e39390..e8ac9dac50a12c6a1566c07626050358940587fc 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -46,3 +46,24 @@ Q(light_sensor)
 Q(start)
 Q(get_reading)
 Q(stop)
+
+/* file */
+Q(__del__)
+Q(__enter__)
+Q(__exit__)
+Q(close)
+Q(encoding)
+Q(file)
+Q(FileIO)
+Q(flush)
+Q(mode)
+Q(r)
+Q(read)
+Q(readinto)
+Q(readline)
+Q(readlines)
+Q(seek)
+Q(tell)
+Q(TextIOWrapper)
+Q(write)
+
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index 6bfb2c6717fcddb0bfcd585fb13c401b5df86bb9..9bed1f600fd79535f4f53d6792f0ded869cf99d0 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -76,3 +76,4 @@ typedef long mp_off_t;
 /* For some reason, we need to define readline history manually */
 #define MICROPY_PORT_ROOT_POINTERS \
     const char *readline_hist[16];
+
diff --git a/pycardium/mphalport.c b/pycardium/mphalport.c
index 9e0402779651f13808726eef4b62a1d6d97ed064..b97c1dd4130734ce2e3145dead584764f7d921f4 100644
--- a/pycardium/mphalport.c
+++ b/pycardium/mphalport.c
@@ -135,26 +135,3 @@ void NORETURN nlr_jump_fail(void *val)
 
 	Reset_Handler();
 }
-
-/******************************************************************************
- * Stubs
- */
-
-mp_lexer_t *mp_lexer_new_from_file(const char *filename)
-{
-	/* TODO: Do we need an implementation for this? */
-	mp_raise_OSError(MP_ENOENT);
-}
-
-mp_import_stat_t mp_import_stat(const char *path)
-{
-	return MP_IMPORT_STAT_NO_EXIST;
-}
-
-mp_obj_t mp_builtin_open(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs)
-{
-	/* TODO: Once fs is implemented, get this working as well */
-	mp_raise_NotImplementedError("FS is not yet implemented");
-	return mp_const_none;
-}
-MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_open_obj, 1, mp_builtin_open);