diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/bindings/python/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
new file mode 100644
index 0000000..3f7ee5f
--- /dev/null
+++ b/bindings/python/Makefile.am
@@ -0,0 +1,40 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+pyexec_LTLIBRARIES = gpiod.la
+
+gpiod_la_SOURCES = \
+	chip.c \
+	chip-info.c \
+	edge-event.c \
+	edge-event-buffer.c \
+	exception.c \
+	info-event.c \
+	line.c \
+	line-config.c \
+	line-info.c \
+	line-request.c \
+	module.c \
+	module.h \
+	request-config.c
+
+gpiod_la_CFLAGS = -I$(top_srcdir)/include/
+gpiod_la_CFLAGS += -Wall -Wextra -g -std=gnu89 $(PYTHON_CPPFLAGS)
+gpiod_la_CFLAGS += -include $(top_builddir)/config.h
+gpiod_la_LDFLAGS = -module -avoid-version
+gpiod_la_LIBADD = $(top_builddir)/lib/libgpiod.la $(PYTHON_LIBS)
+gpiod_la_LIBADD += $(top_builddir)/bindings/python/enum/libpycenum.la
+
+SUBDIRS = enum .
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
+
+if WITH_EXAMPLES
+
+SUBDIRS += examples
+
+endif
diff --git a/bindings/python/chip-info.c b/bindings/python/chip-info.c
new file mode 100644
index 0000000..e48cf74
--- /dev/null
+++ b/bindings/python/chip-info.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_chip_info *info;
+} chip_info_object;
+
+static int chip_info_init(PyObject *Py_UNUSED(self),
+			  PyObject *Py_UNUSED(ignored0),
+			  PyObject *Py_UNUSED(ignored1))
+{
+	PyErr_SetString(PyExc_TypeError,
+			"cannot create 'gpiod.ChipInfo' instances");
+	return -1;
+}
+
+static void chip_info_finalize(chip_info_object *self)
+{
+	if (self->info)
+		gpiod_chip_info_free(self->info);
+}
+
+PyDoc_STRVAR(chip_info_name_doc,
+"Name of the chip as represented in the kernel.");
+
+static PyObject *chip_info_name(chip_info_object *self,
+				void *Py_UNUSED(ignored))
+{
+	return PyUnicode_FromString(gpiod_chip_info_get_name(self->info));
+}
+
+PyDoc_STRVAR(chip_info_label_doc,
+"Label of the chip as represented in the kernel.");
+
+static PyObject *chip_info_label(chip_info_object *self,
+				 void *Py_UNUSED(ignored))
+{
+	return PyUnicode_FromString(gpiod_chip_info_get_label(self->info));
+}
+
+PyDoc_STRVAR(chip_info_num_lines_doc,
+"Number of GPIO lines exposed by the chip.");
+
+static PyObject *chip_info_num_lines(chip_info_object *self,
+				     void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLong(
+			gpiod_chip_info_get_num_lines(self->info));
+}
+
+static PyGetSetDef chip_info_getset[] = {
+	{
+		.name = "name",
+		.get = (getter)chip_info_name,
+		.doc = chip_info_name_doc,
+	},
+	{
+		.name = "label",
+		.get = (getter)chip_info_label,
+		.doc = chip_info_label_doc,
+	},
+	{
+		.name = "num_lines",
+		.get = (getter)chip_info_num_lines,
+		.doc = chip_info_num_lines_doc
+	},
+	{ }
+};
+
+static PyObject *chip_info_str(PyObject *self)
+{
+	PyObject *name, *label, *num_lines, *str = NULL;
+
+	name = PyObject_GetAttrString(self, "name");
+	label = PyObject_GetAttrString(self, "label");
+	num_lines = PyObject_GetAttrString(self, "num_lines");
+	if (!name || !label || !num_lines)
+		goto out;
+
+	str = PyUnicode_FromFormat("<gpiod.ChipInfo name=\"%S\" label=\"%S\" num_lines=%S>",
+				   name, label, num_lines);
+
+out:
+	Py_XDECREF(name);
+	Py_XDECREF(label);
+	Py_XDECREF(num_lines);
+	return str;
+}
+
+PyDoc_STRVAR(chip_info_type_doc,
+"Chip info object contains an immutable snapshot of a chip's status.");
+
+static PyTypeObject chip_info_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.ChipInfo",
+	.tp_basicsize = sizeof(chip_info_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = chip_info_type_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)chip_info_init,
+	.tp_finalize = (destructor)chip_info_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = chip_info_getset,
+	.tp_str = (reprfunc)chip_info_str
+};
+
+int Py_gpiod_RegisterChipInfoType(PyObject *module)
+{
+	return PyModule_AddType(module, &chip_info_type);
+}
+
+PyObject *Py_gpiod_MakeChipInfo(struct gpiod_chip_info *info)
+{
+	chip_info_object *info_obj;
+
+	info_obj = PyObject_New(chip_info_object, &chip_info_type);
+	if (!info_obj)
+		return NULL;
+
+	info_obj->info = info;
+
+	return (PyObject *)info_obj;
+}
diff --git a/bindings/python/chip.c b/bindings/python/chip.c
new file mode 100644
index 0000000..47b8e1c
--- /dev/null
+++ b/bindings/python/chip.c
@@ -0,0 +1,552 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <stdint.h>
+
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_chip *chip;
+} chip_object;
+
+static int chip_init(chip_object *self, PyObject *args,
+		     PyObject *Py_UNUSED(ignored))
+{
+	char *path;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "s", &path);
+	if (!ret)
+		return -1;
+
+	Py_BEGIN_ALLOW_THREADS;
+	self->chip = gpiod_chip_open(path);
+	Py_END_ALLOW_THREADS;
+	if (!self->chip) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	return 0;
+}
+
+static bool chip_is_closed(chip_object *self)
+{
+	return !self->chip;
+}
+
+static bool chip_check_closed(chip_object *self)
+{
+	if (chip_is_closed(self)) {
+		Py_gpiod_SetChipClosedError();
+		return true;
+	}
+
+	return false;
+}
+
+static void chip_finalize(chip_object *self)
+{
+	if (!chip_is_closed(self))
+		PyObject_CallMethod((PyObject *)self, "close", "");
+}
+
+PyDoc_STRVAR(chip_path_doc,
+"Path to the file passed as argument to the constructor.");
+
+static PyObject *chip_path(chip_object *self, void *Py_UNUSED(ignored))
+{
+	if (chip_check_closed(self))
+		return NULL;
+
+	return PyUnicode_FromString(gpiod_chip_get_path(self->chip));
+}
+
+PyDoc_STRVAR(chip_fd_doc,
+"Number of the file descriptor associated with this chip.");
+
+static PyObject *chip_fd(chip_object *self, void *Py_UNUSED(ignored))
+{
+	if (chip_check_closed(self))
+		return NULL;
+
+	return PyLong_FromLong(gpiod_chip_get_fd(self->chip));
+}
+
+static PyGetSetDef chip_getset[] = {
+	{
+		.name = "path",
+		.get = (getter)chip_path,
+		.doc = chip_path_doc,
+	},
+	{
+		.name = "fd",
+		.get = (getter)chip_fd,
+		.doc = chip_fd_doc
+	},
+	{ }
+};
+
+PyDoc_STRVAR(chip_enter_doc, "Controlled execution enter callback.");
+
+static PyObject *chip_enter(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+	if (PyObject_Not(self)) {
+		Py_gpiod_SetChipClosedError();
+		return NULL;
+	}
+
+	Py_INCREF(self);
+	return self;
+}
+
+PyDoc_STRVAR(chip_exit_doc, "Controlled execution exit callback.");
+
+static PyObject *chip_exit(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+	return PyObject_CallMethod(self, "close", "");
+}
+
+PyDoc_STRVAR(chip_close_doc,
+"close() -> None\n"
+"\n"
+"Close the associated GPIO chip descriptor. The chip object must no longer\n"
+"be used after this method is called.");
+
+static PyObject *chip_close(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+	if (chip_check_closed(self))
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	gpiod_chip_close(self->chip);
+	Py_END_ALLOW_THREADS;
+	self->chip = NULL;
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(chip_get_info_doc,
+"get_info() -> gpiod.ChipInfo\n"
+"\n"
+"Get the information about the chip.");
+
+static PyObject *chip_get_info(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+	struct gpiod_chip_info *info;
+	PyObject *info_obj;
+
+	if (chip_check_closed(self))
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	info = gpiod_chip_get_info(self->chip);
+	Py_END_ALLOW_THREADS;
+	if (!info)
+		return Py_gpiod_SetErrFromErrno();
+
+	info_obj = Py_gpiod_MakeChipInfo(info);
+	if (!info_obj) {
+		gpiod_chip_info_free(info);
+		return NULL;
+	}
+
+	return info_obj;
+}
+
+static PyObject *
+do_chip_get_line_info(chip_object *self, PyObject *args, bool watch)
+{
+	struct gpiod_line_info *info;
+	unsigned int offset;
+	PyObject *info_obj;
+	int ret;
+
+	if (chip_check_closed(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "I", &offset);
+	if (!ret)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	if (watch)
+		info = gpiod_chip_watch_line_info(self->chip, offset);
+	else
+		info = gpiod_chip_get_line_info(self->chip, offset);
+	Py_END_ALLOW_THREADS;
+	if (!info)
+		return Py_gpiod_SetErrFromErrno();
+
+	info_obj = Py_gpiod_MakeLineInfo(info);
+	if (!info_obj)
+		gpiod_line_info_free(info);
+
+	return info_obj;
+}
+
+PyDoc_STRVAR(chip_get_line_info_doc,
+"get_line_info(offset) -> gpiod.LineInfo\n"
+"\n"
+"Get the snapshot of information about the line at given offset.\n"
+"\n"
+"  offset\n"
+"    Offset of the GPIO line to get information for");
+
+static PyObject *chip_get_line_info(chip_object *self, PyObject *args)
+{
+	return do_chip_get_line_info(self, args, false);
+}
+
+PyDoc_STRVAR(chip_watch_line_info_doc,
+"watch_line_info(offset) -> gpiod.LineInfo\n"
+"\n"
+"Get the snapshot of information about the line at given offset and start\n"
+"watching it for future changes.\n"
+"\n"
+"  offset\n"
+"    Offset of the GPIO line to get information for");
+
+static PyObject *chip_watch_line_info(chip_object *self, PyObject *args)
+{
+	return do_chip_get_line_info(self, args, true);
+}
+
+PyDoc_STRVAR(chip_unwatch_line_info_doc,
+"unwatch_line_info(offset) -> None\n"
+"\n"
+"Stop watching a line for status changes.\n"
+"\n"
+"  offset\n"
+"    Offset of the line to stop watching");
+
+static PyObject *chip_unwatch_line_info(chip_object *self, PyObject *args)
+{
+	unsigned int offset;
+	int ret;
+
+	if (chip_check_closed(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "I", &offset);
+	if (!ret)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_chip_unwatch_line_info(self->chip, offset);
+	Py_END_ALLOW_THREADS;
+	if (ret)
+		return Py_gpiod_SetErrFromErrno();
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(chip_wait_info_event_doc,
+"wait_info_event(timeout) -> Bool\n"
+"\n"
+"Wait for line status change events on any of the watched lines on the chip.\n"
+"\n"
+"  timeout\n"
+"    Wait time limit stored represented as a datetime.timedelta object");
+
+static PyObject *chip_wait_info_event(chip_object *self, PyObject *args)
+{
+	uint64_t timeout_us, timeout_ns;
+	PyObject *timedelta;
+	int ret;
+
+	if (chip_check_closed(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "O", &timedelta);
+	if (!ret)
+		return NULL;
+
+	timeout_us = Py_gpiod_TimedeltaToMicroseconds(timedelta);
+	if (PyErr_Occurred())
+		return NULL;
+
+	timeout_ns = timeout_us * 1000;
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_chip_wait_info_event(self->chip, timeout_ns);
+	Py_END_ALLOW_THREADS;
+	if (ret < 0)
+		return Py_gpiod_SetErrFromErrno();
+
+	return PyBool_FromLong(ret);
+}
+
+PyDoc_STRVAR(chip_read_info_event_doc,
+"read_info_event() -> gpiod.InfoEvent\n"
+"\n"
+"Read a single line status change event from the chip.");
+
+static PyObject *
+chip_read_info_event(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+	struct gpiod_info_event *event;
+	PyObject *event_obj;
+
+	if (chip_check_closed(self))
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	event = gpiod_chip_read_info_event(self->chip);
+	Py_END_ALLOW_THREADS;
+	if (!event)
+		return Py_gpiod_SetErrFromErrno();
+
+	event_obj = Py_gpiod_MakeInfoEvent(event);
+	if (!event_obj)
+		gpiod_info_event_free(event);
+
+	return event_obj;
+}
+
+PyDoc_STRVAR(chip_get_line_offset_from_name_doc,
+"find_line(name) -> int\n"
+"\n"
+"Map a line's name to its offset within the chip."
+"\n"
+"  name\n"
+"    Name of the GPIO line to map");
+
+static PyObject *
+chip_get_line_offset_from_name(chip_object *self, PyObject *args)
+{
+	const char *name;
+	int ret, offset;
+
+	if (chip_check_closed(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "s", &name);
+	if (!ret)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	offset = gpiod_chip_get_line_offset_from_name(self->chip, name);
+	Py_END_ALLOW_THREADS;
+	if (offset < 0) {
+		if (errno == ENOENT)
+			Py_RETURN_NONE;
+
+		return Py_gpiod_SetErrFromErrno();
+	}
+
+	return PyLong_FromUnsignedLong(offset);
+}
+
+PyDoc_STRVAR(chip_request_lines_doc,
+"request_lines(req_cfg, line_cfg) -> gpiod.LineRequest\n"
+"\n"
+"Request a set of lines for exclusive usage.\n"
+"\n"
+"  req_cfg\n"
+"    Request config object\n"
+"  line_cfg\n"
+"    Line config object");
+
+static PyObject *chip_request_lines(chip_object *self, PyObject *args)
+{
+	PyObject *req_cfg_obj, *line_cfg_obj, *req_obj;
+	struct gpiod_request_config *req_cfg;
+	struct gpiod_line_config *line_cfg;
+	struct gpiod_line_request *req;
+	int ret;
+
+	if (chip_check_closed(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "OO", &req_cfg_obj, &line_cfg_obj);
+	if (!ret)
+		return NULL;
+
+	req_cfg = Py_gpiod_RequestConfigGetData(req_cfg_obj);
+	if (!req_cfg)
+		return NULL;
+
+	line_cfg = Py_gpiod_LineConfigGetData(line_cfg_obj);
+	if (!line_cfg)
+		return NULL;
+
+	req = gpiod_chip_request_lines(self->chip, req_cfg, line_cfg);
+	if (!req) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	req_obj = Py_gpiod_MakeLineRequest(req);
+	if (!req_obj) {
+		gpiod_line_request_release(req);
+		return NULL;
+	}
+
+	return req_obj;
+}
+
+static PyMethodDef chip_methods[] = {
+	{
+		.ml_name = "__enter__",
+		.ml_meth = (PyCFunction)chip_enter,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = chip_enter_doc,
+	},
+	{
+		.ml_name = "__exit__",
+		.ml_meth = (PyCFunction)chip_exit,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = chip_exit_doc,
+	},
+	{
+		.ml_name = "close",
+		.ml_meth = (PyCFunction)chip_close,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = chip_close_doc,
+	},
+	{
+		.ml_name = "get_info",
+		.ml_meth = (PyCFunction)chip_get_info,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = chip_get_info_doc,
+	},
+	{
+		.ml_name = "get_line_info",
+		.ml_meth = (PyCFunction)chip_get_line_info,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = chip_get_line_info_doc,
+	},
+	{
+		.ml_name = "watch_line_info",
+		.ml_meth = (PyCFunction)chip_watch_line_info,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = chip_watch_line_info_doc,
+	},
+	{
+		.ml_name = "unwatch_line_info",
+		.ml_meth = (PyCFunction)chip_unwatch_line_info,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = chip_unwatch_line_info_doc,
+	},
+	{
+		.ml_name = "wait_info_event",
+		.ml_meth = (PyCFunction)chip_wait_info_event,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = chip_wait_info_event_doc,
+	},
+	{
+		.ml_name = "read_info_event",
+		.ml_meth = (PyCFunction)chip_read_info_event,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = chip_read_info_event_doc,
+	},
+	{
+		.ml_name = "get_line_offset_from_name",
+		.ml_meth = (PyCFunction)chip_get_line_offset_from_name,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = chip_get_line_offset_from_name_doc,
+	},
+	{
+		.ml_name = "request_lines",
+		.ml_meth = (PyCFunction)chip_request_lines,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = chip_request_lines_doc,
+	},
+	{ }
+};
+
+static PyObject *chip_str_closed(void)
+{
+	return PyUnicode_FromString("<gpiod.Chip CLOSED>");
+}
+
+static PyObject *chip_repr(chip_object *self)
+{
+	if (chip_is_closed(self))
+		return chip_str_closed();
+
+	return PyUnicode_FromFormat("gpiod.Chip(\"%s\")",
+				    gpiod_chip_get_path(self->chip));
+}
+
+static PyObject *chip_str(PyObject *self)
+{
+	PyObject *path, *fd, *info, *str = NULL;
+
+	if (PyObject_Not(self))
+		return chip_str_closed();
+
+	path = PyObject_GetAttrString(self, "path");
+	fd = PyObject_GetAttrString(self, "fd");
+	info = PyObject_CallMethod(self, "get_info", NULL);
+	if (!path || !fd || !info)
+		goto out;
+
+	str = PyUnicode_FromFormat("<gpiod.Chip path=\"%S\" fd=%S info=%S>",
+				   path, fd, info);
+
+out:
+	Py_XDECREF(path);
+	Py_XDECREF(fd);
+	Py_XDECREF(info);
+	return str;
+}
+
+static int chip_bool(chip_object *self)
+{
+	return !chip_is_closed(self);
+}
+
+static PyNumberMethods chip_number_methods = {
+	.nb_bool = (inquiry)chip_bool,
+};
+
+PyDoc_STRVAR(chip_type_doc,
+"Represents a GPIO chip.\n"
+"\n"
+"Chip object manages all resources associated with the GPIO chip\n"
+"it represents.\n"
+"\n"
+"The gpiochip device file is opened during the object's construction.\n"
+"The Chip object's constructor takes the path to the GPIO chip device file\n"
+"as the only argument.\n"
+"\n"
+"Callers must close the chip by calling the close() method when it's no\n"
+"longer used.\n"
+"\n"
+"Example:\n"
+"\n"
+"    chip = gpiod.Chip(\"/dev/gpiochip0\")\n"
+"    do_something(chip)\n"
+"    chip.close()\n"
+"\n"
+"The gpiod.Chip class also supports controlled execution ('with' statement).\n"
+"\n"
+"Example:\n"
+"\n"
+"    with gpiod.Chip(\"/dev/gpiochip0\") as chip:\n"
+"        do_something(chip)");
+
+static PyTypeObject chip_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.Chip",
+	.tp_basicsize = sizeof(chip_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = chip_type_doc,
+	.tp_as_number = &chip_number_methods,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)chip_init,
+	.tp_finalize = (destructor)chip_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = chip_getset,
+	.tp_methods = chip_methods,
+	.tp_repr = (reprfunc)chip_repr,
+	.tp_str = (reprfunc)chip_str
+};
+
+int Py_gpiod_RegisterChipType(PyObject *module)
+{
+	return PyModule_AddType(module, &chip_type);
+}
diff --git a/bindings/python/edge-event-buffer.c b/bindings/python/edge-event-buffer.c
new file mode 100644
index 0000000..5dba3ec
--- /dev/null
+++ b/bindings/python/edge-event-buffer.c
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_edge_event_buffer *buf;
+	Py_ssize_t seq;
+} edge_event_buffer_object;
+
+static int edge_event_buffer_init(edge_event_buffer_object *self,
+				  PyObject *args, PyObject *Py_UNUSED(ignored))
+{
+	Py_ssize_t capacity = 64;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "|n", &capacity);
+	if (!ret)
+		return -1;
+
+	self->buf = gpiod_edge_event_buffer_new(capacity);
+	if (!self->buf) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	self->seq = -1;
+
+	return 0;
+}
+
+static void edge_event_buffer_finalize(edge_event_buffer_object *self)
+{
+	if (self->buf)
+		gpiod_edge_event_buffer_free(self->buf);
+}
+
+PyDoc_STRVAR(edge_event_buffer_capacity_doc, "Maximum capacity of the buffer.");
+
+static PyObject *edge_event_buffer_capacity(edge_event_buffer_object *self,
+					    void *Py_UNUSED(ignored))
+{
+	return PyLong_FromSize_t(
+			gpiod_edge_event_buffer_get_capacity(self->buf));
+}
+
+PyDoc_STRVAR(edge_event_buffer_num_events_doc,
+"Number of events a buffer has stored.");
+
+static PyObject *edge_event_buffer_num_events(edge_event_buffer_object *self,
+					      void *Py_UNUSED(ignored))
+{
+	return PyLong_FromSize_t(
+			gpiod_edge_event_buffer_get_num_events(self->buf));
+}
+
+static PyGetSetDef edge_event_buffer_getset[] = {
+	{
+		.name = "capacity",
+		.get = (getter)edge_event_buffer_capacity,
+		.doc = edge_event_buffer_capacity_doc,
+	},
+	{
+		.name = "num_events",
+		.get = (getter)edge_event_buffer_num_events,
+		.doc = edge_event_buffer_num_events_doc,
+	},
+	{ }
+};
+
+PyDoc_STRVAR(edge_event_buffer_get_event_doc,
+"get_event(index) -> gpiod.EdgeEvent\n"
+"\n"
+"Get an event stored in the buffer.\n"
+"\n"
+"  index\n"
+"    Index of the event in the buffer.");
+
+static PyObject *
+do_get_event(struct gpiod_edge_event_buffer *buf, unsigned long index)
+{
+	struct gpiod_edge_event *event, *cpy;
+	PyObject *event_obj;
+
+	event = gpiod_edge_event_buffer_get_event(buf, index);
+	if (!event) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	cpy = gpiod_edge_event_copy(event);
+	if (!cpy) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	event_obj = Py_gpiod_MakeEdgeEvent(cpy);
+	if (!event_obj) {
+		gpiod_edge_event_free(cpy);
+		return NULL;
+	}
+
+	return event_obj;
+}
+
+static PyObject *
+edge_event_buffer_get_event(edge_event_buffer_object *self, PyObject *args)
+{
+	unsigned long index;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "k", &index);
+	if (!ret)
+		return NULL;
+
+	return do_get_event(self->buf, index);
+}
+
+static PyMethodDef edge_event_buffer_methods[] = {
+	{
+		.ml_name = "get_event",
+		.ml_meth = (PyCFunction)edge_event_buffer_get_event,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = edge_event_buffer_get_event_doc,
+	},
+	{ }
+};
+
+static PyObject *edge_event_buffer_repr(PyObject *self)
+{
+	PyObject *capacity, *repr;
+
+	capacity = PyObject_GetAttrString(self, "capacity");
+	if (!capacity)
+		return NULL;
+
+	repr = PyUnicode_FromFormat("gpiod.EdgeEventBuffer(%S)", capacity);
+	Py_DECREF(capacity);
+	return repr;
+}
+
+static PyObject *events_str(edge_event_buffer_object *self)
+{
+	PyObject *iter, *next, *list, *str, *joined;
+	size_t num_events;
+	Py_ssize_t i;
+	int ret;
+
+	num_events = gpiod_edge_event_buffer_get_num_events(self->buf);
+
+	list = PyList_New(num_events);
+	if (!list)
+		return NULL;
+
+	iter = PyObject_GetIter((PyObject *)self);
+	if (!iter) {
+		Py_DECREF(list);
+		return NULL;
+	}
+
+	for (i = 0;; i++) {
+		next = PyIter_Next(iter);
+		if (!next) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		str = PyObject_Str(next);
+		Py_DECREF(next);
+		if (!str) {
+			Py_DECREF(iter);
+			Py_DECREF(list);
+			return NULL;
+		}
+
+		ret = PyList_SetItem(list, i, str);
+		if (ret) {
+			Py_DECREF(str);
+			Py_DECREF(iter);
+			Py_DECREF(list);
+			return NULL;
+		}
+	}
+
+	str = PyUnicode_FromString(", ");
+	if (!str) {
+		Py_DECREF(list);
+		return NULL;
+	}
+
+	joined = PyObject_CallMethod(str, "join", "O", list);
+	Py_DECREF(list);
+	return joined;
+}
+
+static PyObject *edge_event_buffer_str(PyObject *self)
+{
+	PyObject *events, *capacity, *num_events, *str = NULL;
+
+	capacity = PyObject_GetAttrString(self, "capacity");
+	num_events = PyObject_GetAttrString(self, "num_events");
+	events = events_str((edge_event_buffer_object *)self);
+	if (!capacity || !num_events || !events)
+		goto out;
+
+	str = PyUnicode_FromFormat(
+		"<gpiod.EdgeEventBuffer capacity=%S num_events=%S events=[%S]>",
+		capacity, num_events, events);
+
+out:
+	Py_XDECREF(capacity);
+	Py_XDECREF(num_events);
+	Py_XDECREF(events);
+	return str;
+}
+
+static Py_ssize_t edge_event_buffer_length(edge_event_buffer_object *self)
+{
+	return gpiod_edge_event_buffer_get_num_events(self->buf);
+}
+
+static PyObject *edge_event_buffer_item(PyObject *self, Py_ssize_t index)
+{
+	return PyObject_CallMethod(self, "get_event", "n", index);
+}
+
+static PySequenceMethods edge_event_buffer_sequence_methods = {
+	.sq_length = (lenfunc)edge_event_buffer_length,
+	.sq_item = (ssizeargfunc)edge_event_buffer_item,
+};
+
+static PyObject *edge_event_buffer_iternext(edge_event_buffer_object *self)
+{
+	PyObject *event;
+
+	if (self->seq < 0)
+		self->seq = 0;
+
+	if ((size_t)self->seq == gpiod_edge_event_buffer_get_num_events(self->buf)) {
+		self->seq = -1;
+		return NULL;
+	}
+
+	event = do_get_event(self->buf, self->seq);
+	if (!event)
+		return NULL;
+
+	self->seq++;
+
+	return event;
+}
+
+PyDoc_STRVAR(edge_event_buffer_type_doc,
+"Object into which edge events are read.");
+
+static PyTypeObject edge_event_buffer_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.EdgeEventBuffer",
+	.tp_basicsize = sizeof(edge_event_buffer_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = edge_event_buffer_type_doc,
+	.tp_as_sequence = &edge_event_buffer_sequence_methods,
+	.tp_iter = PyObject_SelfIter,
+	.tp_iternext = (iternextfunc)edge_event_buffer_iternext,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)edge_event_buffer_init,
+	.tp_finalize = (destructor)edge_event_buffer_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = edge_event_buffer_getset,
+	.tp_methods = edge_event_buffer_methods,
+	.tp_repr = (reprfunc)edge_event_buffer_repr,
+	.tp_str = (reprfunc)edge_event_buffer_str
+};
+
+int Py_gpiod_RegisterEdgeEventBufferType(PyObject *module)
+{
+	return PyModule_AddType(module, &edge_event_buffer_type);
+}
+
+struct gpiod_edge_event_buffer *Py_gpiod_EdgeEventBufferGetData(PyObject *obj)
+{
+	edge_event_buffer_object *bufobj;
+	PyObject *type;
+
+	type = PyObject_Type(obj);
+	if (!type)
+		return NULL;
+
+	if ((PyTypeObject *)type != &edge_event_buffer_type) {
+		PyErr_SetString(PyExc_TypeError,
+				"not a gpiod.EdgeEventBuffer object");
+		Py_DECREF(type);
+		return NULL;
+	}
+	Py_DECREF(type);
+
+	bufobj = (edge_event_buffer_object *)obj;
+
+	return bufobj->buf;
+}
diff --git a/bindings/python/edge-event.c b/bindings/python/edge-event.c
new file mode 100644
index 0000000..7b908e0
--- /dev/null
+++ b/bindings/python/edge-event.c
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "enum/enum.h"
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_edge_event *event;
+} edge_event_object;
+
+static const PyCEnum_EnumVal event_type_vals[] = {
+	{
+		.name = "RISING_EDGE",
+		.value = GPIOD_EDGE_EVENT_RISING_EDGE,
+	},
+	{
+		.name = "FALLING_EDGE",
+		.value = GPIOD_EDGE_EVENT_FALLING_EDGE,
+	},
+	{ }
+};
+
+static const PyCEnum_EnumDef edge_event_enums[] = {
+	{
+		.name = "Type",
+		.values = event_type_vals,
+	},
+	{ }
+};
+
+static int edge_event_init(PyObject *Py_UNUSED(self),
+			   PyObject *Py_UNUSED(ignored0),
+			   PyObject *Py_UNUSED(ignored1))
+{
+	PyErr_SetString(PyExc_TypeError,
+			"cannot create 'gpiod.EdgeEvent' instances");
+	return -1;
+}
+
+static void edge_event_finalize(edge_event_object *self)
+{
+	if (self->event)
+		gpiod_edge_event_free(self->event);
+}
+
+PyDoc_STRVAR(edge_event_get_type_doc, "Type of the event.");
+
+static PyObject *edge_event_get_type(edge_event_object *self,
+				     void *Py_UNUSED(ignored))
+{
+	int type = gpiod_edge_event_get_event_type(self->event);
+
+	return PyCEnum_MapCToPy((PyObject *)self, "Type", type);
+}
+
+PyDoc_STRVAR(edge_event_timestamp_ns_doc, "Time of the event in nanoseconds.");
+
+static PyObject *edge_event_timestamp_ns(edge_event_object *self,
+					 void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLongLong(
+			gpiod_edge_event_get_timestamp_ns(self->event));
+}
+
+PyDoc_STRVAR(edge_event_line_offset_doc,
+"Offset of the line on which this event was registered.");
+
+static PyObject *edge_event_line_offset(edge_event_object *self,
+					void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLong(
+			gpiod_edge_event_get_line_offset(self->event));
+}
+
+PyDoc_STRVAR(edge_event_global_seqno_doc,
+"Sequence number of the event relative to all lines in the associated line\n"
+"request.");
+
+static PyObject *edge_event_global_seqno(edge_event_object *self,
+					void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLong(
+			gpiod_edge_event_get_global_seqno(self->event));
+}
+
+PyDoc_STRVAR(edge_event_line_seqno_doc,
+"Sequence number of the event relative to this line within the lifetime of\n"
+"the associated line request..");
+
+static PyObject *edge_event_line_seqno(edge_event_object *self,
+					void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLong(
+			gpiod_edge_event_get_line_seqno(self->event));
+}
+
+static PyGetSetDef edge_event_getset[] = {
+	{
+		.name = "type",
+		.get = (getter)edge_event_get_type,
+		.doc = edge_event_get_type_doc,
+	},
+	{
+		.name = "timestamp_ns",
+		.get = (getter)edge_event_timestamp_ns,
+		.doc = edge_event_timestamp_ns_doc,
+	},
+	{
+		.name = "line_offset",
+		.get = (getter)edge_event_line_offset,
+		.doc = edge_event_line_offset_doc,
+	},
+	{
+		.name = "global_seqno",
+		.get = (getter)edge_event_global_seqno,
+		.doc = edge_event_global_seqno_doc,
+	},
+	{
+		.name = "line_seqno",
+		.get = (getter)edge_event_line_seqno,
+		.doc = edge_event_line_seqno_doc,
+	},
+	{ }
+};
+
+static PyObject *edge_event_str(PyObject *self)
+{
+	PyObject *type, *ts, *offset, *gseqno, *lseqno, *str = NULL;
+
+	type = PyObject_GetAttrString(self, "type");
+	ts = PyObject_GetAttrString(self, "timestamp_ns");
+	offset = PyObject_GetAttrString(self, "line_offset");
+	gseqno = PyObject_GetAttrString(self, "global_seqno");
+	lseqno = PyObject_GetAttrString(self, "line_seqno");
+	if (!type || !ts || !offset || !gseqno || !lseqno)
+		goto out;
+
+	str = PyUnicode_FromFormat(
+		"<gpiod.EdgeEvent type=%S timestamp_ns=%S line_offset=%S global_seqno=%S line_seqno=%S>",
+		type, ts, offset, gseqno, lseqno);
+
+out:
+	Py_XDECREF(type);
+	Py_XDECREF(ts);
+	Py_XDECREF(offset);
+	Py_XDECREF(gseqno);
+	Py_XDECREF(lseqno);
+	return str;
+}
+
+PyDoc_STRVAR(edge_event_type_doc,
+"Immutable object containing data about a single line edge event.");
+
+static PyTypeObject edge_event_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.EdgeEvent",
+	.tp_basicsize = sizeof(edge_event_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = edge_event_type_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)edge_event_init,
+	.tp_finalize = (destructor)edge_event_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = edge_event_getset,
+	.tp_str = (reprfunc)edge_event_str
+};
+
+int Py_gpiod_RegisterEdgeEventType(PyObject *module)
+{
+	int ret;
+
+	ret = PyModule_AddType(module, &edge_event_type);
+	if (ret)
+		return ret;
+
+	return PyCEnum_AddEnumsToType(edge_event_enums, &edge_event_type);
+}
+
+PyObject *Py_gpiod_MakeEdgeEvent(struct gpiod_edge_event *event)
+{
+	edge_event_object *event_obj;
+
+	event_obj = PyObject_New(edge_event_object, &edge_event_type);
+	if (!event_obj)
+		return NULL;
+
+	event_obj->event = event;
+
+	return (PyObject *)event_obj;
+}
diff --git a/bindings/python/exception.c b/bindings/python/exception.c
new file mode 100644
index 0000000..30e2dd0
--- /dev/null
+++ b/bindings/python/exception.c
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <stdarg.h>
+#include <stdio.h>
+
+#include "module.h"
+
+struct exception_desc {
+	char *name;
+	char *base;
+	char *doc;
+};
+
+static const struct exception_desc exceptions[] = {
+	{
+		.name = "ChipClosedError",
+		.base = "Exception",
+		.doc = "Error raised when an already closed chip is used.",
+	},
+	{
+		.name = "RequestReleasedError",
+		.base = "Exception",
+		.doc = "Error raised when a released request is used.",
+	},
+	{
+		.name = "BadMappingError",
+		.base = "Exception",
+		.doc = "Exception thrown when the core C library returns an invalid value for any of the line properties.",
+	},
+	{ }
+};
+
+PyObject *_Py_gpiod_SetErrFromErrno(const char *filename)
+{
+	PyObject *exc;
+
+	if (errno == ENOMEM)
+		return PyErr_NoMemory();
+
+	switch (errno) {
+	case EINVAL:
+		exc = PyExc_ValueError;
+		break;
+	case EOPNOTSUPP:
+		exc = PyExc_NotImplementedError;
+		break;
+	case EPIPE:
+		exc = PyExc_BrokenPipeError;
+		break;
+	case ECHILD:
+		exc = PyExc_ChildProcessError;
+		break;
+	case EINTR:
+		exc = PyExc_InterruptedError;
+		break;
+	case EEXIST:
+		exc = PyExc_FileExistsError;
+		break;
+	case ENOENT:
+		exc = PyExc_FileNotFoundError;
+		break;
+	case EISDIR:
+		exc = PyExc_IsADirectoryError;
+		break;
+	case ENOTDIR:
+		exc = PyExc_NotADirectoryError;
+		break;
+	case EPERM:
+		exc = PyExc_PermissionError;
+		break;
+	case ETIMEDOUT:
+		exc = PyExc_TimeoutError;
+		break;
+	default:
+		exc = PyExc_OSError;
+		break;
+	}
+
+	return PyErr_SetFromErrnoWithFilename(exc, filename);
+}
+
+static int add_exception_type(PyObject *module, PyObject *globals,
+			      PyObject *locals,
+			      const struct exception_desc *exc)
+{
+	static const char *const fmt =
+		"class %s(%s):\n"
+		"    \"\"\"%s\"\"\"\n"
+		"    pass";
+
+	PyObject *code, *res, *type;
+	char *src;
+	int ret;
+
+	ret = asprintf(&src, fmt, exc->name, exc->base, exc->doc);
+	if (ret < 0) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	code = Py_CompileString(src, __FILE__, Py_single_input);
+	free(src);
+	if (!code)
+		return -1;
+
+	res = PyEval_EvalCode(code, globals, locals);
+	Py_DECREF(code);
+	if (!res)
+		return -1;
+
+	Py_DECREF(res);
+
+	type = PyDict_GetItemString(locals, exc->name);
+	if (!type)
+		return -1;
+
+	return PyModule_AddType(module, (PyTypeObject *)type);
+}
+
+int Py_gpiod_RegisterExceptionTypes(PyObject *module)
+{
+	const struct exception_desc *exc;
+	PyObject *globals, *locals;
+	int ret;
+
+	globals = PyModule_GetDict(module);
+	if (!globals)
+		return -1;
+
+	locals = PyDict_New();
+	if (!locals)
+		return -1;
+
+	for (exc = exceptions; exc->name; exc++) {
+		ret = add_exception_type(module, globals, locals, exc);
+		if (ret) {
+			Py_DECREF(locals);
+			return -1;
+		}
+	}
+
+	Py_DECREF(locals);
+	return 0;
+}
+
+static void set_error(const char *name, const char *fmt, ...)
+{
+	PyObject *mod, *dict, *type;
+	va_list va;
+
+	mod = Py_gpiod_GetModule();
+	if (!mod)
+		return;
+
+	dict = PyModule_GetDict(mod);
+	if (!dict)
+		return;
+
+	type = PyDict_GetItemString(dict, name);
+	if (!type)
+		return;
+
+	va_start(va, fmt);
+	PyErr_FormatV(type, fmt, va);
+	va_end(va);
+}
+
+void Py_gpiod_SetChipClosedError(void)
+{
+	set_error("ChipClosedError", "I/O operation on closed chip");
+}
+
+void Py_gpiod_SetRequestReleasedError(void)
+{
+	set_error("RequestReleasedError", "GPIO lines have been released");
+}
+
+void Py_gpiod_SetBadMappingError(const char *name)
+{
+	set_error("BadMappingError", "Bad mapping for %s", name);
+}
diff --git a/bindings/python/info-event.c b/bindings/python/info-event.c
new file mode 100644
index 0000000..9e9bcb6
--- /dev/null
+++ b/bindings/python/info-event.c
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "enum/enum.h"
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_info_event *event;
+	PyObject *info;
+} info_event_object;
+
+static const PyCEnum_EnumVal event_type_vals[] = {
+	{
+		.name = "LINE_REQUESTED",
+		.value = GPIOD_INFO_EVENT_LINE_REQUESTED,
+	},
+	{
+		.name = "LINE_RELEASED",
+		.value = GPIOD_INFO_EVENT_LINE_RELEASED,
+	},
+	{
+		.name = "LINE_CONFIG_CHANGED",
+		.value = GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED,
+	},
+	{ }
+};
+
+static const PyCEnum_EnumDef info_event_enums[] = {
+	{
+		.name = "Type",
+		.values = event_type_vals,
+	},
+	{ }
+};
+
+static int info_event_init(PyObject *Py_UNUSED(self),
+			   PyObject *Py_UNUSED(ignored0),
+			   PyObject *Py_UNUSED(ignored1))
+{
+	PyErr_SetString(PyExc_TypeError,
+			"cannot create 'gpiod.InfoEvent' instances");
+	return -1;
+}
+
+static void info_event_finalize(info_event_object *self)
+{
+	Py_XDECREF(self->info);
+
+	if (self->event)
+		gpiod_info_event_free(self->event);
+}
+
+PyDoc_STRVAR(info_event_get_type_doc, "Type of the event.");
+
+static PyObject *info_event_get_type(info_event_object *self,
+				     void *Py_UNUSED(ignored))
+{
+	int type = gpiod_info_event_get_event_type(self->event);
+
+	return PyCEnum_MapCToPy((PyObject *)self, "Type", type);
+}
+
+PyDoc_STRVAR(info_event_timestamp_ns_doc, "Time of the event in nanoseconds.");
+
+static PyObject *info_event_timestamp_ns(info_event_object *self,
+					 void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLongLong(
+			gpiod_info_event_get_timestamp_ns(self->event));
+}
+
+PyDoc_STRVAR(info_event_line_info_doc, "New line information.");
+
+static PyObject *info_event_line_info(info_event_object *self,
+				      void *Py_UNUSED(ignored))
+{
+	struct gpiod_line_info *info, *cpy;
+
+	if (!self->info) {
+		info = gpiod_info_event_get_line_info(self->event);
+		cpy = gpiod_line_info_copy(info);
+		if (!cpy)
+			return NULL;
+
+		self->info = Py_gpiod_MakeLineInfo(cpy);
+		if (!self->info)
+			return NULL;
+	}
+
+	Py_INCREF(self->info);
+	return self->info;
+}
+
+static PyGetSetDef info_event_getset[] = {
+	{
+		.name = "type",
+		.get = (getter)info_event_get_type,
+		.doc = info_event_get_type_doc,
+	},
+	{
+		.name = "timestamp_ns",
+		.get = (getter)info_event_timestamp_ns,
+		.doc = info_event_timestamp_ns_doc,
+	},
+	{
+		.name = "line_info",
+		.get = (getter)info_event_line_info,
+		.doc = info_event_line_info_doc,
+	},
+	{ }
+};
+
+static PyObject *info_event_str(PyObject *self)
+{
+	PyObject *type, *ts, *info, *str = NULL;
+
+	type = PyObject_GetAttrString(self, "type");
+	ts = PyObject_GetAttrString(self, "timestamp_ns");
+	info = PyObject_GetAttrString(self, "line_info");
+	if (!type || !ts || !info)
+		goto out;
+
+	str = PyUnicode_FromFormat(
+		"<gpiod.InfoEvent type=%S timestamp_ns=%S line_info=%S>",
+		type, ts, info);
+
+out:
+	Py_XDECREF(type);
+	Py_XDECREF(ts);
+	Py_XDECREF(info);
+	return str;
+}
+
+PyDoc_STRVAR(info_event_type_doc,
+"Immutable object containing data about a single line info event.");
+
+static PyTypeObject info_event_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.InfoEvent",
+	.tp_basicsize = sizeof(info_event_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = info_event_type_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)info_event_init,
+	.tp_finalize = (destructor)info_event_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = info_event_getset,
+	.tp_str = (reprfunc)info_event_str
+};
+
+int Py_gpiod_RegisterInfoEventType(PyObject *module)
+{
+	int ret;
+
+	ret = PyModule_AddType(module, &info_event_type);
+	if (ret)
+		return ret;
+
+	return PyCEnum_AddEnumsToType(info_event_enums, &info_event_type);
+}
+
+PyObject *Py_gpiod_MakeInfoEvent(struct gpiod_info_event *event)
+{
+	info_event_object *event_obj;
+
+	event_obj = PyObject_New(info_event_object, &info_event_type);
+	if (!event_obj)
+		return NULL;
+
+	event_obj->event = event;
+	event_obj->info = NULL;
+
+	return (PyObject *)event_obj;
+}
diff --git a/bindings/python/line-config.c b/bindings/python/line-config.c
new file mode 100644
index 0000000..35d0589
--- /dev/null
+++ b/bindings/python/line-config.c
@@ -0,0 +1,1338 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <limits.h>
+
+#include "enum/enum.h"
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_line_config *cfg;
+} line_config_object;
+
+struct properties {
+	PyObject *direction;
+	PyObject *edge;
+	PyObject *bias;
+	PyObject *drive;
+	PyObject *active_low;
+	PyObject *debounce_period;
+	PyObject *event_clock;
+	PyObject *output_value;
+	PyObject *output_values;
+};
+
+enum property {
+	PROP_DIRECTION = 1,
+	PROP_EDGE_DETECTION,
+	PROP_BIAS,
+	PROP_DRIVE,
+	PROP_ACTIVE_LOW,
+	PROP_DEBOUNCE_PERIOD,
+	PROP_EVENT_CLOCK,
+	PROP_OUTPUT_VALUE,
+	PROP_OUTPUT_VALUES
+};
+
+static const PyCEnum_EnumVal property_vals[] = {
+	{
+		.name = "DIRECTION",
+		.value = PROP_DIRECTION,
+	},
+	{
+		.name = "EDGE_DETECTION",
+		.value = PROP_EDGE_DETECTION,
+	},
+	{
+		.name = "BIAS",
+		.value = PROP_BIAS,
+	},
+	{
+		.name = "DRIVE",
+		.value = PROP_DRIVE,
+	},
+	{
+		.name = "ACTIVE_LOW",
+		.value = PROP_ACTIVE_LOW,
+	},
+	{
+		.name = "DEBOUNCE_PERIOD",
+		.value = PROP_DEBOUNCE_PERIOD,
+	},
+	{
+		.name = "EVENT_CLOCK",
+		.value = PROP_EVENT_CLOCK,
+	},
+	{
+		.name = "OUTPUT_VALUE",
+		.value = PROP_OUTPUT_VALUE,
+	},
+	{
+		.name = "OUTPUT_VALUES",
+		.value = PROP_OUTPUT_VALUES,
+	},
+	{ }
+};
+
+static const PyCEnum_EnumDef line_config_enums[] = {
+	{
+		.name = "Property",
+		.values = property_vals,
+	},
+	{ }
+};
+
+static int set_default_enum(struct gpiod_line_config *cfg,
+			    void (*set_func)(struct gpiod_line_config *, int),
+			    int prop, PyObject *valobj)
+{
+	int val;
+
+	if (!valobj)
+		return 0;
+
+	val = Py_gpiod_MapLinePropPyToC(prop, valobj);
+	if (val < 0)
+		return -1;
+
+	set_func(cfg, val);
+
+	return 0;
+}
+
+static int set_defaults(struct gpiod_line_config *cfg, struct properties *props)
+{
+	unsigned long debounce_period;
+	bool active_low;
+	int ret;
+
+	ret = set_default_enum(cfg, gpiod_line_config_set_direction_default,
+			       PY_GPIOD_LINE_DIRECTION, props->direction);
+	if (ret)
+		return ret;
+
+	ret = set_default_enum(cfg,
+			       gpiod_line_config_set_edge_detection_default,
+			       PY_GPIOD_LINE_EDGE, props->edge);
+	if (ret)
+		return ret;
+
+	ret = set_default_enum(cfg, gpiod_line_config_set_bias_default,
+			       PY_GPIOD_LINE_BIAS, props->bias);
+	if (ret)
+		return ret;
+
+	ret = set_default_enum(cfg, gpiod_line_config_set_drive_default,
+			       PY_GPIOD_LINE_DRIVE, props->drive);
+	if (ret)
+		return ret;
+
+	if (props->active_low) {
+		if (props->active_low == Py_True) {
+			active_low = true;
+		} else if (props->active_low == Py_False) {
+			active_low = false;
+		} else {
+			PyErr_SetString(PyExc_TypeError,
+					"active_low must be a boolean value");
+			return -1;
+		}
+
+		gpiod_line_config_set_active_low_default(cfg, active_low);
+	}
+
+	if (props->debounce_period) {
+		debounce_period = Py_gpiod_TimedeltaToMicroseconds(
+						props->debounce_period);
+		if (PyErr_Occurred())
+			return -1;
+
+		gpiod_line_config_set_debounce_period_us_default(cfg,
+							debounce_period);
+	}
+
+	ret = set_default_enum(cfg, gpiod_line_config_set_event_clock_default,
+			       PY_GPIOD_LINE_CLOCK, props->event_clock);
+	if (ret)
+		return ret;
+
+	ret = set_default_enum(cfg, gpiod_line_config_set_output_value_default,
+			       PY_GPIOD_LINE_VALUE, props->output_value);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static int line_config_init(line_config_object *self,
+			    PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {
+		"direction",
+		"edge_detection",
+		"bias",
+		"drive",
+		"active_low",
+		"debounce_period",
+		"event_clock",
+		"output_value",
+		"output_values",
+		NULL
+	};
+
+	struct properties props;
+	PyObject *retobj;
+	int ret;
+
+	self->cfg = gpiod_line_config_new();
+	if (!self->cfg) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	memset(&props, 0, sizeof(props));
+
+	ret = PyArg_ParseTupleAndKeywords(args, kwargs, "|$OOOOOOOOO", kwlist,
+					  &props.direction,
+					  &props.edge,
+					  &props.bias,
+					  &props.drive,
+					  &props.active_low,
+					  &props.debounce_period,
+					  &props.event_clock,
+					  &props.output_value,
+					  &props.output_values);
+	if (!ret)
+		return -1;
+
+	if (props.output_values) {
+		retobj = PyObject_CallMethod((PyObject *)self,
+					     "set_output_values",
+					     "O", props.output_values);
+		if (!retobj)
+			return -1;
+
+		Py_DECREF(retobj);
+	}
+
+	return set_defaults(self->cfg, &props);
+}
+
+static void line_config_finalize(line_config_object *self)
+{
+	if (self->cfg)
+		gpiod_line_config_free(self->cfg);
+}
+
+PyDoc_STRVAR(line_config_num_overrides_doc,
+"Number of configuration overrides.");
+
+static PyObject *
+line_config_num_overrides(line_config_object *self, void *Py_UNUSED(ignored))
+{
+	return PyLong_FromSize_t(
+			gpiod_line_config_get_num_overrides(self->cfg));
+}
+
+PyDoc_STRVAR(line_config_overrides_doc,
+"Dictionary of property overrides with keys representing the overridden\n"
+"offsets and values representing the properties.");
+
+static PyObject *
+line_config_overrides(line_config_object *self, void *Py_UNUSED(ignored))
+{
+	PyObject *overrides, *key, *val;
+	size_t num_overrides, i;
+	unsigned int *offsets;
+	int *props, ret;
+
+	num_overrides = gpiod_line_config_get_num_overrides(self->cfg);
+
+	offsets = PyMem_Calloc(num_overrides, sizeof(unsigned int));
+	if (!offsets) {
+		PyErr_NoMemory();
+		return NULL;
+	}
+
+	props = PyMem_Calloc(num_overrides, sizeof(int));
+	if (!props) {
+		PyErr_NoMemory();
+		PyMem_Free(offsets);
+		return NULL;
+	}
+
+	gpiod_line_config_get_overrides(self->cfg, offsets, props);
+
+	overrides = PyDict_New();
+	if (!overrides) {
+		PyMem_Free(offsets);
+		PyMem_Free(props);
+		return NULL;
+	}
+
+	for (i = 0; i < num_overrides; i++) {
+		key = PyLong_FromUnsignedLong(offsets[i]);
+		if (PyErr_Occurred()) {
+			Py_DECREF(overrides);
+			PyMem_Free(offsets);
+			PyMem_Free(props);
+			return NULL;
+		}
+
+		val = PyCEnum_MapCToPy((PyObject *)self, "Property", props[i]);
+		if (!val) {
+			Py_DECREF(key);
+			Py_DECREF(overrides);
+			PyMem_Free(offsets);
+			PyMem_Free(props);
+			return NULL;
+		}
+
+		ret = PyDict_SetItem(overrides, key, val);
+		Py_DECREF(key);
+		Py_DECREF(val);
+		if (ret) {
+			Py_DECREF(overrides);
+			PyMem_Free(offsets);
+			PyMem_Free(props);
+			return NULL;
+		}
+	}
+
+	PyMem_Free(offsets);
+	PyMem_Free(props);
+
+	return overrides;
+}
+
+static PyGetSetDef line_config_getset[] = {
+	{
+		.name = "num_overrides",
+		.get = (getter)line_config_num_overrides,
+		.doc = line_config_num_overrides_doc,
+	},
+	{
+		.name = "overrides",
+		.get = (getter)line_config_overrides,
+		.doc = line_config_overrides_doc,
+	},
+	{ }
+};
+
+PyDoc_STRVAR(line_config_reset_doc,
+"reset() -> None\n"
+"\n"
+"Reset the line config object.");
+
+static PyObject *
+line_config_reset(line_config_object *self, PyObject *Py_UNUSED(ignored))
+{
+	gpiod_line_config_reset(self->cfg);
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(line_config_set_props_default_doc,
+"set_props_default(**kwargs) -> None\n"
+"\n"
+"Set the defaults for properties.\n"
+"\n"
+"  direction\n"
+"    default direction\n"
+"  edge\n"
+"    default edge detection\n"
+"  bias\n"
+"    default bias\n"
+"  drive\n"
+"    default drive\n"
+"  active_low\n"
+"    default active-low setting\n"
+"  debounce_period\n"
+"    default debounce period\n"
+"  event_clock\n"
+"    default event clock\n"
+"  output_value\n"
+"    default output value");
+
+static PyObject *
+line_config_set_props_default(line_config_object *self,
+			      PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {
+		"direction",
+		"edge_detection",
+		"bias",
+		"drive",
+		"active_low",
+		"debounce_period",
+		"event_clock",
+		"output_value",
+		NULL
+	};
+
+	struct properties props;
+	int ret;
+
+	memset(&props, 0, sizeof(props));
+
+	ret = PyArg_ParseTupleAndKeywords(args, kwargs, "|$OOOOOOOO", kwlist,
+					  &props.direction,
+					  &props.edge,
+					  &props.bias,
+					  &props.drive,
+					  &props.active_low,
+					  &props.debounce_period,
+					  &props.event_clock,
+					  &props.output_value);
+	if (!ret)
+		return NULL;
+
+	ret = set_defaults(self->cfg, &props);
+	if (ret)
+		return NULL;
+
+	Py_RETURN_NONE;
+}
+
+static int set_override_enum(struct gpiod_line_config *cfg,
+			     void (*set_func)(struct gpiod_line_config *,
+					      int, unsigned int),
+			     unsigned int offset, int prop, PyObject *valobj)
+{
+	int val;
+
+	if (!valobj)
+		return 0;
+
+	val = Py_gpiod_MapLinePropPyToC(prop, valobj);
+	if (val < 0)
+		return -1;
+
+	set_func(cfg, val, offset);
+
+	return 0;
+}
+
+PyDoc_STRVAR(line_config_set_props_override_doc,
+"set_props_override(offset, **kwargs) -> None\n"
+"\n"
+"Set property overrides for line.\n"
+"\n"
+"  offset\n"
+"    offset of the line for which to set the overrides\n"
+"  direction\n"
+"    default direction\n"
+"  edge\n"
+"    default edge detection\n"
+"  bias\n"
+"    default bias\n"
+"  drive\n"
+"    default drive\n"
+"  active_low\n"
+"    default active-low setting\n"
+"  debounce_period\n"
+"    default debounce period\n"
+"  event_clock\n"
+"    default event clock\n"
+"  output_value\n"
+"    default output value");
+
+static PyObject *
+line_config_set_props_override(line_config_object *self,
+			       PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {
+		"",
+		"direction",
+		"edge_detection",
+		"bias",
+		"drive",
+		"active_low",
+		"debounce_period",
+		"event_clock",
+		"output_value",
+		NULL
+	};
+
+	struct gpiod_line_config *cfg = self->cfg;
+	unsigned long debounce_period;
+	struct properties props;
+	unsigned int offset;
+	bool active_low;
+	int ret;
+
+	memset(&props, 0, sizeof(props));
+
+	ret = PyArg_ParseTupleAndKeywords(args, kwargs, "I|$OOOOOOOO", kwlist,
+					  &offset,
+					  &props.direction,
+					  &props.edge,
+					  &props.bias,
+					  &props.drive,
+					  &props.active_low,
+					  &props.debounce_period,
+					  &props.event_clock,
+					  &props.output_value);
+	if (!ret)
+		return NULL;
+
+	ret = set_override_enum(cfg, gpiod_line_config_set_direction_override,
+				offset, PY_GPIOD_LINE_DIRECTION,
+				props.direction);
+	if (ret)
+		return NULL;
+
+	ret = set_override_enum(cfg,
+				gpiod_line_config_set_edge_detection_override,
+				offset, PY_GPIOD_LINE_EDGE, props.edge);
+	if (ret)
+		return NULL;
+
+	ret = set_override_enum(cfg, gpiod_line_config_set_bias_override,
+				offset, PY_GPIOD_LINE_BIAS, props.bias);
+	if (ret)
+		return NULL;
+
+	ret = set_override_enum(cfg, gpiod_line_config_set_drive_override,
+				offset, PY_GPIOD_LINE_DRIVE, props.drive);
+	if (ret)
+		return NULL;
+
+	if (props.active_low) {
+		if (props.active_low == Py_True) {
+			active_low = true;
+		} else if (props.active_low == Py_False) {
+			active_low = false;
+		} else {
+			PyErr_SetString(PyExc_TypeError,
+					"active_low must be a boolean value");
+			return NULL;
+		}
+
+		gpiod_line_config_set_active_low_override(cfg, active_low,
+							  offset);
+	}
+
+	if (props.debounce_period) {
+		debounce_period = Py_gpiod_TimedeltaToMicroseconds(
+						props.debounce_period);
+		if (PyErr_Occurred())
+			return NULL;
+
+		gpiod_line_config_set_debounce_period_us_override(cfg,
+								debounce_period,
+								offset);
+	}
+
+	ret = set_override_enum(cfg, gpiod_line_config_set_event_clock_override,
+				offset, PY_GPIOD_LINE_CLOCK, props.event_clock);
+	if (ret)
+		return NULL;
+
+	ret = set_override_enum(cfg,
+				gpiod_line_config_set_output_value_override,
+				offset, PY_GPIOD_LINE_VALUE,
+				props.output_value);
+	if (ret)
+		return NULL;
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(line_config_get_props_default_doc,
+"get_prop_default(**kwargs) -> (val0, val1, ...)\n"
+"\n"
+"Get default values for a set of line properties.\n"
+"\n"
+"Takes a variable number of property types as defined by the\n"
+"gpiod.LineConfig.Property enum.");
+
+static PyObject *
+line_config_get_props_default(line_config_object *self, PyObject *args)
+{
+	PyObject *iter, *props, *next, *item;
+	struct gpiod_line_config *cfg;
+	unsigned long debounce_period;
+	Py_ssize_t num_props, i;
+	int prop, val, ret;
+	bool active_low;
+
+	num_props = PyTuple_GET_SIZE(args);
+	if (num_props < 0)
+		return NULL;
+
+	if (num_props < 1)
+		Py_RETURN_NONE;
+
+	props = PyTuple_New(num_props);
+	if (!props)
+		return NULL;
+
+	iter = PyObject_GetIter(args);
+	if (!iter) {
+		Py_DECREF(props);
+		return NULL;
+	}
+
+	cfg = self->cfg;
+
+	for (i = 0;; i++) {
+		next = PyIter_Next(iter);
+		if (!next)
+			break;
+
+		prop = PyCEnum_MapPyToC((PyObject *)self, "Property", next);
+		Py_DECREF(next);
+		if (prop < 0) {
+			Py_DECREF(props);
+			return NULL;
+		}
+
+		switch (prop) {
+		case PROP_DIRECTION:
+			val = gpiod_line_config_get_direction_default(cfg);
+			item = Py_gpiod_MapLinePropCToPy(
+					PY_GPIOD_LINE_DIRECTION, val);
+			break;
+		case PROP_EDGE_DETECTION:
+			val = gpiod_line_config_get_edge_detection_default(cfg);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_EDGE,
+							 val);
+			break;
+		case PROP_BIAS:
+			val = gpiod_line_config_get_bias_default(cfg);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_BIAS,
+							 val);
+			break;
+		case PROP_DRIVE:
+			val = gpiod_line_config_get_drive_default(cfg);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_DRIVE,
+							 val);
+			break;
+		case PROP_ACTIVE_LOW:
+			active_low =
+				gpiod_line_config_get_active_low_default(cfg);
+			item = active_low ? Py_True : Py_False;
+			Py_INCREF(item);
+			break;
+		case PROP_DEBOUNCE_PERIOD:
+			debounce_period =
+			gpiod_line_config_get_debounce_period_us_default(cfg);
+			item = Py_gpiod_MicrosecondsToTimedelta(
+							debounce_period);
+			break;
+		case PROP_EVENT_CLOCK:
+			val = gpiod_line_config_get_event_clock_default(cfg);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_CLOCK,
+							 val);
+			break;
+		case PROP_OUTPUT_VALUE:
+			val = gpiod_line_config_get_output_value_default(cfg);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_VALUE,
+							 val);
+			break;
+		default:
+			Py_DECREF(props);
+			PyErr_SetString(PyExc_ValueError,
+					"unsupported property type");
+			return NULL;
+		}
+
+		if (!item) {
+			Py_DECREF(props);
+			return NULL;
+		}
+
+		ret = PyTuple_SetItem(props, i, item);
+		if (ret < 0) {
+			Py_DECREF(props);
+			return NULL;
+		}
+	}
+
+	if (num_props == 1) {
+		item = PyTuple_GetItem(props, 0);
+		Py_INCREF(item);
+		Py_DECREF(props);
+		return item;
+	}
+
+	return props;
+}
+
+PyDoc_STRVAR(line_config_get_props_offset_doc,
+"get_prop_offset(offset, **kwargs) -> (val0, val1, ...)\n"
+"\n"
+"Get actual values for a set of line properties for a line.\n"
+"\n"
+"Takes a variable number of property types as defined by the\n"
+"gpiod.LineConfig.Property enum.\n"
+"\n"
+"  offset\n"
+"    The offset of the line for which to read the properties");
+
+static PyObject *
+line_config_get_props_offset(line_config_object *self, PyObject *args)
+{
+	unsigned long tmp, debounce_period;
+	PyObject *props, *item, *next;
+	struct gpiod_line_config *cfg;
+	Py_ssize_t num_args, i;
+	unsigned int offset;
+	int ret, prop, val;
+	bool active_low;
+
+	num_args = PyTuple_GET_SIZE(args);
+	if (num_args < 0)
+		return NULL;
+
+	if (num_args < 1) {
+		PyErr_SetString(PyExc_TypeError, "line offset must be given");
+		return NULL;
+	}
+
+	item = PyTuple_GetItem(args, 0);
+	if (!item)
+		return NULL;
+
+	tmp = PyLong_AsUnsignedLong(item);
+	if (PyErr_Occurred())
+		return NULL;
+
+	if (tmp > UINT_MAX) {
+		PyErr_SetString(PyExc_ValueError, "max offset value exceeded");
+		return NULL;
+	}
+
+	offset = tmp;
+
+	props = PyTuple_New(num_args - 1);
+	if (!props)
+		return NULL;
+
+	cfg = self->cfg;
+
+	for (i = 1; i < num_args; i++) {
+		next = PyTuple_GetItem(args, i);
+		if (!next) {
+			Py_DECREF(props);
+			return NULL;
+		}
+
+		prop = PyCEnum_MapPyToC((PyObject *)self, "Property", next);
+		if (prop < 0) {
+			Py_DECREF(props);
+			return NULL;
+		}
+
+		switch (prop) {
+		case PROP_DIRECTION:
+			val = gpiod_line_config_get_direction_offset(cfg,
+								     offset);
+			item = Py_gpiod_MapLinePropCToPy(
+					PY_GPIOD_LINE_DIRECTION, val);
+			break;
+		case PROP_EDGE_DETECTION:
+			val = gpiod_line_config_get_edge_detection_offset(cfg,
+									offset);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_EDGE,
+							 val);
+			break;
+		case PROP_BIAS:
+			val = gpiod_line_config_get_bias_offset(cfg, offset);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_BIAS,
+							 val);
+			break;
+		case PROP_DRIVE:
+			val = gpiod_line_config_get_drive_offset(cfg, offset);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_DRIVE,
+							 val);
+			break;
+		case PROP_ACTIVE_LOW:
+			active_low =
+				gpiod_line_config_get_active_low_offset(cfg,
+									offset);
+			item = active_low ? Py_True : Py_False;
+			Py_INCREF(item);
+			break;
+		case PROP_DEBOUNCE_PERIOD:
+			debounce_period =
+			gpiod_line_config_get_debounce_period_us_offset(cfg,
+									offset);
+			item = Py_gpiod_MicrosecondsToTimedelta(
+							debounce_period);
+			break;
+		case PROP_EVENT_CLOCK:
+			val = gpiod_line_config_get_event_clock_offset(cfg,
+								       offset);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_CLOCK,
+							 val);
+			break;
+		case PROP_OUTPUT_VALUE:
+			val = gpiod_line_config_get_output_value_offset(cfg,
+									offset);
+			item = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_VALUE,
+							 val);
+			break;
+		default:
+			Py_DECREF(props);
+			PyErr_SetString(PyExc_ValueError,
+					"unsupported property type");
+			return NULL;
+		}
+
+		if (!item) {
+			Py_DECREF(props);
+			return NULL;
+		}
+
+		ret = PyTuple_SetItem(props, i - 1, item);
+		if (ret < 0) {
+			Py_DECREF(props);
+			return NULL;
+		}
+	}
+
+	if (num_args == 2) {
+		item = PyTuple_GetItem(props, 0);
+		Py_INCREF(item);
+		Py_DECREF(props);
+		return item;
+	}
+
+	return props;
+}
+
+PyDoc_STRVAR(line_config_prop_is_overridden_doc,
+"prop_is_overridden(offset, prop) -> bool\n"
+"\n"
+"Check if the property is overridden for a line.\n"
+"\n"
+"  offset\n"
+"    Offset of the line for which to check the property\n"
+"  prop\n"
+"    Which property to check");
+
+static PyObject *
+line_config_prop_is_overridden(line_config_object *self, PyObject *args)
+{
+	struct gpiod_line_config *cfg = self->cfg;
+	unsigned int offset;
+	PyObject *prop_obj;
+	int ret, prop;
+	bool val;
+
+	ret = PyArg_ParseTuple(args, "IO", &offset, &prop_obj);
+	if (!ret)
+		return NULL;
+
+	prop = PyCEnum_MapPyToC((PyObject *)self, "Property", prop_obj);
+	if (prop < 0)
+		return NULL;
+
+	switch (prop) {
+	case PROP_DIRECTION:
+		val = gpiod_line_config_direction_is_overridden(cfg, offset);
+		break;
+	case PROP_EDGE_DETECTION:
+		val = gpiod_line_config_edge_detection_is_overridden(cfg,
+								     offset);
+		break;
+	case PROP_BIAS:
+		val = gpiod_line_config_bias_is_overridden(cfg, offset);
+		break;
+	case PROP_DRIVE:
+		val = gpiod_line_config_drive_is_overridden(cfg, offset);
+		break;
+	case PROP_ACTIVE_LOW:
+		val = gpiod_line_config_active_low_is_overridden(cfg, offset);
+		break;
+	case PROP_DEBOUNCE_PERIOD:
+		val = gpiod_line_config_debounce_period_us_is_overridden(cfg,
+									offset);
+		break;
+	case PROP_EVENT_CLOCK:
+		val = gpiod_line_config_event_clock_is_overridden(cfg, offset);
+		break;
+	case PROP_OUTPUT_VALUE:
+		val = gpiod_line_config_output_value_is_overridden(cfg, offset);
+		break;
+	default:
+		PyErr_SetString(PyExc_ValueError,
+				"unsupported property type");
+		return NULL;
+	}
+
+	return PyBool_FromLong(val);
+}
+
+PyDoc_STRVAR(line_config_clear_prop_override_doc,
+"clear_override(offset, prop) -> None\n"
+"\n"
+"Check if the property is overridden for a line.\n"
+"\n"
+"  offset\n"
+"    Offset of the line for which to check the property\n"
+"  prop\n"
+"    Which property to check");
+
+static PyObject *
+line_config_clear_prop_override(line_config_object *self, PyObject *args)
+{
+	struct gpiod_line_config *cfg = self->cfg;
+	unsigned int offset;
+	PyObject *prop_obj;
+	int ret, prop;
+
+	ret = PyArg_ParseTuple(args, "IO", &offset, &prop_obj);
+	if (!ret)
+		return NULL;
+
+	prop = PyCEnum_MapPyToC((PyObject *)self, "Property", prop_obj);
+	if (prop < 0)
+		return NULL;
+
+	switch (prop) {
+	case PROP_DIRECTION:
+		gpiod_line_config_clear_direction_override(cfg, offset);
+		break;
+	case PROP_EDGE_DETECTION:
+		gpiod_line_config_clear_edge_detection_override(cfg, offset);
+		break;
+	case PROP_BIAS:
+		gpiod_line_config_clear_bias_override(cfg, offset);
+		break;
+	case PROP_DRIVE:
+		gpiod_line_config_clear_drive_override(cfg, offset);
+		break;
+	case PROP_ACTIVE_LOW:
+		gpiod_line_config_clear_active_low_override(cfg, offset);
+		break;
+	case PROP_DEBOUNCE_PERIOD:
+		gpiod_line_config_clear_debounce_period_us_override(cfg,
+								    offset);
+		break;
+	case PROP_EVENT_CLOCK:
+		gpiod_line_config_clear_event_clock_override(cfg, offset);
+		break;
+	case PROP_OUTPUT_VALUE:
+		gpiod_line_config_clear_output_value_override(cfg, offset);
+		break;
+	default:
+		PyErr_SetString(PyExc_ValueError,
+				"unsupported property type");
+		return NULL;
+	}
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(line_config_set_output_values_doc,
+"set_output_values(values) -> None\n"
+"\n"
+"Override the output values for multiple lines.\n"
+"\n"
+"  values\n"
+"    Dictionary mapping line offsets to their values");
+
+static PyObject *
+line_config_set_output_values(line_config_object *self, PyObject *args)
+{
+	PyObject *dict, *items, *iter, *next, *key, *val;
+	unsigned int offset;
+	unsigned long tmp;
+	int ret, value;
+
+	ret = PyArg_ParseTuple(args, "O", &dict);
+	if (!ret)
+		return NULL;
+
+	if (!PyDict_Check(dict)) {
+		PyErr_SetString(PyExc_TypeError,
+				"argument must be a dictionary");
+		return NULL;
+	}
+
+	items = PyDict_Items(dict);
+	if (!items)
+		return NULL;
+
+	iter = PyObject_GetIter(items);
+	if (!iter) {
+		Py_DECREF(items);
+		return NULL;
+	}
+
+	for (;;) {
+		next = PyIter_Next(iter);
+		if (!next) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		key = PyTuple_GetItem(next, 0);
+		val = PyTuple_GetItem(next, 1);
+		if (!key || !val) {
+			Py_DECREF(next);
+			Py_DECREF(iter);
+			Py_DECREF(items);
+			return NULL;
+		}
+
+		tmp = PyLong_AsUnsignedLong(key);
+		if (PyErr_Occurred()) {
+			Py_DECREF(next);
+			Py_DECREF(iter);
+			Py_DECREF(items);
+			return NULL;
+		}
+
+		if (tmp > UINT_MAX) {
+			Py_DECREF(next);
+			Py_DECREF(iter);
+			Py_DECREF(items);
+			return NULL;
+		}
+
+		offset = tmp;
+
+		value = Py_gpiod_MapLinePropPyToC(PY_GPIOD_LINE_VALUE, val);
+		if (value < 0) {
+			Py_DECREF(next);
+			Py_DECREF(iter);
+			Py_DECREF(items);
+			return NULL;
+		}
+
+		gpiod_line_config_set_output_value_override(self->cfg,
+							    value, offset);
+		Py_DECREF(next);
+	}
+
+	Py_DECREF(items);
+
+	Py_RETURN_NONE;
+}
+
+static PyMethodDef line_config_methods[] = {
+	{
+		.ml_name = "reset",
+		.ml_meth = (PyCFunction)line_config_reset,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = line_config_reset_doc,
+	},
+	{
+		.ml_name = "set_props_default",
+		.ml_meth = (PyCFunction)(void(*)(void))
+				line_config_set_props_default,
+		.ml_flags = METH_VARARGS | METH_KEYWORDS,
+		.ml_doc = line_config_set_props_default_doc,
+	},
+	{
+		.ml_name = "set_props_override",
+		.ml_meth = (PyCFunction)(void(*)(void))
+				line_config_set_props_override,
+		.ml_flags = METH_VARARGS | METH_KEYWORDS,
+		.ml_doc = line_config_set_props_override_doc,
+	},
+	{
+		.ml_name = "get_props_default",
+		.ml_meth = (PyCFunction)line_config_get_props_default,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_config_get_props_default_doc,
+	},
+	{
+		.ml_name = "get_props_offset",
+		.ml_meth = (PyCFunction)line_config_get_props_offset,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_config_get_props_offset_doc,
+	},
+	{
+		.ml_name = "prop_is_overridden",
+		.ml_meth = (PyCFunction)line_config_prop_is_overridden,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_config_prop_is_overridden_doc,
+	},
+	{
+		.ml_name = "clear_prop_override",
+		.ml_meth = (PyCFunction)line_config_clear_prop_override,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_config_clear_prop_override_doc,
+	},
+	{
+		.ml_name = "set_output_values",
+		.ml_meth = (PyCFunction)line_config_set_output_values,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_config_set_output_values_doc,
+	},
+	{ }
+};
+
+static PyObject *str_get_defaults(PyObject *self)
+{
+	static const int enums[] = {
+		PROP_DIRECTION,
+		PROP_EDGE_DETECTION,
+		PROP_BIAS,
+		PROP_DRIVE,
+		PROP_ACTIVE_LOW,
+		PROP_DEBOUNCE_PERIOD,
+		PROP_EVENT_CLOCK,
+		PROP_OUTPUT_VALUE,
+		PROP_OUTPUT_VALUES
+	};
+
+	PyObject *defaults = NULL, *enum_objs[8], *str = NULL;
+	int i;
+
+	memset(enum_objs, 0, sizeof(enum_objs));
+
+	for (i = 0; i < 8; i++) {
+		enum_objs[i] = PyCEnum_MapCToPy(self, "Property", enums[i]);
+		if (!enum_objs[i]) {
+			for (i = 0; i < 8; i++)
+				Py_XDECREF(enum_objs[i]);
+			return NULL;
+		}
+	}
+
+	defaults = PyObject_CallMethod(self, "get_props_default", "OOOOOOOO",
+				       enum_objs[0], enum_objs[1], enum_objs[2],
+				       enum_objs[3], enum_objs[4], enum_objs[5],
+				       enum_objs[6], enum_objs[7]);
+	for (i = 0; i < 8; i++)
+		Py_XDECREF(enum_objs[i]);
+	if (!defaults)
+		return NULL;
+
+	str = PyUnicode_FromFormat(
+		"direction=%S edge_detection=%S bias=%S drive=%S active_low=%S debounce_period=%S event_clock=%S output_value=%S",
+		PyTuple_GetItem(defaults, 0), PyTuple_GetItem(defaults, 1),
+		PyTuple_GetItem(defaults, 2), PyTuple_GetItem(defaults, 3),
+		PyTuple_GetItem(defaults, 4), PyTuple_GetItem(defaults, 5),
+		PyTuple_GetItem(defaults, 6), PyTuple_GetItem(defaults, 7));
+	Py_DECREF(defaults);
+	return str;
+}
+
+static int
+str_fill_override_strings(PyObject *self, Py_ssize_t num_overrides,
+			  const int *props, const unsigned int *offsets,
+			  PyObject *list)
+{
+	Py_ssize_t i;
+	const char *propname;
+	int prop, ret;
+	unsigned int offset;
+	PyObject *str, *propobj, *val;
+
+	for (i = 0; i < num_overrides; i++) {
+		prop = props[i];
+		offset = offsets[i];
+
+		switch (prop) {
+		case GPIOD_LINE_CONFIG_PROP_DIRECTION:
+			prop = PROP_DIRECTION;
+			propname = "direction";
+			break;
+		case GPIOD_LINE_CONFIG_PROP_EDGE_DETECTION:
+			prop = PROP_EDGE_DETECTION;
+			propname = "edge_detection";
+			break;
+		case GPIOD_LINE_CONFIG_PROP_BIAS:
+			prop = PROP_BIAS;
+			propname = "bias";
+			break;
+		case GPIOD_LINE_CONFIG_PROP_DRIVE:
+			prop = PROP_DRIVE;
+			propname = "drive";
+			break;
+		case GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW:
+			prop = PROP_ACTIVE_LOW;
+			propname = "active_low";
+			break;
+		case GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD_US:
+			prop = PROP_DEBOUNCE_PERIOD;
+			propname = "debounce_period";
+			break;
+		case GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK:
+			prop = PROP_EVENT_CLOCK;
+			propname = "event_clock";
+			break;
+		case GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE:
+			prop = PROP_OUTPUT_VALUE;
+			propname = "output_value";
+			break;
+		default:
+			Py_gpiod_SetBadMappingError("LineConfig property");
+			return -1;
+		}
+
+		propobj = PyCEnum_MapCToPy(self, "Property", prop);
+		if (!propobj)
+			return -1;
+
+		val = PyObject_CallMethod(self, "get_props_offset",
+					  "IO", offset, propobj);
+		Py_DECREF(propobj);
+		if (!val)
+			return -1;
+
+		str = PyUnicode_FromFormat("%u: %s=%S", offset, propname, val);
+		Py_DECREF(val);
+		if (!str)
+			return -1;
+
+		ret = PyList_SetItem(list, i, str);
+		if (ret) {
+			Py_DECREF(str);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+static PyObject *str_get_override_list(line_config_object *self)
+{
+	Py_ssize_t num_overrides;
+	unsigned int *offsets;
+	PyObject *overrides;
+	int *props, ret;
+
+	num_overrides = gpiod_line_config_get_num_overrides(self->cfg);
+	if (num_overrides == 0)
+		return NULL;
+
+	overrides = PyList_New(num_overrides);
+	if (!overrides)
+		return NULL;
+
+	offsets = PyMem_Calloc(num_overrides, sizeof(unsigned int));
+	if (!offsets) {
+		PyErr_NoMemory();
+		Py_DECREF(overrides);
+		return NULL;
+	}
+
+	props = PyMem_Calloc(num_overrides, sizeof(int));
+	if (!props) {
+		PyErr_NoMemory();
+		Py_DECREF(overrides);
+		PyMem_Free(offsets);
+		return NULL;
+	}
+
+	gpiod_line_config_get_overrides(self->cfg, offsets, props);
+
+	ret = str_fill_override_strings((PyObject *)self, num_overrides,
+					props, offsets, overrides);
+	PyMem_Free(props);
+	PyMem_Free(offsets);
+	if (ret) {
+		Py_DECREF(overrides);
+		return NULL;
+	}
+
+	return overrides;
+}
+
+static PyObject *str_get_overrides(line_config_object *self)
+{
+	PyObject *overrides, *joined, *str, *final;
+
+	overrides = str_get_override_list(self);
+	if (!overrides)
+		return NULL;
+
+	str = PyUnicode_FromString(", ");
+	if (!str) {
+		Py_DECREF(overrides);
+		return NULL;
+	}
+
+	joined = PyObject_CallMethod(str, "join", "O", overrides);
+	Py_DECREF(overrides);
+	Py_DECREF(str);
+
+	final = PyUnicode_FromFormat("{%S}", joined);
+	Py_DECREF(joined);
+	return final;
+}
+
+static PyObject *line_config_str(PyObject *self)
+{
+	PyObject *defaults, *overrides, *str;
+
+	defaults = str_get_defaults(self);
+	if (!defaults)
+		return NULL;
+
+	overrides = str_get_overrides((line_config_object *)self);
+	if (PyErr_Occurred()) {
+		Py_DECREF(defaults);
+		return NULL;
+	}
+
+	if (overrides)
+		str = PyUnicode_FromFormat("<gpiod.LineConfig %S overrides=%S>",
+					   defaults, overrides);
+	else
+		str = PyUnicode_FromFormat("<gpiod.LineConfig %S>", defaults);
+	Py_DECREF(defaults);
+	Py_XDECREF(overrides);
+	return str;
+}
+
+PyDoc_STRVAR(line_config_type_doc,
+"Contains a set of line config options used in line requests and\n"
+"reconfiguration.");
+
+static PyTypeObject line_config_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.LineConfig",
+	.tp_basicsize = sizeof(line_config_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = line_config_type_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)line_config_init,
+	.tp_finalize = (destructor)line_config_finalize,
+	.tp_getset = line_config_getset,
+	.tp_methods = line_config_methods,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_str = (reprfunc)line_config_str
+};
+
+int Py_gpiod_RegisterLineConfigType(PyObject *module)
+{
+	int ret;
+
+	ret = PyModule_AddType(module, &line_config_type);
+	if (ret)
+		return -1;
+
+	return PyCEnum_AddEnumsToType(line_config_enums, &line_config_type);
+}
+
+struct gpiod_line_config *Py_gpiod_LineConfigGetData(PyObject *obj)
+{
+	line_config_object *linecfg;
+	PyObject *type;
+
+	type = PyObject_Type(obj);
+	if (!type)
+		return NULL;
+
+	if ((PyTypeObject *)type != &line_config_type) {
+		PyErr_SetString(PyExc_TypeError,
+				"not a gpiod.LineConfig object");
+		Py_DECREF(type);
+		return NULL;
+	}
+	Py_DECREF(type);
+
+	linecfg = (line_config_object *)obj;
+
+	return linecfg->cfg;
+}
diff --git a/bindings/python/line-info.c b/bindings/python/line-info.c
new file mode 100644
index 0000000..47ed2da
--- /dev/null
+++ b/bindings/python/line-info.c
@@ -0,0 +1,286 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_line_info *info;
+} line_info_object;
+
+static int line_info_init(PyObject *Py_UNUSED(self),
+			  PyObject *Py_UNUSED(ignored0),
+			  PyObject *Py_UNUSED(ignored1))
+{
+	PyErr_SetString(PyExc_TypeError,
+			"cannot create 'gpiod.LineInfo' instances");
+	return -1;
+}
+
+static void line_info_finalize(line_info_object *self)
+{
+	if (self->info)
+		gpiod_line_info_free(self->info);
+}
+
+PyDoc_STRVAR(line_info_offset_doc,
+"Offset of the line within the parent chip.");
+
+static PyObject *line_info_offset(line_info_object *self,
+				  void *Py_UNUSED(ignored))
+{
+	return PyLong_FromUnsignedLong(gpiod_line_info_get_offset(self->info));
+}
+
+PyDoc_STRVAR(line_info_name_doc,
+"Name of the line as represented in the kernel.");
+
+static PyObject *line_info_name(line_info_object *self,
+				void *Py_UNUSED(ignored))
+{
+	const char *name = gpiod_line_info_get_name(self->info);
+
+	if (!name)
+		Py_RETURN_NONE;
+
+	return PyUnicode_FromString(name);
+}
+
+PyDoc_STRVAR(line_info_used_doc,
+"True if the line is in use, False otherwise.");
+
+static PyObject *line_info_used(line_info_object *self,
+				void *Py_UNUSED(ignored))
+{
+	return PyBool_FromLong(gpiod_line_info_is_used(self->info));
+}
+
+PyDoc_STRVAR(line_info_consumer_doc,
+"Consumer of the line as represented in the kernel.\n"
+"\n"
+"None if the line is unused");
+
+static PyObject *line_info_consumer(line_info_object *self,
+				    void *Py_UNUSED(ignored))
+{
+	const char *consumer = gpiod_line_info_get_consumer(self->info);
+
+	if (!consumer)
+		Py_RETURN_NONE;
+
+	return PyUnicode_FromString(consumer);
+}
+
+PyDoc_STRVAR(line_info_direction_doc, "Line direction.");
+
+static PyObject *line_info_direction(line_info_object *self,
+				     void *Py_UNUSED(ignored))
+{
+	return Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_DIRECTION,
+				gpiod_line_info_get_direction(self->info));
+}
+
+PyDoc_STRVAR(line_info_active_low_doc,
+"True if the line is active-low, false otherwise.");
+
+static PyObject *line_info_active_low(line_info_object *self,
+				      void *Py_UNUSED(ignored))
+{
+	return PyBool_FromLong(gpiod_line_info_is_active_low(self->info));
+}
+
+PyDoc_STRVAR(line_info_bias_doc, "Line bias.");
+
+static PyObject *line_info_bias(line_info_object *self,
+				void *Py_UNUSED(ignored))
+{
+	return Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_BIAS,
+				gpiod_line_info_get_bias(self->info));
+}
+
+PyDoc_STRVAR(line_info_drive_doc, "Line drive.");
+
+static PyObject *line_info_drive(line_info_object *self,
+				 void *Py_UNUSED(ignored))
+{
+	return Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_DRIVE,
+				gpiod_line_info_get_drive(self->info));
+}
+
+PyDoc_STRVAR(line_info_edge_detection_doc, "Edge event detection.");
+
+static PyObject *line_info_edge_detection(line_info_object *self,
+					  void *Py_UNUSED(ignored))
+{
+	return Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_EDGE,
+			gpiod_line_info_get_edge_detection(self->info));
+}
+
+PyDoc_STRVAR(line_info_event_clock_doc, "Clock used to timestamp edge events.");
+
+static PyObject *line_info_event_clock(line_info_object *self,
+				       void *Py_UNUSED(ignored))
+{
+	return Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_CLOCK,
+			gpiod_line_info_get_event_clock(self->info));
+}
+
+PyDoc_STRVAR(line_info_debounced_doc,
+"True if the line is debounced, false otherwise.");
+
+static PyObject *line_info_debounced(line_info_object *self,
+				     void *Py_UNUSED(ignored))
+{
+	return PyBool_FromLong(gpiod_line_info_is_debounced(self->info));
+}
+
+PyDoc_STRVAR(line_info_debounce_period_doc, "Debounce period.");
+
+static PyObject *line_info_debounce_period(line_info_object *self,
+					   void *Py_UNUSED(ignored))
+{
+	return Py_gpiod_MicrosecondsToTimedelta(
+			gpiod_line_info_get_debounce_period_us(self->info));
+}
+
+static PyGetSetDef line_info_getset[] = {
+	{
+		.name = "offset",
+		.get = (getter)line_info_offset,
+		.doc = line_info_offset_doc,
+	},
+	{
+		.name = "name",
+		.get = (getter)line_info_name,
+		.doc = line_info_name_doc,
+	},
+	{
+		.name = "used",
+		.get = (getter)line_info_used,
+		.doc = line_info_used_doc,
+	},
+	{
+		.name = "consumer",
+		.get = (getter)line_info_consumer,
+		.doc = line_info_consumer_doc,
+	},
+	{
+		.name = "direction",
+		.get = (getter)line_info_direction,
+		.doc = line_info_direction_doc,
+	},
+	{
+		.name = "active_low",
+		.get = (getter)line_info_active_low,
+		.doc = line_info_active_low_doc,
+	},
+	{
+		.name = "bias",
+		.get = (getter)line_info_bias,
+		.doc = line_info_bias_doc,
+	},
+	{
+		.name = "drive",
+		.get = (getter)line_info_drive,
+		.doc = line_info_drive_doc,
+	},
+	{
+		.name = "edge_detection",
+		.get = (getter)line_info_edge_detection,
+		.doc = line_info_edge_detection_doc,
+	},
+	{
+		.name = "event_clock",
+		.get = (getter)line_info_event_clock,
+		.doc = line_info_event_clock_doc,
+	},
+	{
+		.name = "debounced",
+		.get = (getter)line_info_debounced,
+		.doc = line_info_debounced_doc,
+	},
+	{
+		.name = "debounce_period",
+		.get = (getter)line_info_debounce_period,
+		.doc = line_info_debounce_period_doc,
+	},
+	{ }
+};
+
+static PyObject *line_info_str(PyObject *self)
+{
+	PyObject *offset, *name, *used, *consumer, *direction, *active_low,
+		 *bias, *drive, *edge_detection, *event_clock, *debounced,
+		 *debounce_period, *str = NULL;
+
+	offset = PyObject_GetAttrString(self, "offset");
+	name = PyObject_GetAttrString(self, "name");
+	used = PyObject_GetAttrString(self, "used");
+	consumer = PyObject_GetAttrString(self, "consumer");
+	direction = PyObject_GetAttrString(self, "direction");
+	active_low = PyObject_GetAttrString(self, "active_low");
+	bias = PyObject_GetAttrString(self, "bias");
+	drive = PyObject_GetAttrString(self, "drive");
+	edge_detection = PyObject_GetAttrString(self, "edge_detection");
+	event_clock = PyObject_GetAttrString(self, "event_clock");
+	debounced = PyObject_GetAttrString(self, "debounced");
+	debounce_period = PyObject_GetAttrString(self, "debounce_period");
+	if (!offset || !name || !used || !consumer || !direction ||
+	    !active_low || !bias || !drive || !edge_detection || !event_clock ||
+	    !debounced || !debounce_period)
+		goto out;
+
+	str = PyUnicode_FromFormat(
+"<gpiod.LineInfo offset=%S name=\"%S\" used=%S consumer=\"%S\" direction=%S active_low=%S bias=%S drive=%S edge_detection=%S event_clock=%S debounced=%S debounce_period=%S>",
+offset, name, used, consumer, direction, active_low, bias, drive, edge_detection, event_clock, debounced, debounce_period);
+
+out:
+	Py_XDECREF(offset);
+	Py_XDECREF(name);
+	Py_XDECREF(used);
+	Py_XDECREF(consumer);
+	Py_XDECREF(direction);
+	Py_XDECREF(active_low);
+	Py_XDECREF(bias);
+	Py_XDECREF(drive);
+	Py_XDECREF(edge_detection);
+	Py_XDECREF(event_clock);
+	Py_XDECREF(debounced);
+	Py_XDECREF(debounce_period);
+	return str;
+}
+
+PyDoc_STRVAR(line_info_type_doc,
+"Line info object contains an immutable snapshot of a line's status.");
+
+static PyTypeObject line_info_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.LineInfo",
+	.tp_basicsize = sizeof(line_info_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = line_info_type_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)line_info_init,
+	.tp_finalize = (destructor)line_info_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = line_info_getset,
+	.tp_str = (reprfunc)line_info_str
+};
+
+int Py_gpiod_RegisterLineInfoType(PyObject *module)
+{
+	return PyModule_AddType(module, &line_info_type);
+}
+
+PyObject *Py_gpiod_MakeLineInfo(struct gpiod_line_info *info)
+{
+	line_info_object *info_obj;
+
+	info_obj = PyObject_New(line_info_object, &line_info_type);
+	if (!info_obj)
+		return NULL;
+
+	info_obj->info = info;
+
+	return (PyObject *)info_obj;
+}
diff --git a/bindings/python/line-request.c b/bindings/python/line-request.c
new file mode 100644
index 0000000..f3f4de9
--- /dev/null
+++ b/bindings/python/line-request.c
@@ -0,0 +1,713 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "module.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_line_request *request;
+} line_request_object;
+
+static int line_request_init(PyObject *Py_UNUSED(self),
+			     PyObject *Py_UNUSED(ignored0),
+			     PyObject *Py_UNUSED(ignored1))
+{
+	PyErr_SetString(PyExc_TypeError,
+			"cannot create 'gpiod.LineRequest' instances");
+	return -1;
+}
+
+static bool line_request_released(line_request_object *self)
+{
+	return !self->request;
+}
+
+static bool line_request_check_released(line_request_object *self)
+{
+	if (line_request_released(self)) {
+		Py_gpiod_SetRequestReleasedError();
+		return true;
+	}
+
+	return false;
+}
+
+static void line_request_finalize(line_request_object *self)
+{
+	if (!line_request_released(self))
+		PyObject_CallMethod((PyObject *)self, "release", "");
+}
+
+PyDoc_STRVAR(line_request_fd_doc,
+"Number of the file descriptor associated with this request.");
+
+static PyObject *
+line_request_fd(line_request_object *self, void *Py_UNUSED(ignored))
+{
+	if (line_request_check_released(self))
+		return NULL;
+
+	return PyLong_FromLong(gpiod_line_request_get_fd(self->request));
+}
+
+PyDoc_STRVAR(line_request_num_lines_doc, "Number of requested lines.");
+
+static PyObject *
+line_request_num_lines(line_request_object *self, void *Py_UNUSED(ignored))
+{
+	if (line_request_check_released(self))
+		return NULL;
+
+	return PyLong_FromSize_t(
+			gpiod_line_request_get_num_lines(self->request));
+}
+
+PyDoc_STRVAR(line_request_offsets_doc, "Offsets of the lines in the request.");
+
+static PyObject *
+line_request_offsets(line_request_object *self, void *Py_UNUSED(ignored))
+{
+	PyObject *offset_list, *offset_obj;
+	size_t num_offsets, i;
+	unsigned int *offsets;
+	int ret;
+
+	if (line_request_check_released(self))
+		return NULL;
+
+	num_offsets = gpiod_line_request_get_num_lines(self->request);
+
+	offsets = PyMem_Calloc(num_offsets, sizeof(unsigned int));
+	if (!offsets) {
+		PyErr_NoMemory();
+		return NULL;
+	}
+
+	gpiod_line_request_get_offsets(self->request, offsets);
+
+	offset_list = PyList_New(num_offsets);
+	if (!offset_list) {
+		PyMem_Free(offsets);
+		return NULL;
+	}
+
+	for (i = 0; i < num_offsets; i++) {
+		offset_obj = PyLong_FromUnsignedLong(offsets[i]);
+		if (!offset_obj) {
+			Py_DECREF(offset_list);
+			PyMem_Free(offsets);
+			return NULL;
+		}
+
+		ret = PyList_SetItem(offset_list, i, offset_obj);
+		if (ret) {
+			Py_DECREF(offset_obj);
+			Py_DECREF(offset_list);
+			PyMem_Free(offsets);
+			return NULL;
+		}
+	}
+
+	PyMem_Free(offsets);
+
+	return offset_list;
+}
+
+static PyGetSetDef line_request_getset[] = {
+	{
+		.name = "fd",
+		.get = (getter)line_request_fd,
+		.doc = line_request_fd_doc,
+	},
+	{
+		.name = "num_lines",
+		.get = (getter)line_request_num_lines,
+		.doc = line_request_num_lines_doc,
+	},
+	{
+		.name = "offsets",
+		.get = (getter)line_request_offsets,
+		.doc = line_request_offsets_doc,
+	},
+	{ }
+};
+
+PyDoc_STRVAR(line_request_enter_doc,
+"Controlled execution enter callback.");
+
+static PyObject *
+line_request_enter(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+	if (PyObject_Not(self)) {
+		Py_gpiod_SetRequestReleasedError();
+		return NULL;
+	}
+
+	Py_INCREF(self);
+	return self;
+}
+
+PyDoc_STRVAR(line_request_exit_doc,
+"Controlled execution exit callback.");
+
+static PyObject *line_request_exit(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+	return PyObject_CallMethod(self, "release", "");
+}
+
+PyDoc_STRVAR(line_request_release_doc,
+"release() -> None\n"
+"\n"
+"Close the associated request file descriptor. The request object must no\n"
+"longer be used after this method is called.");
+
+static PyObject *
+line_request_release(line_request_object *self, PyObject *Py_UNUSED(ignored))
+{
+	if (line_request_check_released(self))
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	gpiod_line_request_release(self->request);
+	Py_END_ALLOW_THREADS;
+	self->request = NULL;
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(line_request_get_value_doc,
+"get_value(offset) -> gpiod.Line.Value\n"
+"\n"
+"Get a single line value.");
+
+static PyObject *
+line_request_get_value(line_request_object *self, PyObject *args)
+{
+	unsigned int offset;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "I", &offset);
+	if (!ret)
+		return NULL;
+
+	return PyObject_CallMethod((PyObject *)self, "get_values", "I", offset);
+}
+
+PyDoc_STRVAR(line_request_get_values_doc,
+"get_values(offset(s)) -> value|[values]\n"
+"\n"
+"Get the values of all or a subset of requested lines");
+
+static PyObject *
+line_request_get_values(line_request_object *self, PyObject *args)
+{
+	PyObject *offsets_obj = NULL, *values_obj, *val, *offset;
+	Py_ssize_t num_values, i;
+	unsigned int *offsets;
+	int ret, *values;
+
+	if (line_request_check_released(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "|O", &offsets_obj);
+	if (!ret)
+		return NULL;
+
+	if (!offsets_obj) {
+		num_values = gpiod_line_request_get_num_lines(self->request);
+	} else if (PyLong_Check(offsets_obj)) {
+		num_values = 1;
+	} else if (PyList_Check(offsets_obj)) {
+		num_values = PyList_Size(offsets_obj);
+		if (num_values < 0)
+			return NULL;
+	} else {
+		PyErr_SetString(PyExc_TypeError,
+				"offsets must be either a single integer or a list of integers");
+		return NULL;
+	}
+
+	offsets = PyMem_Calloc(num_values, sizeof(unsigned int));
+	if (!offsets)
+		return NULL;
+
+	values = PyMem_Calloc(num_values, sizeof(int));
+	if (!values) {
+		PyMem_Free(offsets);
+		return NULL;
+	}
+
+	if (!offsets_obj) {
+		gpiod_line_request_get_offsets(self->request, offsets);
+	} else if (num_values == 1) {
+		offsets[0] = Py_gpiod_PyLongAsUnsignedInt(offsets_obj);
+		if (PyErr_Occurred()) {
+			PyMem_Free(values);
+			PyMem_Free(offsets);
+			return NULL;
+		}
+	} else {
+		for (i = 0; i < num_values; i++) {
+			offset = PyList_GetItem(offsets_obj, i);
+			if (!offset) {
+				PyMem_Free(values);
+				PyMem_Free(offsets);
+				return NULL;
+			}
+
+			offsets[i] = Py_gpiod_PyLongAsUnsignedInt(offset);
+			if (PyErr_Occurred()){
+				PyMem_Free(values);
+				PyMem_Free(offsets);
+				return NULL;
+			}
+		}
+	}
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_get_values_subset(self->request, num_values,
+						   offsets, values);
+	Py_END_ALLOW_THREADS;
+	PyMem_Free(offsets);
+	if (ret) {
+		PyMem_Free(values);
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	if (num_values == 1) {
+		values_obj = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_VALUE,
+						       values[0]);
+		if (!values_obj) {
+			PyMem_Free(values);
+			return NULL;
+		}
+	} else {
+		values_obj = PyList_New(num_values);
+		if (!values_obj) {
+			PyMem_Free(values);
+			return NULL;
+		}
+
+		for (i = 0; i < num_values; i++) {
+			val = Py_gpiod_MapLinePropCToPy(PY_GPIOD_LINE_VALUE,
+							values[i]);
+			if (!val) {
+				Py_DECREF(values_obj);
+				PyMem_Free(values);
+				return NULL;
+			}
+
+			ret = PyList_SetItem(values_obj, i, val);
+			if (ret) {
+				Py_DECREF(val);
+				Py_DECREF(values_obj);
+				PyMem_Free(values);
+				return NULL;
+			}
+		}
+	}
+
+	PyMem_Free(values);
+
+	return values_obj;
+}
+
+PyDoc_STRVAR(line_request_set_value_doc,
+"set_value(offset, value) -> None\n"
+"\n"
+"Set value of a single line.");
+
+static PyObject *
+line_request_set_value(line_request_object *self, PyObject *args)
+{
+	PyObject *offset, *value, *dict, *result;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "OO", &offset, &value);
+	if (!ret)
+		return NULL;
+
+	dict = PyDict_New();
+	if (!dict)
+		return NULL;
+
+	ret = PyDict_SetItem(dict, offset, value);
+	if (ret)
+		return NULL;
+
+	result = PyObject_CallMethod((PyObject *)self, "set_values", "O", dict);
+	Py_DECREF(dict);
+	return result;
+}
+
+PyDoc_STRVAR(line_request_set_values_doc,
+"set_values({offset: value}) -> None\n"
+"\n"
+"Set the values of all or a subset of requested lines");
+
+static PyObject *
+line_request_set_values(line_request_object *self, PyObject *args)
+{
+	PyObject *valobj, *off, *val, *iter;
+	Py_ssize_t num_values, pos = 0;
+	unsigned int *offsets;
+	int *values;
+	int ret;
+
+	if (line_request_check_released(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "O", &valobj);
+	if (!ret)
+		return NULL;
+
+	num_values = PyObject_Size(valobj);
+	if (num_values < 0)
+		return NULL;
+
+	values = PyMem_Calloc(num_values, sizeof(int));
+	if (!values) {
+		PyErr_NoMemory();
+		return NULL;
+	}
+
+	if (PyDict_Check(valobj)) {
+		offsets = PyMem_Calloc(num_values, sizeof(unsigned int));
+		if (!offsets) {
+			PyErr_NoMemory();
+			return NULL;
+		}
+
+		while (PyDict_Next(valobj, &pos, &off, &val)) {
+			offsets[pos - 1] = Py_gpiod_PyLongAsUnsignedInt(off);
+			if (PyErr_Occurred()) {
+				PyMem_Free(offsets);
+				PyMem_Free(values);
+				PyErr_NoMemory();
+				return NULL;
+			}
+
+			values[pos - 1] = Py_gpiod_MapLinePropPyToC(
+						PY_GPIOD_LINE_VALUE, val);
+			if (values[pos - 1] < 0) {
+				PyMem_Free(offsets);
+				PyMem_Free(values);
+				PyErr_NoMemory();
+				return NULL;
+			}
+		}
+
+		Py_BEGIN_ALLOW_THREADS;
+		ret = gpiod_line_request_set_values_subset(self->request,
+							   num_values,
+							   offsets, values);
+		Py_END_ALLOW_THREADS;
+		PyMem_Free(offsets);
+	} else if (PyList_Check(valobj)) {
+		if ((size_t)num_values != gpiod_line_request_get_num_lines(
+							self->request)) {
+			PyErr_SetString(PyExc_ValueError,
+					"list of values must be the same size as the number of requested lines");
+			PyMem_Free(values);
+			return NULL;
+		}
+
+		iter = PyObject_GetIter(valobj);
+		if (!iter) {
+			PyMem_Free(values);
+			return NULL;
+		}
+
+		for (pos = 0;; pos++) {
+			val = PyIter_Next(iter);
+			if (!val) {
+				Py_DECREF(iter);
+				break;
+			}
+
+			values[pos] = Py_gpiod_MapLinePropPyToC(
+						PY_GPIOD_LINE_VALUE, val);
+			Py_DECREF(val);
+			if (values[pos] < 0) {
+				PyMem_Free(values);
+				return NULL;
+			}
+		}
+
+		Py_BEGIN_ALLOW_THREADS;
+		ret = gpiod_line_request_set_values(self->request, values);
+		Py_END_ALLOW_THREADS;
+	}
+
+	PyMem_Free(values);
+	if (ret) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(line_request_reconfigure_lines_doc,
+"reconfigure_lines(line_cfg) -> None\n"
+"\n"
+"Update the configuration of lines associated with a line request\n"
+"\n"
+"  line_cfg\n"
+"    gpiod.LineConfig containing new configuration");
+
+static PyObject *
+line_request_reconfigure_lines(line_request_object *self, PyObject *args)
+{
+	struct gpiod_line_config *cfg;
+	PyObject *cfgobj;
+	int ret;
+
+	if (line_request_check_released(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "O", &cfgobj);
+	if (!ret)
+		return NULL;
+
+	cfg = Py_gpiod_LineConfigGetData(cfgobj);
+	if (!cfg)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_reconfigure_lines(self->request, cfg);
+	Py_END_ALLOW_THREADS;
+	if (ret) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(line_request_wait_edge_event_doc,
+"wait_edge_event(timeout) -> bool\n"
+"\n"
+"Wait for edge events on any of the requested lines.\n"
+"\n"
+"  timeout\n"
+"    datetime.timedelta containing the max time to wait for events");
+
+static PyObject *
+line_request_wait_edge_event(line_request_object *self, PyObject *args)
+{
+	PyObject *timedelta = NULL;
+	int64_t timeout_us = 0, timeout_ns;
+	int ret;
+
+	if (line_request_check_released(self))
+		return NULL;
+
+	ret = PyArg_ParseTuple(args, "|O", &timedelta);
+	if (!ret)
+		return NULL;
+
+	if (timedelta) {
+		timeout_us = Py_gpiod_TimedeltaToMicroseconds(timedelta);
+		if (PyErr_Occurred())
+			return NULL;
+	}
+
+	timeout_ns = timeout_us * 1000;
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_wait_edge_event(self->request, timeout_ns);
+	Py_END_ALLOW_THREADS;
+	if (ret < 0) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	return PyBool_FromLong(ret);
+}
+
+PyDoc_STRVAR(line_request_read_edge_event_doc,
+"read_edge_event(buffer, **kwargs) -> int\n"
+"\n"
+"Read a number of edge events from a line request.\n"
+"\n"
+"  buffer\n"
+"    gpiod.EdgeEventBuffer into which events will be read");
+
+static PyObject *
+line_request_read_edge_event(line_request_object *self,
+			     PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {
+		"",
+		"max_events",
+		NULL
+	};
+
+	struct gpiod_edge_event_buffer *buffer;
+	Py_ssize_t max_events;
+	PyObject *bufobj;
+	int ret;
+
+	if (line_request_check_released(self))
+		return NULL;
+
+	ret = PyArg_ParseTupleAndKeywords(args, kwargs, "O|n", kwlist,
+					  &bufobj, &max_events);
+	if (!ret)
+		return NULL;
+
+	buffer = Py_gpiod_EdgeEventBufferGetData(bufobj);
+	if (!buffer)
+		return NULL;
+
+	if (!max_events)
+		max_events = gpiod_edge_event_buffer_get_capacity(buffer);
+
+	Py_BEGIN_ALLOW_THREADS;
+	ret = gpiod_line_request_read_edge_event(self->request,
+						 buffer, max_events);
+	Py_END_ALLOW_THREADS;
+	if (ret < 0) {
+		Py_gpiod_SetErrFromErrno();
+		return NULL;
+	}
+
+	return PyLong_FromLong(ret);
+}
+
+static PyMethodDef line_request_methods[] = {
+	{
+		.ml_name = "__enter__",
+		.ml_meth = (PyCFunction)line_request_enter,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = line_request_enter_doc,
+	},
+	{
+		.ml_name = "__exit__",
+		.ml_meth = (PyCFunction)line_request_exit,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_request_exit_doc,
+	},
+	{
+		.ml_name = "release",
+		.ml_meth = (PyCFunction)line_request_release,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = line_request_release_doc,
+	},
+	{
+		.ml_name = "get_value",
+		.ml_meth = (PyCFunction)line_request_get_value,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_request_get_value_doc,
+	},
+	{
+		.ml_name = "get_values",
+		.ml_meth = (PyCFunction)line_request_get_values,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_request_get_values_doc,
+	},
+	{
+		.ml_name = "set_value",
+		.ml_meth = (PyCFunction)line_request_set_value,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_request_set_value_doc,
+	},
+	{
+		.ml_name = "set_values",
+		.ml_meth = (PyCFunction)line_request_set_values,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_request_set_values_doc,
+	},
+	{
+		.ml_name = "reconfigure_lines",
+		.ml_meth = (PyCFunction)line_request_reconfigure_lines,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_request_reconfigure_lines_doc,
+	},
+	{
+		.ml_name = "wait_edge_event",
+		.ml_meth = (PyCFunction)line_request_wait_edge_event,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = line_request_wait_edge_event_doc,
+	},
+	{
+		.ml_name = "read_edge_event",
+		.ml_meth = (PyCFunction)(void(*)(void))
+					line_request_read_edge_event,
+		.ml_flags = METH_VARARGS | METH_KEYWORDS,
+		.ml_doc = line_request_read_edge_event_doc,
+	},
+	{ }
+};
+
+static PyObject *line_request_str(PyObject *self)
+{
+	PyObject *num_lines, *offsets, *fd, *str = NULL;
+
+	if (PyObject_Not(self))
+		return PyUnicode_FromString("<gpiod.LineRequest RELEASED>");
+
+	num_lines = PyObject_GetAttrString(self, "num_lines");
+	offsets = PyObject_GetAttrString(self, "offsets");
+	fd = PyObject_GetAttrString(self, "fd");
+	if (!num_lines || !offsets || !fd)
+		goto out;
+
+	str = PyUnicode_FromFormat(
+			"<gpiod.LineRequest num_lines=%S offsets=%S fd=%S>",
+			num_lines, offsets, fd);
+
+out:
+	Py_XDECREF(num_lines);
+	Py_XDECREF(offsets);
+	Py_XDECREF(fd);
+	return str;
+}
+
+static int line_request_bool(line_request_object *self)
+{
+	return !line_request_released(self);
+}
+
+static PyNumberMethods line_request_number_methods = {
+	.nb_bool = (inquiry)line_request_bool,
+};
+
+PyDoc_STRVAR(line_request_doc,
+"Stores the context of a set of requested GPIO lines.");
+
+static PyTypeObject line_request_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.LineRequest",
+	.tp_basicsize = sizeof(line_request_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = line_request_doc,
+	.tp_as_number = &line_request_number_methods,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)line_request_init,
+	.tp_finalize = (destructor)line_request_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = line_request_getset,
+	.tp_methods = line_request_methods,
+	.tp_str = (reprfunc)line_request_str
+};
+
+int Py_gpiod_RegisterLineRequestType(PyObject *module)
+{
+	return PyModule_AddType(module, &line_request_type);
+}
+
+PyObject *Py_gpiod_MakeLineRequest(struct gpiod_line_request *req)
+{
+	line_request_object *req_obj;
+
+	req_obj = PyObject_New(line_request_object, &line_request_type);
+	if (!req_obj)
+		return NULL;
+
+	req_obj->request = req;
+
+	return (PyObject *)req_obj;
+}
diff --git a/bindings/python/line.c b/bindings/python/line.c
new file mode 100644
index 0000000..7003ab0
--- /dev/null
+++ b/bindings/python/line.c
@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "enum/enum.h"
+#include "module.h"
+
+static const PyCEnum_EnumVal value_enum_vals[] = {
+	{
+		.name = "INACTIVE",
+		.value = GPIOD_LINE_VALUE_INACTIVE
+	},
+	{
+		.name = "ACTIVE",
+		.value = GPIOD_LINE_VALUE_ACTIVE
+	},
+	{ }
+};
+
+static const PyCEnum_EnumVal direction_enum_vals[] = {
+	{
+		.name = "AS_IS",
+		.value = GPIOD_LINE_DIRECTION_AS_IS
+	},
+	{
+		.name = "INPUT",
+		.value = GPIOD_LINE_DIRECTION_INPUT
+	},
+	{
+		.name = "OUTPUT",
+		.value = GPIOD_LINE_DIRECTION_OUTPUT
+	},
+	{ }
+};
+
+static const PyCEnum_EnumVal bias_enum_vals[] = {
+	{
+		.name = "AS_IS",
+		.value = GPIOD_LINE_BIAS_AS_IS
+	},
+	{
+		.name = "UNKNOWN",
+		.value = GPIOD_LINE_BIAS_UNKNOWN
+	},
+	{
+		.name = "DISABLED",
+		.value = GPIOD_LINE_BIAS_DISABLED
+	},
+	{
+		.name = "PULL_UP",
+		.value = GPIOD_LINE_BIAS_PULL_UP
+	},
+	{
+		.name = "PULL_DOWN",
+		.value = GPIOD_LINE_BIAS_PULL_DOWN
+	},
+	{ }
+};
+
+static const PyCEnum_EnumVal drive_enum_vals[] = {
+	{
+		.name = "PUSH_PULL",
+		.value = GPIOD_LINE_DRIVE_PUSH_PULL
+	},
+	{
+		.name = "OPEN_DRAIN",
+		.value = GPIOD_LINE_DRIVE_OPEN_DRAIN
+	},
+	{
+		.name = "OPEN_SOURCE",
+		.value = GPIOD_LINE_DRIVE_OPEN_SOURCE
+	},
+	{ }
+};
+
+static const PyCEnum_EnumVal edge_enum_vals[] = {
+	{
+		.name = "NONE",
+		.value = GPIOD_LINE_EDGE_NONE
+	},
+	{
+		.name = "RISING",
+		.value = GPIOD_LINE_EDGE_RISING
+	},
+	{
+		.name = "FALLING",
+		.value = GPIOD_LINE_EDGE_FALLING
+	},
+	{
+		.name = "BOTH",
+		.value = GPIOD_LINE_EDGE_BOTH
+	},
+	{ }
+};
+
+static const PyCEnum_EnumVal event_clock_enum_vals[] = {
+	{
+		.name = "MONOTONIC",
+		.value = GPIOD_LINE_EVENT_CLOCK_MONOTONIC
+	},
+	{
+		.name = "REALTIME",
+		.value = GPIOD_LINE_EVENT_CLOCK_REALTIME
+	},
+	{ }
+};
+
+static const PyCEnum_EnumDef line_enums[] = {
+	{
+		.name = "Value",
+		.values = value_enum_vals
+	},
+	{
+		.name = "Direction",
+		.values = direction_enum_vals
+	},
+	{
+		.name = "Bias",
+		.values = bias_enum_vals
+	},
+	{
+		.name = "Drive",
+		.values = drive_enum_vals
+	},
+	{
+		.name = "Edge",
+		.values = edge_enum_vals
+	},
+	{
+		.name = "Clock",
+		.values = event_clock_enum_vals
+	},
+	{ }
+};
+
+PyDoc_STRVAR(line_type_doc,
+"Container for common definitions related to GPIO lines.\n");
+
+static PyTypeObject line_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.Line",
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = line_type_doc
+};
+
+int Py_gpiod_RegisterLineType(PyObject *module)
+{
+	int ret;
+
+	ret = PyType_Ready(&line_type);
+	if (ret)
+		return -1;
+
+	Py_INCREF(&line_type);
+	ret = PyModule_AddObject(module, "Line", (PyObject *)&line_type);
+	if (ret) {
+		Py_DECREF(&line_type);
+		return -1;
+	}
+
+	ret = PyCEnum_AddEnumsToType(line_enums, &line_type);
+	if (ret) {
+		Py_DECREF(&line_type);
+		return -1;
+	}
+
+	return 0;
+}
+
+static const char *get_enum_name(int prop)
+{
+	switch (prop) {
+	case PY_GPIOD_LINE_VALUE:
+		return "Value";
+	case PY_GPIOD_LINE_DIRECTION:
+		return "Direction";
+	case PY_GPIOD_LINE_EDGE:
+		return "Edge";
+	case PY_GPIOD_LINE_BIAS:
+		return "Bias";
+	case PY_GPIOD_LINE_DRIVE:
+		return "Drive";
+	case PY_GPIOD_LINE_CLOCK:
+		return "Clock";
+	}
+
+	PyErr_SetString(PyExc_ValueError, "unsupported line property");
+	return NULL;
+}
+
+static PyObject *get_line_type(void)
+{
+	PyObject *mod, *dict, *type;
+
+	mod = Py_gpiod_GetModule();
+	if (!mod)
+		return NULL;
+
+	dict = PyModule_GetDict(mod);
+	if (!dict)
+		return NULL;
+
+	type = PyDict_GetItemString(dict, "Line");
+	if (!type)
+		return NULL;
+
+	return type;
+}
+
+PyObject *Py_gpiod_MapLinePropCToPy(int prop, int value)
+{
+	const char *enum_name;
+	PyObject *type;
+
+	enum_name = get_enum_name(prop);
+	if (!enum_name)
+		return NULL;
+
+	type = get_line_type();
+	if (!type)
+		return NULL;
+
+	return PyCEnum_MapCToPy(type, enum_name, value);
+}
+
+int Py_gpiod_MapLinePropPyToC(int prop, PyObject *value)
+{
+	const char *enum_name;
+	PyObject *type;
+
+	enum_name = get_enum_name(prop);
+	if (!enum_name)
+		return -1;
+
+	type = get_line_type();
+	if (!type)
+		return -1;
+
+	return PyCEnum_MapPyToC(type, enum_name, value);
+}
diff --git a/bindings/python/module.c b/bindings/python/module.c
new file mode 100644
index 0000000..2175426
--- /dev/null
+++ b/bindings/python/module.c
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <errno.h>
+#include <limits.h>
+
+#include "module.h"
+
+/* Generic dealloc callback for all gpiod objects. */
+void Py_gpiod_dealloc(PyObject *self)
+{
+	int ret;
+
+	ret = PyObject_CallFinalizerFromDealloc(self);
+	if (ret < 0)
+		return;
+
+	PyObject_Del(self);
+}
+
+PyDoc_STRVAR(module_is_gpiochip_device_doc,
+"is_gpiochip_device(path) -> boolean\n"
+"\n"
+"Check if the file pointed to by path is a GPIO chip character device.\n"
+"Returns true if so, False otherwise.\n"
+"\n"
+"  path\n"
+"    Path to the file that should be checked.\n");
+
+static PyObject *
+module_is_gpiochip_device(PyObject *Py_UNUSED(self), PyObject *args)
+{
+	const char *path;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "s", &path);
+	if (!ret)
+		return NULL;
+
+	return PyBool_FromLong(gpiod_is_gpiochip_device(path));
+}
+
+PyDoc_STRVAR(module_request_lines_doc,
+"request_lines(path, req_cfg, line_cfg) -> gpiod.LineRequest\n"
+"\n"
+"Open a GPIO chip indicated by path, request a set of lines for exclusive\n"
+"usage and close the chip.\n"
+"\n"
+"  path\n"
+"    Path to the GPIO chip character device\n"
+"  req_cfg\n"
+"    Request config object\n"
+"  line_cfg\n"
+"    Line config object");
+
+static PyObject *module_request_lines(PyObject *self, PyObject *args)
+{
+	PyObject *path, *req_cfg, *line_cfg, *dict, *type, *chip, *req,
+		 *errtype, *errvalue, *errtraceback;
+	int ret;
+
+	ret = PyArg_ParseTuple(args, "OOO", &path, &req_cfg, &line_cfg);
+	if (!ret)
+		return NULL;
+
+	dict = PyModule_GetDict(self);
+	if (!dict)
+		return NULL;
+
+	type = PyDict_GetItemString(dict, "Chip");
+	if (!type)
+		return NULL;
+
+	chip = PyObject_CallOneArg(type, path);
+	if (!chip)
+		return NULL;
+
+	req = PyObject_CallMethod(chip, "request_lines",
+				  "OO", req_cfg, line_cfg);
+	PyErr_Fetch(&errtype, &errvalue, &errtraceback);
+	PyObject_CallMethod(chip, "close", NULL);
+	PyErr_Restore(errtype, errvalue, errtraceback);
+	Py_DECREF(chip);
+	return req;
+}
+
+static PyMethodDef module_methods[] = {
+	{
+		.ml_name = "is_gpiochip_device",
+		.ml_meth = (PyCFunction)module_is_gpiochip_device,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = module_is_gpiochip_device_doc,
+	},
+	{
+		.ml_name = "request_lines",
+		.ml_meth = (PyCFunction)module_request_lines,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = module_request_lines_doc,
+	},
+	{ }
+};
+
+struct module_state {
+	PyObject *timedelta_type;
+};
+
+static void free_module_state(void *mod)
+{
+	struct module_state *state = PyModule_GetState((PyObject *)mod);
+
+	Py_XDECREF(state->timedelta_type);
+}
+
+PyDoc_STRVAR(module_doc,
+"Python bindings for libgpiod.\n\
+\n\
+This module wraps the native C API of libgpiod in a set of python classes.");
+
+static PyModuleDef module_def = {
+	PyModuleDef_HEAD_INIT,
+	.m_name = "gpiod",
+	.m_doc = module_doc,
+	.m_size = sizeof(struct module_state),
+	.m_free = free_module_state,
+	.m_methods = module_methods,
+};
+
+typedef int (*register_func)(PyObject *);
+
+static const register_func register_type_funcs[] = {
+	Py_gpiod_RegisterChipType,
+	Py_gpiod_RegisterChipInfoType,
+	Py_gpiod_RegisterEdgeEventType,
+	Py_gpiod_RegisterEdgeEventBufferType,
+	Py_gpiod_RegisterExceptionTypes,
+	Py_gpiod_RegisterInfoEventType,
+	Py_gpiod_RegisterLineConfigType,
+	Py_gpiod_RegisterLineType,
+	Py_gpiod_RegisterLineInfoType,
+	Py_gpiod_RegisterLineRequestType,
+	Py_gpiod_RegisterRequestConfigType,
+};
+
+static int init_timedelta_type(struct module_state *state)
+{
+	PyObject *datetime;
+
+	datetime = PyImport_ImportModule("datetime");
+	if (!datetime)
+		return -1;
+
+	state->timedelta_type = PyObject_GetAttrString(datetime, "timedelta");
+	Py_DECREF(datetime);
+	if (!state->timedelta_type)
+		return -1;
+
+	return 0;
+}
+
+PyMODINIT_FUNC PyInit_gpiod(void)
+{
+	struct module_state *state;
+	size_t num_funcs, i;
+	PyObject *module;
+	int ret;
+
+	module = PyModule_Create(&module_def);
+	if (!module)
+		return NULL;
+
+	ret = PyState_AddModule(module, &module_def);
+	if (ret) {
+		Py_DECREF(module);
+		return NULL;
+	}
+
+	state = PyModule_GetState(module);
+
+	ret = init_timedelta_type(state);
+	if (ret) {
+		Py_DECREF(module);
+		return NULL;
+	}
+
+	num_funcs = sizeof(register_type_funcs) / sizeof(*register_type_funcs);
+	for (i = 0; i < num_funcs; i++) {
+		ret = register_type_funcs[i](module);
+		if (ret) {
+			Py_DECREF(module);
+			return NULL;
+		}
+	}
+
+	ret = PyModule_AddStringConstant(module, "__version__",
+					 gpiod_version_string());
+	if (ret < 0) {
+		Py_DECREF(module);
+		return NULL;
+	}
+
+	return module;
+}
+
+PyObject *Py_gpiod_GetModule(void)
+{
+	return PyState_FindModule(&module_def);
+}
+
+PyObject *Py_gpiod_MicrosecondsToTimedelta(unsigned long us)
+{
+	PyObject *module, *timedelta, *args, *kwargs, *val;
+	struct module_state *state;
+	int ret;
+
+	module = PyState_FindModule(&module_def);
+	if (!module)
+		return NULL;
+
+	state = PyModule_GetState(module);
+
+	kwargs = PyDict_New();
+	if (!kwargs)
+		return NULL;
+
+	val = PyLong_FromUnsignedLong(us);
+	if (!val) {
+		Py_DECREF(kwargs);
+		return NULL;
+	}
+
+	ret = PyDict_SetItemString(kwargs, "microseconds", val);
+	Py_DECREF(val);
+	if (ret) {
+		Py_DECREF(kwargs);
+		return NULL;
+	}
+
+	args = PyTuple_New(0);
+	if (!args) {
+		Py_DECREF(kwargs);
+		return NULL;
+	}
+
+	timedelta = PyObject_Call(state->timedelta_type, args, kwargs);
+	Py_DECREF(args);
+	Py_DECREF(kwargs);
+	return timedelta;
+}
+
+unsigned long Py_gpiod_TimedeltaToMicroseconds(PyObject *timedelta)
+{
+	PyObject *total_seconds;
+	double val;
+
+	total_seconds = PyObject_CallMethod(timedelta, "total_seconds", NULL);
+	if (!total_seconds)
+		return 0;
+
+	val = PyFloat_AsDouble(total_seconds);
+	Py_DECREF(total_seconds);
+	if (PyErr_Occurred())
+		return 0;
+
+	return val * 1000000;
+}
+
+unsigned int Py_gpiod_PyLongAsUnsignedInt(PyObject *pylong)
+{
+	unsigned long tmp;
+
+	tmp = PyLong_AsUnsignedLong(pylong);
+	if (PyErr_Occurred())
+		return 0;
+
+	if (tmp > UINT_MAX) {
+		PyErr_SetString(PyExc_ValueError, "value exceeding UINT_MAX");
+		return 0;
+	}
+
+	return tmp;
+}
diff --git a/bindings/python/module.h b/bindings/python/module.h
new file mode 100644
index 0000000..6a135d5
--- /dev/null
+++ b/bindings/python/module.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __LIBGPIOD_PYTHON_MODULE_H__
+#define __LIBGPIOD_PYTHON_MODULE_H__
+
+#include <gpiod.h>
+#include <Python.h>
+
+PyObject *Py_gpiod_GetModule(void);
+void Py_gpiod_dealloc(PyObject *self);
+PyObject *Py_gpiod_MicrosecondsToTimedelta(unsigned long us);
+unsigned long Py_gpiod_TimedeltaToMicroseconds(PyObject *timedelta);
+unsigned int Py_gpiod_PyLongAsUnsignedInt(PyObject *pylong);
+
+enum {
+	PY_GPIOD_LINE_VALUE = 1,
+	PY_GPIOD_LINE_DIRECTION,
+	PY_GPIOD_LINE_EDGE,
+	PY_GPIOD_LINE_BIAS,
+	PY_GPIOD_LINE_DRIVE,
+	PY_GPIOD_LINE_CLOCK,
+};
+
+PyObject *Py_gpiod_MapLinePropCToPy(int prop, int value);
+int Py_gpiod_MapLinePropPyToC(int prop, PyObject *value);
+
+PyObject *_Py_gpiod_SetErrFromErrno(const char *filename);
+#define Py_gpiod_SetErrFromErrno() _Py_gpiod_SetErrFromErrno(__FILE__)
+
+PyObject *Py_gpiod_MakeChipInfo(struct gpiod_chip_info *info);
+PyObject *Py_gpiod_MakeEdgeEvent(struct gpiod_edge_event *event);
+PyObject *Py_gpiod_MakeInfoEvent(struct gpiod_info_event *event);
+PyObject *Py_gpiod_MakeLineInfo(struct gpiod_line_info *info);
+PyObject *Py_gpiod_MakeLineRequest(struct gpiod_line_request *req);
+
+int Py_gpiod_RegisterChipType(PyObject *module);
+int Py_gpiod_RegisterChipInfoType(PyObject *module);
+int Py_gpiod_RegisterEdgeEventType(PyObject *module);
+int Py_gpiod_RegisterEdgeEventBufferType(PyObject *module);
+int Py_gpiod_RegisterExceptionTypes(PyObject *module);
+int Py_gpiod_RegisterInfoEventType(PyObject *module);
+int Py_gpiod_RegisterLineConfigType(PyObject *module);
+int Py_gpiod_RegisterLineType(PyObject *module);
+int Py_gpiod_RegisterLineInfoType(PyObject *module);
+int Py_gpiod_RegisterLineRequestType(PyObject *module);
+int Py_gpiod_RegisterRequestConfigType(PyObject *module);
+
+void Py_gpiod_SetChipClosedError(void);
+void Py_gpiod_SetRequestReleasedError(void);
+void Py_gpiod_SetBadMappingError(const char *name);
+
+struct gpiod_edge_event_buffer *Py_gpiod_EdgeEventBufferGetData(PyObject *obj);
+struct gpiod_line_config *Py_gpiod_LineConfigGetData(PyObject *obj);
+struct gpiod_request_config *Py_gpiod_RequestConfigGetData(PyObject *obj);
+
+#endif /* __LIBGPIOD_PYTHON_MODULE_H__ */
diff --git a/bindings/python/request-config.c b/bindings/python/request-config.c
new file mode 100644
index 0000000..3e45847
--- /dev/null
+++ b/bindings/python/request-config.c
@@ -0,0 +1,320 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "module.h"
+#include "enum/enum.h"
+
+typedef struct {
+	PyObject_HEAD;
+	struct gpiod_request_config *cfg;
+} request_config_object;
+
+static int set_offsets(struct gpiod_request_config *cfg, PyObject *offsets_obj)
+{
+	PyObject *iter, *item;
+	unsigned int *offsets;
+	Py_ssize_t len;
+	int i;
+
+	len = PyObject_Size(offsets_obj);
+	if (len < 0)
+		return -1;
+
+	if (len == 0) {
+		gpiod_request_config_set_offsets(cfg, 0, NULL);
+		return 0;
+	}
+
+	offsets = PyMem_Calloc(len, sizeof(unsigned int));
+	if (!offsets) {
+		PyErr_NoMemory();
+		return -1;
+	}
+
+	iter = PyObject_GetIter(offsets_obj);
+	if (!iter) {
+		PyMem_Free(offsets);
+		return -1;
+	}
+
+	for (i = 0;; i++) {
+		item = PyIter_Next(iter);
+		if (!item) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		offsets[i] = PyLong_AsUnsignedLong(item);
+		Py_DECREF(item);
+		if (PyErr_Occurred()) {
+			PyMem_Free(offsets);
+			Py_DECREF(iter);
+			return -1;
+		}
+	}
+
+	gpiod_request_config_set_offsets(cfg, len, offsets);
+	PyMem_Free(offsets);
+
+	return 0;
+}
+
+static int request_config_init(request_config_object *self,
+			       PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {
+		"consumer",
+		"offsets",
+		"event_buffer_size",
+		NULL
+	};
+
+	PyObject *event_buffer_size = NULL, *offsets = NULL;
+	char *consumer = NULL;
+	size_t evbufsiz;
+	int ret;
+
+	ret = PyArg_ParseTupleAndKeywords(args, kwargs, "|$sOO", kwlist,
+					  &consumer, &offsets,
+					  &event_buffer_size);
+	if (!ret)
+		return -1;
+
+	self->cfg = gpiod_request_config_new();
+	if (!self->cfg) {
+		Py_gpiod_SetErrFromErrno();
+		return -1;
+	}
+
+	if (consumer)
+		gpiod_request_config_set_consumer(self->cfg, consumer);
+
+	if (offsets) {
+		ret = set_offsets(self->cfg, offsets);
+		if (ret)
+			return -1;
+	}
+
+	if (event_buffer_size) {
+		evbufsiz = PyLong_AsSize_t(event_buffer_size);
+		if (PyErr_Occurred())
+			return -1;
+
+		gpiod_request_config_set_event_buffer_size(self->cfg, evbufsiz);
+	}
+
+	return 0;
+}
+
+static void request_config_finalize(request_config_object *self)
+{
+	if (self->cfg)
+		gpiod_request_config_free(self->cfg);
+}
+
+PyDoc_STRVAR(request_config_prop_consumer_doc,
+"Consumer name for the request.");
+
+static PyObject *request_config_get_consumer(request_config_object *self,
+					     void *Py_UNUSED(ignored))
+{
+	const char *consumer;
+
+	consumer = gpiod_request_config_get_consumer(self->cfg);
+	if (!consumer)
+		Py_RETURN_NONE;
+
+	return PyUnicode_FromString(consumer);
+}
+
+static int request_config_set_consumer(request_config_object *self,
+				       PyObject *val, void *Py_UNUSED(ignored))
+{
+	const char *consumer;
+
+	consumer = PyUnicode_AsUTF8(val);
+	if (!consumer)
+		return -1;
+
+	gpiod_request_config_set_consumer(self->cfg, consumer);
+
+	return 0;
+}
+
+PyDoc_STRVAR(request_config_prop_offsets_doc,
+"Offsets of the lines to be requested.");
+
+static PyObject *request_config_get_offsets(request_config_object *self,
+					    void *Py_UNUSED(ignored))
+{
+	unsigned int *offsets, i;
+	PyObject *list, *item;
+	size_t num_offsets;
+	int ret;
+
+	num_offsets = gpiod_request_config_get_num_offsets(self->cfg);
+	if (num_offsets == 0)
+		Py_RETURN_NONE;
+
+	offsets = PyMem_Calloc(num_offsets, sizeof(unsigned int));
+	if (!offsets) {
+		PyErr_NoMemory();
+		return NULL;
+	}
+
+	gpiod_request_config_get_offsets(self->cfg, offsets);
+
+	list = PyList_New(num_offsets);
+	if (!list) {
+		PyMem_Free(offsets);
+		return NULL;
+	}
+
+	for (i = 0; i < num_offsets; i++) {
+		item = PyLong_FromUnsignedLong(offsets[i]);
+		if (!item) {
+			Py_DECREF(list);
+			PyMem_Free(offsets);
+			return NULL;
+		}
+
+		ret = PyList_SetItem(list, i, item);
+		if (ret) {
+			Py_DECREF(item);
+			Py_DECREF(list);
+			PyMem_Free(offsets);
+			return NULL;
+		}
+	}
+
+	PyMem_Free(offsets);
+	return list;
+}
+
+static int request_config_set_offsets(request_config_object *self,
+				      PyObject *val, void *Py_UNUSED(ignored))
+{
+	return set_offsets(self->cfg, val);
+}
+
+PyDoc_STRVAR(request_config_prop_event_buffer_size_doc,
+"Size of the kernel event buffer for the request.");
+
+static PyObject *
+request_config_get_event_buffer_size(request_config_object *self,
+				     void *Py_UNUSED(ignored))
+{
+	return PyLong_FromSize_t(
+			gpiod_request_config_get_event_buffer_size(self->cfg));
+}
+
+static int request_config_set_event_buffer_size(request_config_object *self,
+				       PyObject *val, void *Py_UNUSED(ignored))
+{
+	size_t event_buffer_size;
+
+	event_buffer_size = PyLong_AsSize_t(val);
+	if (PyErr_Occurred())
+		return -1;
+
+	gpiod_request_config_set_event_buffer_size(self->cfg,
+						   event_buffer_size);
+
+	return 0;
+}
+
+static PyGetSetDef request_config_getset[] = {
+	{
+		.name = "consumer",
+		.get = (getter)request_config_get_consumer,
+		.set = (setter)request_config_set_consumer,
+		.doc = request_config_prop_consumer_doc,
+	},
+	{
+		.name = "offsets",
+		.get = (getter)request_config_get_offsets,
+		.set = (setter)request_config_set_offsets,
+		.doc = request_config_prop_offsets_doc,
+	},
+	{
+		.name = "event_buffer_size",
+		.get = (getter)request_config_get_event_buffer_size,
+		.set = (setter)request_config_set_event_buffer_size,
+		.doc = request_config_prop_event_buffer_size_doc,
+	},
+	{ }
+};
+
+static PyObject *make_str(PyObject *self, const char *fmt)
+{
+	PyObject *consumer, *offsets, *event_buffer_size, *str = NULL;
+
+	consumer = PyObject_GetAttrString(self, "consumer");
+	offsets = PyObject_GetAttrString(self, "offsets");
+	event_buffer_size = PyObject_GetAttrString(self, "event_buffer_size");
+	if (!consumer || !offsets || !event_buffer_size)
+		goto out;
+
+	str = PyUnicode_FromFormat(fmt, consumer, offsets, event_buffer_size);
+
+out:
+	Py_XDECREF(consumer);
+	Py_XDECREF(offsets);
+	Py_XDECREF(event_buffer_size);
+	return str;
+}
+
+static PyObject *request_config_repr(PyObject *self)
+{
+	return make_str(self, "gpiod.RequestConfig(consumer=\"%S\", offsets=%S, event_buffer_size=%S)");
+}
+
+static PyObject *request_config_str(PyObject *self)
+{
+	return make_str(self, "<gpiod.RequestConfig consumer=\"%S\" offsets=%S event_buffer_size=%S>");
+}
+
+PyDoc_STRVAR(request_config_type_doc,
+"Stores a set of options passed to the kernel when making a line request.");
+
+static PyTypeObject request_config_type = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.RequestConfig",
+	.tp_basicsize = sizeof(request_config_object),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = request_config_type_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)request_config_init,
+	.tp_finalize = (destructor)request_config_finalize,
+	.tp_dealloc = (destructor)Py_gpiod_dealloc,
+	.tp_getset = request_config_getset,
+	.tp_repr = (reprfunc)request_config_repr,
+	.tp_str = (reprfunc)request_config_str
+};
+
+int Py_gpiod_RegisterRequestConfigType(PyObject *module)
+{
+	return PyModule_AddType(module, &request_config_type);
+}
+
+struct gpiod_request_config *Py_gpiod_RequestConfigGetData(PyObject *obj)
+{
+	request_config_object *reqcfg;
+	PyObject *type;
+
+	type = PyObject_Type(obj);
+	if (!type)
+		return NULL;
+
+	if ((PyTypeObject *)type != &request_config_type) {
+		PyErr_SetString(PyExc_TypeError,
+				"not a gpiod.RequestConfig object");
+		Py_DECREF(type);
+		return NULL;
+	}
+	Py_DECREF(type);
+
+	reqcfg = (request_config_object *)obj;
+
+	return reqcfg->cfg;
+}
diff --git a/configure.ac b/configure.ac
index ab03673..7a794e2 100644
--- a/configure.ac
+++ b/configure.ac
@@ -198,7 +198,7 @@ AM_CONDITIONAL([WITH_BINDINGS_PYTHON], [test "x$with_bindings_python" = xtrue])
 
 if test "x$with_bindings_python" = xtrue
 then
-	AM_PATH_PYTHON([3.0], [],
+	AM_PATH_PYTHON([3.9], [],
 		[AC_MSG_ERROR([python3 not found - needed for python bindings])])
 	AC_CHECK_PROG([has_python_config], [python3-config], [true], [false])
 	if test "x$has_python_config" = xfalse
@@ -243,6 +243,7 @@ AC_CONFIG_FILES([Makefile
 		 bindings/cxx/examples/Makefile
 		 bindings/cxx/tests/Makefile
 		 bindings/python/Makefile
+		 bindings/python/enum/Makefile
 		 bindings/python/examples/Makefile
 		 bindings/python/tests/Makefile
 		 man/Makefile])
