diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..21c125c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
+#
+# SPDX-License-Identifier: Unlicense
+
+.py text eol=lf
+.rst text eol=lf
+.txt text eol=lf
+.yaml text eol=lf
+.toml text eol=lf
+.license text eol=lf
+.md text eol=lf
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4fdf32f..ff19dde 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,45 +1,21 @@
-# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò
+# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
 #
 # SPDX-License-Identifier: Unlicense
 
 repos:
-  - repo: https://github.com/python/black
-    rev: 24.10.0
-    hooks:
-      - id: black
-  - repo: https://github.com/fsfe/reuse-tool
-    rev: v4.0.3
-    hooks:
-      - id: reuse
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v5.0.0
+    rev: v4.5.0
     hooks:
       - id: check-yaml
       - id: end-of-file-fixer
       - id: trailing-whitespace
-      - id: mixed-line-ending
-        args:
-         - --fix=lf
-  - repo: https://github.com/pycqa/pylint
-    rev: v3.3.1
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.3.4
     hooks:
-      - id: pylint
-        name: pylint (library code)
-        types: [python]
-        args:
-          - --disable=consider-using-f-string
-        exclude: "^(docs/|examples/|tests/|setup.py$)"
-      - id: pylint
-        name: pylint (example code)
-        description: Run pylint rules on "examples/*.py" files
-        types: [python]
-        files: "^examples/"
-        args:
-          - --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code
-      - id: pylint
-        name: pylint (test code)
-        description: Run pylint rules on "tests/*.py" files
-        types: [python]
-        files: "^tests/"
-        args:
-          - --disable=missing-docstring,consider-using-f-string,duplicate-code
+      - id: ruff-format
+      - id: ruff
+        args: ["--fix"]
+  - repo: https://github.com/fsfe/reuse-tool
+    rev: v3.0.1
+    hooks:
+      - id: reuse
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index f945e92..0000000
--- a/.pylintrc
+++ /dev/null
@@ -1,399 +0,0 @@
-# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
-#
-# SPDX-License-Identifier: Unlicense
-
-[MASTER]
-
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code
-extension-pkg-whitelist=
-
-# Add files or directories to the ignore-list. They should be base names, not
-# paths.
-ignore=CVS
-
-# Add files or directories matching the regex patterns to the ignore-list. The
-# regex matches against base names, not paths.
-ignore-patterns=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Use multiple processes to speed up Pylint.
-jobs=1
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=pylint.extensions.no_self_use
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# Specify a configuration file.
-#rcfile=
-
-# Allow loading of arbitrary C extensions. Extensions are imported into the
-# active Python interpreter and may run arbitrary code.
-unsafe-load-any-extension=no
-
-
-[MESSAGES CONTROL]
-
-# Only show warnings with the listed confidence levels. Leave empty to show
-# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
-confidence=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once).You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use"--disable=all --enable=classes
-# --disable=W"
-# disable=import-error,raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,deprecated-str-translate-call
-disable=raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,import-error,pointless-string-statement,unspecified-encoding
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time (only on the command line, not in the configuration file where
-# it should appear only once). See also the "--disable" option for examples.
-enable=
-
-
-[REPORTS]
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details
-#msg-template=
-
-# Set the output format. Available formats are text, parseable, colorized, json
-# and msvs (visual studio).You can also give a reporter class, eg
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Tells whether to display a full report or only the messages
-reports=no
-
-# Activate the evaluation score.
-score=yes
-
-
-[REFACTORING]
-
-# Maximum number of nested blocks for function / method body
-max-nested-blocks=5
-
-
-[LOGGING]
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format
-logging-modules=logging
-
-
-[SPELLING]
-
-# Spelling dictionary name. Available dictionaries: none. To make it working
-# install python-enchant package.
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to indicated private dictionary in
-# --spelling-private-dict-file option instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-# notes=FIXME,XXX,TODO
-notes=FIXME,XXX
-
-
-[TYPECHECK]
-
-# List of decorators that produce context managers, such as
-# contextlib.contextmanager. Add to this list to register other decorators that
-# produce valid context managers.
-contextmanager-decorators=contextlib.contextmanager
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E1101 when accessed. Python regular
-# expressions are accepted.
-generated-members=
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# This flag controls whether pylint should warn about no-member and similar
-# checks whenever an opaque object is returned when inferring. The inference
-# can return multiple potential results while evaluating a Python object, but
-# some branches might not be evaluated, which results in partial inference. In
-# that case, it might be useful to still emit no-member and other checks for
-# the rest of the inferred objects.
-ignore-on-opaque-inference=yes
-
-# List of class names for which member attributes should not be checked (useful
-# for classes with dynamically set attributes). This supports the use of
-# qualified names.
-ignored-classes=optparse.Values,thread._local,_thread._local
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis. It
-# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=board
-
-# Show a hint with possible names when a member name was not found. The aspect
-# of finding the hint is based on edit distance.
-missing-member-hint=yes
-
-# The minimum edit distance a name should have in order to be considered a
-# similar match for a missing member name.
-missing-member-hint-distance=1
-
-# The total number of similar names that should be taken in consideration when
-# showing a hint for a missing member.
-missing-member-max-choices=1
-
-
-[VARIABLES]
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-# Tells whether unused global variables should be treated as a violation.
-allow-global-unused-variables=yes
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,_cb
-
-# A regular expression matching the name of dummy variables (i.e. expectedly
-# not used).
-dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore
-ignored-argument-names=_.*|^ignored_|^unused_
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# List of qualified module names which can have objects that can redefine
-# builtins.
-redefining-builtins-modules=six.moves,future.builtins
-
-
-[FORMAT]
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-# expected-line-ending-format=
-expected-line-ending-format=LF
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )?<?https?://\S+>?$
-
-# Number of spaces of indent required inside a hanging  or continued line.
-indent-after-paren=4
-
-# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
-# tab).
-indent-string='    '
-
-# Maximum number of characters on a single line.
-max-line-length=100
-
-# Maximum number of lines in a module
-max-module-lines=1000
-
-# Allow the body of a class to be on the same line as the declaration if body
-# contains single statement.
-single-line-class-stmt=no
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-
-[SIMILARITIES]
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=yes
-
-# Minimum lines number of a similarity.
-min-similarity-lines=12
-
-
-[BASIC]
-
-# Regular expression matching correct argument names
-argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
-
-# Regular expression matching correct attribute names
-attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
-
-# Regular expression matching correct class attribute names
-class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
-# Regular expression matching correct class names
-# class-rgx=[A-Z_][a-zA-Z0-9]+$
-class-rgx=[A-Z_][a-zA-Z0-9_]+$
-
-# Regular expression matching correct constant names
-const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-# Regular expression matching correct function names
-function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
-
-# Good variable names which should always be accepted, separated by a comma
-# good-names=i,j,k,ex,Run,_
-good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_
-
-# Include a hint for the correct naming format with invalid-name
-include-naming-hint=no
-
-# Regular expression matching correct inline iteration names
-inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-
-# Regular expression matching correct method names
-method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
-
-# Regular expression matching correct module names
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=^_
-
-# List of decorators that produce properties, such as abc.abstractproperty. Add
-# to this list to register other decorators that produce valid properties.
-property-classes=abc.abstractproperty
-
-# Regular expression matching correct variable names
-variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
-
-
-[IMPORTS]
-
-# Allow wildcard imports from modules that define __all__.
-allow-wildcard-with-all=no
-
-# Analyse import fallback blocks. This can be used to support both Python 2 and
-# 3 compatible code, which means that the block might have code that exists
-# only in one or another interpreter, leading to false positives when analysed.
-analyse-fallback-blocks=no
-
-# Deprecated modules which should not be used, separated by a comma
-deprecated-modules=optparse,tkinter.tix
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled)
-ext-import-graph=
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled)
-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled)
-int-import-graph=
-
-# Force import order to recognize a module as part of the standard
-# compatibility libraries.
-known-standard-library=
-
-# Force import order to recognize a module as part of a third party library.
-known-third-party=enchant
-
-
-[CLASSES]
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,__new__,setUp
-
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,_fields,_replace,_source,_make
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=mcs
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method
-max-args=5
-
-# Maximum number of attributes for a class (see R0902).
-# max-attributes=7
-max-attributes=11
-
-# Maximum number of boolean expressions in a if statement
-max-bool-expr=5
-
-# Maximum number of branch for function / method body
-max-branches=12
-
-# Maximum number of locals for function / method body
-max-locals=15
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-# Maximum number of return / yield for function / method body
-max-returns=6
-
-# Maximum number of statements in function / method body
-max-statements=50
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=1
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "Exception"
-overgeneral-exceptions=builtins.Exception
diff --git a/README.rst b/README.rst
index 5b92d8a..6eae56b 100644
--- a/README.rst
+++ b/README.rst
@@ -17,9 +17,9 @@ Introduction
     :alt: Build Status
 
 
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-    :target: https://github.com/psf/black
-    :alt: Code Style: Black
+.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
+    :target: https://github.com/astral-sh/ruff
+    :alt: Code Style: Ruff
 
 CircuitPython module for interacting with the VL53L1X distance sensor.
 
diff --git a/adafruit_vl53l1x.py b/adafruit_vl53l1x.py
index 9e6ebfd..7f2f6a7 100644
--- a/adafruit_vl53l1x.py
+++ b/adafruit_vl53l1x.py
@@ -26,8 +26,9 @@
 * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
 """
 
-import time
 import struct
+import time
+
 from adafruit_bus_device import i2c_device
 from micropython import const
 
@@ -56,23 +57,23 @@
 
 TB_SHORT_DIST = {
     # ms: (MACROP_A_HI, MACROP_B_HI)
-    15: (b"\x00\x1D", b"\x00\x27"),
-    20: (b"\x00\x51", b"\x00\x6E"),
-    33: (b"\x00\xD6", b"\x00\x6E"),
-    50: (b"\x01\xAE", b"\x01\xE8"),
-    100: (b"\x02\xE1", b"\x03\x88"),
-    200: (b"\x03\xE1", b"\x04\x96"),
-    500: (b"\x05\x91", b"\x05\xC1"),
+    15: (b"\x00\x1d", b"\x00\x27"),
+    20: (b"\x00\x51", b"\x00\x6e"),
+    33: (b"\x00\xd6", b"\x00\x6e"),
+    50: (b"\x01\xae", b"\x01\xe8"),
+    100: (b"\x02\xe1", b"\x03\x88"),
+    200: (b"\x03\xe1", b"\x04\x96"),
+    500: (b"\x05\x91", b"\x05\xc1"),
 }
 
 TB_LONG_DIST = {
     # ms: (MACROP_A_HI, MACROP_B_HI)
-    20: (b"\x00\x1E", b"\x00\x22"),
-    33: (b"\x00\x60", b"\x00\x6E"),
-    50: (b"\x00\xAD", b"\x00\xC6"),
-    100: (b"\x01\xCC", b"\x01\xEA"),
-    200: (b"\x02\xD9", b"\x02\xF8"),
-    500: (b"\x04\x8F", b"\x04\xA4"),
+    20: (b"\x00\x1e", b"\x00\x22"),
+    33: (b"\x00\x60", b"\x00\x6e"),
+    50: (b"\x00\xad", b"\x00\xc6"),
+    100: (b"\x01\xcc", b"\x01\xea"),
+    200: (b"\x02\xd9", b"\x02\xf8"),
+    500: (b"\x04\x8f", b"\x04\xa4"),
 }
 
 
@@ -90,13 +91,12 @@ def __init__(self, i2c, address=41):
         self.timing_budget = 50
 
     def _sensor_init(self):
-        # pylint: disable=line-too-long
         init_seq = bytes(
             [  # value    addr : description
                 0x00,  # 0x2d : set bit 2 and 5 to 1 for fast plus mode (1MHz I2C), else don't touch
-                0x00,  # 0x2e : bit 0 if I2C pulled up at 1.8V, else set bit 0 to 1 (pull up at AVDD)
-                0x00,  # 0x2f : bit 0 if GPIO pulled up at 1.8V, else set bit 0 to 1 (pull up at AVDD)
-                0x01,  # 0x30 : set bit 4 to 0 for active high interrupt and 1 for active low (bits 3:0 must be 0x1), use SetInterruptPolarity()
+                0x00,  # 0x2e : bit 0 if I2C pulled up at 1.8V, else set bit 0 to 1 (pull up at AVDD)  # noqa: E501
+                0x00,  # 0x2f : bit 0 if GPIO pulled up at 1.8V, else set bit 0 to 1 (pull up at AVDD)  # noqa: E501
+                0x01,  # 0x30 : set bit 4 to 0 for active high interrupt and 1 for active low (bits 3:0 must be 0x1), use SetInterruptPolarity()  # noqa: E501
                 0x02,  # 0x31 : bit 1 = interrupt depending on the polarity
                 0x00,  # 0x32 : not user-modifiable
                 0x02,  # 0x33 : not user-modifiable
@@ -118,7 +118,7 @@ def _sensor_init(self):
                 0x00,  # 0x43 : not user-modifiable
                 0x00,  # 0x44 : not user-modifiable
                 0x00,  # 0x45 : not user-modifiable
-                0x20,  # 0x46 : interrupt configuration 0->level low detection, 1-> level high, 2-> Out of window, 3->In window, 0x20-> New sample ready , TBC
+                0x20,  # 0x46 : interrupt configuration 0->level low detection, 1-> level high, 2-> Out of window, 3->In window, 0x20-> New sample ready , TBC  # noqa: E501
                 0x0B,  # 0x47 : not user-modifiable
                 0x00,  # 0x48 : not user-modifiable
                 0x00,  # 0x49 : not user-modifiable
@@ -148,7 +148,7 @@ def _sensor_init(self):
                 0x01,  # 0x61 : not user-modifiable
                 0xF1,  # 0x62 : not user-modifiable
                 0x0D,  # 0x63 : not user-modifiable
-                0x01,  # 0x64 : Sigma threshold MSB (mm in 14.2 format for MSB+LSB), default value 90 mm
+                0x01,  # 0x64 : Sigma threshold MSB (mm in 14.2 format for MSB+LSB), default value 90 mm  # noqa: E501
                 0x68,  # 0x65 : Sigma threshold LSB
                 0x00,  # 0x66 : Min count Rate MSB (MCPS in 9.7 format for MSB+LSB)
                 0x80,  # 0x67 : Min count Rate LSB
@@ -206,9 +206,7 @@ def distance(self):
         """The distance in units of centimeters."""
         if self._read_register(_VL53L1X_RESULT__RANGE_STATUS)[0] != 0x09:
             return None
-        dist = self._read_register(
-            _VL53L1X_RESULT__FINAL_CROSSTALK_CORRECTED_RANGE_MM_SD0, 2
-        )
+        dist = self._read_register(_VL53L1X_RESULT__FINAL_CROSSTALK_CORRECTED_RANGE_MM_SD0, 2)
         dist = struct.unpack(">H", dist)[0]
         return dist / 10
 
@@ -227,10 +225,7 @@ def clear_interrupt(self):
     @property
     def data_ready(self):
         """Returns true if new data is ready, otherwise false."""
-        if (
-            self._read_register(_GPIO__TIO_HV_STATUS)[0] & 0x01
-            == self._interrupt_polarity
-        ):
+        if self._read_register(_GPIO__TIO_HV_STATUS)[0] & 0x01 == self._interrupt_polarity:
             return True
         return False
 
@@ -287,12 +282,12 @@ def distance_mode(self, mode):
             self._write_register(_SD_CONFIG__INITIAL_PHASE_SD0, b"\x06\x06")
         elif mode == 2:
             # long distance
-            self._write_register(_PHASECAL_CONFIG__TIMEOUT_MACROP, b"\x0A")
-            self._write_register(_RANGE_CONFIG__VCSEL_PERIOD_A, b"\x0F")
-            self._write_register(_RANGE_CONFIG__VCSEL_PERIOD_B, b"\x0D")
-            self._write_register(_RANGE_CONFIG__VALID_PHASE_HIGH, b"\xB8")
-            self._write_register(_SD_CONFIG__WOI_SD0, b"\x0F\x0D")
-            self._write_register(_SD_CONFIG__INITIAL_PHASE_SD0, b"\x0E\x0E")
+            self._write_register(_PHASECAL_CONFIG__TIMEOUT_MACROP, b"\x0a")
+            self._write_register(_RANGE_CONFIG__VCSEL_PERIOD_A, b"\x0f")
+            self._write_register(_RANGE_CONFIG__VCSEL_PERIOD_B, b"\x0d")
+            self._write_register(_RANGE_CONFIG__VALID_PHASE_HIGH, b"\xb8")
+            self._write_register(_SD_CONFIG__WOI_SD0, b"\x0f\x0d")
+            self._write_register(_SD_CONFIG__INITIAL_PHASE_SD0, b"\x0e\x0e")
         else:
             raise ValueError("Unsupported mode.")
         self.timing_budget = self._timing_budget
@@ -318,13 +313,11 @@ def roi_xy(self, data):
         if x > 10 or y > 10:
             optical_center = 199
 
-        self._write_register(
-            _ROI_CONFIG__USER_ROI_CENTRE_SPAD, optical_center.to_bytes()
-        )
+        self._write_register(_ROI_CONFIG__USER_ROI_CENTRE_SPAD, optical_center.to_bytes())
         self._write_register(
             _ROI_CONFIG__USER_ROI_REQUESTED_GLOBAL_XY_SIZE,
             ((y - 1) << 4 | (x - 1)).to_bytes(),
-        )
+        )  # noqa: E501
 
     @property
     def roi_center(self):
@@ -355,7 +348,5 @@ def set_address(self, new_address):
         multiple VL53L0X sensors on the same I2C bus (SDA & SCL pins). See also the
         `example <examples.html#multiple-vl53l1x-on-same-i2c-bus>`_ for proper usage.
         """
-        self._write_register(
-            _VL53L1X_I2C_SLAVE_DEVICE_ADDRESS, struct.pack(">B", new_address)
-        )
+        self._write_register(_VL53L1X_I2C_SLAVE_DEVICE_ADDRESS, struct.pack(">B", new_address))
         self.i2c_device = i2c_device.I2CDevice(self._i2c, new_address)
diff --git a/docs/api.rst b/docs/api.rst
index 7e5a29d..eb8d317 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -4,5 +4,8 @@
 .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py)
 .. use this format as the module name: "adafruit_foo.foo"
 
+API Reference
+#############
+
 .. automodule:: adafruit_vl53l1x
     :members:
diff --git a/docs/conf.py b/docs/conf.py
index c2a23eb..00bb9fd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,12 +1,10 @@
-# -*- coding: utf-8 -*-
-
 # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
 #
 # SPDX-License-Identifier: MIT
 
+import datetime
 import os
 import sys
-import datetime
 
 sys.path.insert(0, os.path.abspath(".."))
 
@@ -55,9 +53,7 @@
 creation_year = "2021"
 current_year = str(datetime.datetime.now().year)
 year_duration = (
-    current_year
-    if current_year == creation_year
-    else creation_year + " - " + current_year
+    current_year if current_year == creation_year else creation_year + " - " + current_year
 )
 copyright = year_duration + " Carter Nelson"
 author = "Carter Nelson"
diff --git a/examples/vl53l1x_displayio_simpletest.py b/examples/vl53l1x_displayio_simpletest.py
index a00640f..d7e7dd9 100644
--- a/examples/vl53l1x_displayio_simpletest.py
+++ b/examples/vl53l1x_displayio_simpletest.py
@@ -6,10 +6,12 @@
 # SPDX-License-Identifier: MIT
 
 import time
+
 import board
 from adafruit_display_text.bitmap_label import Label
-from terminalio import FONT
 from displayio import Group
+from terminalio import FONT
+
 import adafruit_vl53l1x
 
 # create a main_group to hold anything we want to show on the display.
diff --git a/examples/vl53l1x_set_address_multiple_sensors.py b/examples/vl53l1x_set_address_multiple_sensors.py
index f877609..dea50fc 100755
--- a/examples/vl53l1x_set_address_multiple_sensors.py
+++ b/examples/vl53l1x_set_address_multiple_sensors.py
@@ -12,8 +12,10 @@
 """
 
 import time
+
 import board
 import digitalio
+
 import adafruit_vl53l1x
 
 # Define the I2C pins.
@@ -64,6 +66,6 @@
     # the sensor distance readings for all available sensors.
     for sensor_number, sensor in enumerate(vl53l1x):
         if sensor.data_ready:
-            print("Sensor {}: {}".format(sensor_number + 1, sensor.distance))
+            print(f"Sensor {sensor_number + 1}: {sensor.distance}")
             sensor.clear_interrupt()
     time.sleep(0.5)
diff --git a/examples/vl53l1x_simpletest.py b/examples/vl53l1x_simpletest.py
index cd0c7e7..aba8845 100644
--- a/examples/vl53l1x_simpletest.py
+++ b/examples/vl53l1x_simpletest.py
@@ -7,7 +7,9 @@
 # Will print the sensed range/distance every second.
 
 import time
+
 import board
+
 import adafruit_vl53l1x
 
 i2c = board.I2C()  # uses board.SCL and board.SDA
@@ -22,9 +24,9 @@
 print("VL53L1X Simple Test.")
 print("--------------------")
 model_id, module_type, mask_rev = vl53.model_info
-print("Model ID: 0x{:0X}".format(model_id))
-print("Module Type: 0x{:0X}".format(module_type))
-print("Mask Revision: 0x{:0X}".format(mask_rev))
+print(f"Model ID: 0x{model_id:0X}")
+print(f"Module Type: 0x{module_type:0X}")
+print(f"Mask Revision: 0x{mask_rev:0X}")
 print("Distance Mode: ", end="")
 if vl53.distance_mode == 1:
     print("SHORT")
@@ -32,13 +34,13 @@
     print("LONG")
 else:
     print("UNKNOWN")
-print("Timing Budget: {}".format(vl53.timing_budget))
+print(f"Timing Budget: {vl53.timing_budget}")
 print("--------------------")
 
 vl53.start_ranging()
 
 while True:
     if vl53.data_ready:
-        print("Distance: {} cm".format(vl53.distance))
+        print(f"Distance: {vl53.distance} cm")
         vl53.clear_interrupt()
         time.sleep(1.0)
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..1b887b1
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
+#
+# SPDX-License-Identifier: MIT
+
+target-version = "py38"
+line-length = 100
+
+[lint]
+preview = true
+select = ["I", "PL", "UP"]
+
+extend-select = [
+  "D419",  # empty-docstring
+  "E501",  # line-too-long
+  "W291",  # trailing-whitespace
+  "PLC0414",  # useless-import-alias
+  "PLC2401",  # non-ascii-name
+  "PLC2801",  # unnecessary-dunder-call
+  "PLC3002",  # unnecessary-direct-lambda-call
+  "E999",  # syntax-error
+  "PLE0101",  # return-in-init
+  "F706",  # return-outside-function
+  "F704",  # yield-outside-function
+  "PLE0116",  # continue-in-finally
+  "PLE0117",  # nonlocal-without-binding
+  "PLE0241",  # duplicate-bases
+  "PLE0302",  # unexpected-special-method-signature
+  "PLE0604",  # invalid-all-object
+  "PLE0605",  # invalid-all-format
+  "PLE0643",  # potential-index-error
+  "PLE0704",  # misplaced-bare-raise
+  "PLE1141",  # dict-iter-missing-items
+  "PLE1142",  # await-outside-async
+  "PLE1205",  # logging-too-many-args
+  "PLE1206",  # logging-too-few-args
+  "PLE1307",  # bad-string-format-type
+  "PLE1310",  # bad-str-strip-call
+  "PLE1507",  # invalid-envvar-value
+  "PLE2502",  # bidirectional-unicode
+  "PLE2510",  # invalid-character-backspace
+  "PLE2512",  # invalid-character-sub
+  "PLE2513",  # invalid-character-esc
+  "PLE2514",  # invalid-character-nul
+  "PLE2515",  # invalid-character-zero-width-space
+  "PLR0124",  # comparison-with-itself
+  "PLR0202",  # no-classmethod-decorator
+  "PLR0203",  # no-staticmethod-decorator
+  "UP004",  # useless-object-inheritance
+  "PLR0206",  # property-with-parameters
+  "PLR0904",  # too-many-public-methods
+  "PLR0911",  # too-many-return-statements
+  "PLR0912",  # too-many-branches
+  "PLR0913",  # too-many-arguments
+  "PLR0914",  # too-many-locals
+  "PLR0915",  # too-many-statements
+  "PLR0916",  # too-many-boolean-expressions
+  "PLR1702",  # too-many-nested-blocks
+  "PLR1704",  # redefined-argument-from-local
+  "PLR1711",  # useless-return
+  "C416",  # unnecessary-comprehension
+  "PLR1733",  # unnecessary-dict-index-lookup
+  "PLR1736",  # unnecessary-list-index-lookup
+
+  # ruff reports this rule is unstable
+  #"PLR6301",  # no-self-use
+
+  "PLW0108",  # unnecessary-lambda
+  "PLW0120",  # useless-else-on-loop
+  "PLW0127",  # self-assigning-variable
+  "PLW0129",  # assert-on-string-literal
+  "B033",  # duplicate-value
+  "PLW0131",  # named-expr-without-context
+  "PLW0245",  # super-without-brackets
+  "PLW0406",  # import-self
+  "PLW0602",  # global-variable-not-assigned
+  "PLW0603",  # global-statement
+  "PLW0604",  # global-at-module-level
+
+  # fails on the try: import typing used by libraries
+  #"F401",  # unused-import
+
+  "F841",  # unused-variable
+  "E722",  # bare-except
+  "PLW0711",  # binary-op-exception
+  "PLW1501",  # bad-open-mode
+  "PLW1508",  # invalid-envvar-default
+  "PLW1509",  # subprocess-popen-preexec-fn
+  "PLW2101",  # useless-with-lock
+  "PLW3301",  # nested-min-max
+]
+
+ignore = [
+    "PLR2004", # magic-value-comparison
+    "UP030", # format literals
+    "PLW1514",  # unspecified-encoding
+    "PLR0913", # too-many-arguments
+    "PLR0915", # too-many-statements
+    "PLR0917", # too-many-positional-arguments
+    "PLR0904", # too-many-public-methods
+    "PLR0912", # too-many-branches
+    "PLR0916", # too-many-boolean-expressions
+    "PLR6301", # could-be-static no-self-use
+    "PLC0415", # import outside toplevel
+]
+
+[format]
+line-ending = "lf"