diff --git a/.gitignore b/.gitignore
index 818b1860c3f968b2c4dc1f56cdfd311e98d3fe44..e653de55b5cddd83321df34349a06e3d277ace06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@
 **/pending_log_messages.db
 **/vscode-server.tar
 **/*.orig
+**/venv
 
 tangostationcontrol/build
 tangostationcontrol/cover
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3c78a212a309ec4d38aec121dc46212c220a63d7
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,23 @@
+default_stages: [push]
+
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.5.0
+    hooks:
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+      - id: check-yaml
+  - repo: local
+    hooks:
+      - id: tox-black
+        name: tox-black (local)
+        entry: tox
+        language: system
+        args: ["-c", "tangostationcontrol/tox.ini", "-e", "black", "--"]
+  - repo: local
+    hooks:
+      - id: tox-pep8
+        name: tox-pep8 (local)
+        entry: tox
+        language: system
+        args: ["-c", "tangostationcontrol/tox.ini", "-e", "pep8", "--"]
diff --git a/README.md b/README.md
index 25e9c4e6143e302ae7a63bc2c771727029d91566..c4d3f5338b40f0c1fe79d87b61d970ba19cc59fa 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,7 @@ Next change the version in the following places:
 
 # Release Notes
 
+* 0.31.1 Add pre-push git hooks for basic CI checks
 * 0.31.0 Poll attributes independently from Tango
 * 0.30.5 Log and count event subscription errors
 * 0.30.4 Fix Tango attribute parameter types
diff --git a/sbin/install-pre-commit.sh b/sbin/install-pre-commit.sh
new file mode 100644
index 0000000000000000000000000000000000000000..e589fa12613a0db53519fb76290fdf8b1dfb2808
--- /dev/null
+++ b/sbin/install-pre-commit.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+pip install pre-commit
+pre-commit install --hook-type pre-push --allow-missing-config
+
+# --allow-missing-config: Allows the installation to proceed
+# even if the configuration file is missing, and hooks won't be executed
+# without a configuration file.
+#
+# e.g. switching between branches who might have or not have pre-push config
+# files result in no annoying messages
+
+# To uninstall pre-commit:
+#
+# pre-commit uninstall
+# pip uninstall pre-commit
+#
+# You might want to remove also '.git/hooks/pre-push'
diff --git a/setup.sh b/setup.sh
index 646af626004bf7909386c4214060c81428573f54..867f2035326f9b3974616359bd014ca694852670 100755
--- a/setup.sh
+++ b/setup.sh
@@ -13,12 +13,27 @@ fi
 ABSOLUTE_PATH=$(realpath $(dirname ${BASH_SOURCE}))
 export LOFAR20_DIR=${1:-${ABSOLUTE_PATH}}
 
+# Create a virtual environment directory if it doesn't exist
+VENV_DIR="${LOFAR20_DIR}/venv"
+if [ ! -d "$VENV_DIR" ]; then
+    echo "Creating virtual environment..."
+    python3 -m venv "$VENV_DIR"
+fi
+
+# Activate the virtual environment
+source "$VENV_DIR/bin/activate"
+
 # Install git post-checkout hook upon next execution of git command
 # git alias eventually automatically uninstalled
 if [ ! -f "${LOFAR20_DIR}/.git/hooks/post-checkout" ]; then
   alias git="cp ${LOFAR20_DIR}/bin/update_submodules.sh ${LOFAR20_DIR}/.git/hooks/post-checkout; cp ${LOFAR20_DIR}/bin/update_submodules.sh ${LOFAR20_DIR}/.git/hooks/post-merge; unalias git; git"
 fi
 
+# Install git pre-push hook
+if [ ! -f "${LOFAR20_DIR}/.git/hooks/pre-push" ]; then
+  source "${LOFAR20_DIR}/sbin/install-pre-commit.sh"
+fi
+
 # It checks if Tango is running within nomad, by trying to query the service from consul.
 # If consul is not available it is assumed that the Tango host, the computer that runs the TangoDB,
 # is this host. If this is not true, then modify to the Tango host's FQDN and
diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION
index 26bea73e811981fe7a2a09a00c54f23943d309aa..f176c9441921f57eac9b70c203a2277a692e5288 100644
--- a/tangostationcontrol/VERSION
+++ b/tangostationcontrol/VERSION
@@ -1 +1 @@
-0.31.0
+0.31.1
diff --git a/tangostationcontrol/tangostationcontrol/clients/README.md b/tangostationcontrol/tangostationcontrol/clients/README.md
index e824f57a0adc315c2286ff3425dce1402735696d..6136ef88b02e82335784de8c565dcd7ea1ae2744 100644
--- a/tangostationcontrol/tangostationcontrol/clients/README.md
+++ b/tangostationcontrol/tangostationcontrol/clients/README.md
@@ -1,10 +1,10 @@
 # Attribute wrapper use guide
 
-The attribute wrapper is an abstraction layer around tango attributes. This abstraction layer provides an easier and more consistent way of creating and using attributes and allows for easy reuse of code. 
+The attribute wrapper is an abstraction layer around tango attributes. This abstraction layer provides an easier and more consistent way of creating and using attributes and allows for easy reuse of code.
 
 You can find example uses of the attribute wrapper inside the devices folder: https://git.astron.nl/lofar2.0/tango/-/tree/master/tangostationcontrol/tangostationcontrol/devices
 
-Inside lofar/tango/tangostationcontrol/tangostationcontrol/devices/lofar_device.py we import the attribute wrapper. Here we also create a dictionary containing all attribute values in the devices with the setup_value_dict method. This dictionary is set up when the device is initialized. This file, together with the opcua_client.py may be of interest as they are created as generic base classes. 
+Inside lofar/tango/tangostationcontrol/tangostationcontrol/devices/lofar_device.py we import the attribute wrapper. Here we also create a dictionary containing all attribute values in the devices with the setup_value_dict method. This dictionary is set up when the device is initialized. This file, together with the opcua_client.py may be of interest as they are created as generic base classes.
 
 ## Functions/methods
 `__init__`:
@@ -12,16 +12,16 @@ Inside lofar/tango/tangostationcontrol/tangostationcontrol/devices/lofar_device.
 	Comms_annotation:	: data passed along to the attribute. can be given any form of data. handling is up to client implementation
 	Datatype:		The numpy type of the attribute
 	Dims:			Tuple of the dimensions (x,) or (x,y)
-	Access:		AttrWriteType.READ or AttrWriteType.READWRITE	, determines whether this is a read or read/write function. 	
+	Access:		AttrWriteType.READ or AttrWriteType.READWRITE	, determines whether this is a read or read/write function.
 	Init_value:		Initialisation value. If none is presents, fills the attribute with zero  data of the correct type and dimension
 	**kwargs: any other non AttributeWrapper arguments.
-NOTE: the `__init__` function contains wrappers for the unassigned read/write functions. In previous versions the read function of an RW attribute used to return the last value it had written *to* the client  instead of the value from the client. This has since been changed.  
-	
+NOTE: the `__init__` function contains wrappers for the unassigned read/write functions. In previous versions the read function of an RW attribute used to return the last value it had written *to* the client  instead of the value from the client. This has since been changed.
+
 `set_comm_client`:
-This function can be called to assign a read and write function to the attribute using the data accessor or client given to this function. The attribute wrapper assumes the client is running and has a function called ‘setup_attribute’ which will provide it with a valid read/write function. 
+This function can be called to assign a read and write function to the attribute using the data accessor or client given to this function. The attribute wrapper assumes the client is running and has a function called ‘setup_attribute’ which will provide it with a valid read/write function.
 
 `async_set_comm_client`:
-	Aynchronous version of the set_comm_client function. 
+	Aynchronous version of the set_comm_client function.
 
 `set_pass_func`:
 	Can be called to assign a 'fake' read/write function. This is useful as a fallback option while development is still ongoing.
@@ -31,7 +31,7 @@ This function can be called to assign a read and write function to the attribute
 
 
 ## Example Device / usage
-Here an example of a Tango Device that uses the attribute wrapper is presented. 
+Here an example of a Tango Device that uses the attribute wrapper is presented.
 The device class is a sub-class of OPCUADevice, which is, in turn, a sub-class of lofar_device class which implements the attribute initialisation methods (attr_list and setup_value_dict ) as stated above.
 ```python
 class RECV(OPCUADevice):
@@ -39,13 +39,13 @@ class RECV(OPCUADevice):
     # ----------
     # Attributes
     # ----------
-    
+
     # Scalar attribute
     RECVTR_translator_busy_R = AttributeWrapper(comms_annotation=["RECVTR_translator_busy_R"],datatype=bool)
-    
-    # Array attribute 
+
+    # Array attribute
     RCU_TEMP_R  = AttributeWrapper(comms_annotation=["RCU_TEMP_R"], datatype=numpy.float64, dims=(32,))
-    
+
     # Image (2D array) attribute
     HBAT_BF_delays_R = AttributeWrapper(comms_annotation=["HBAT_BF_delays_R" ],datatype=numpy.int64  , dims=(32,96))
 
@@ -53,28 +53,28 @@ class RECV(OPCUADevice):
 
 Once the Tango device is up and running, one can interact with the device attributes just as it would be with the standard Tango attribute classes. For example:
 > d = DeviceProxy(‘fqdn_name’)
-> 
-> attr_names = d.get_attribute_list()	
-> 
+>
+> attr_names = d.get_attribute_list()
+>
 > d.RCU_TEMP_R 				# read attribute value
-> 
+>
 > d.HBAT_BF_delays_RW = [....]		# write attribute value
 
 ## How clients work:
-Clients work by providing a communication interface or data accessor for the attribute. The implementation of this is largely up to the user, but must contain a `setup_attribute` function that returns a valid read and write function. 
+Clients work by providing a communication interface or data accessor for the attribute. The implementation of this is largely up to the user, but must contain a `setup_attribute` function that returns a valid read and write function.
 
-Once a client has been initialized, the attributes can be assigned their read/write functions. 
+Once a client has been initialized, the attributes can be assigned their read/write functions.
 To couple a client to an attribute the set_comm_client or async_set_comm_client of the AttributeWrapper can be called. This function then calls the `setup_attribute` function of the client.
 
-Attributes can be given any custom data in the `Comms_annotation` argument during their creation. This data as well as the AttributeWrapper object itself can be accessed by the `setup_attribute` function in order to correctly configure the read/write functions. The read function should return a numpy array of the correct type and shape, while the write function has to take an input value, which will be of the type and shape of the attribute. 
+Attributes can be given any custom data in the `Comms_annotation` argument during their creation. This data as well as the AttributeWrapper object itself can be accessed by the `setup_attribute` function in order to correctly configure the read/write functions. The read function should return a numpy array of the correct type and shape, while the write function has to take an input value, which will be of the type and shape of the attribute.
 
 Clients can be set up in the device, during the initialization and then can be assigned to the attributes by looping through the list of attributes and calling the `set_comm_client` function.
 
-`tangostationcontrol/tangostationcontrol/clients/comms_client.py` provides a generic client class for us and may be of interest. 
+`tangostationcontrol/tangostationcontrol/clients/comms_client.py` provides a generic client class for us and may be of interest.
 
 ## Dependencies
-Attribute wrappers wraps around tango attributes. As such, Tango needs to be installed. 
-The attribute wrapper relies on 1 internal file. tango/tangostationcontrol/devices/device_decorators.py, which is imported by the attribute_wrapper.py. This file is used for ensuring the read/write functions are only called in the correct device state. 
+Attribute wrappers wraps around tango attributes. As such, Tango needs to be installed.
+The attribute wrapper relies on 1 internal file. tango/tangostationcontrol/devices/device_decorators.py, which is imported by the attribute_wrapper.py. This file is used for ensuring the read/write functions are only called in the correct device state.