Repository: lightwave-lab/lightlab (language: python)

Issue:
Bump ipython from 7.16.3 to 8.10.0
Bumps [ipython](https://github.com/ipython/ipython) from 7.16.3 to 8.10.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/ipython/ipython/releases">ipython's releases</a>.</em></p>
<blockquote>
<h2>See <a href="https://pypi.org/project/ipython/">https://pypi.org/project/ipython/</a></h2>
<p>We do not use GitHub release anymore. Please see PyPI <a href="https://pypi.org/project/ipython/">https://pypi.org/project/ipython/</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/ipython/ipython/commit/15ea1ed5a886d6c19c1cc4856f2cf04a2a547c57"><code>15ea1ed</code></a> release 8.10.0</li>
<li><a href="https://github.com/ipython/ipython/commit/560ad109197c0f8373865896af369bb3b36fd229"><code>560ad10</code></a> DOC: Update what's new for 8.10 (<a href="https://github-redirect.dependabot.com/ipython/ipython/issues/13939">#13939</a>)</li>
<li><a href="https://github.com/ipython/ipython/commit/7557ade0ed927475d5ab5b573d0ea4febfb22683"><code>7557ade</code></a> DOC: Update what's new for 8.10</li>
<li><a href="https://github.com/ipython/ipython/commit/385d69325319a5972ee9b5983638e3617f21cb1f"><code>385d693</code></a> Merge pull request from GHSA-29gw-9793-fvw7</li>
<li><a href="https://github.com/ipython/ipython/commit/e548ee23ac460a99901f1cd43b94ae84a35ec393"><code>e548ee2</code></a> Swallow potential exceptions from showtraceback() (<a href="https://github-redirect.dependabot.com/ipython/ipython/issues/13934">#13934</a>)</li>
<li><a href="https://github.com/ipython/ipython/commit/0694b08b436203817059ec7e7136cf8561a6f013"><code>0694b08</code></a> MAINT: mock slowest test. (<a href="https://github-redirect.dependabot.com/ipython/ipython/issues/13885">#13885</a>)</li>
<li><a href="https://github.com/ipython/ipython/commit/865591252a67c6907fe03228b4053305715286e6"><code>8655912</code></a> MAINT: mock slowest test.</li>
<li><a href="https://github.com/ipython/ipython/commit/a011765b44febfb11bae122d2ed7db763621ac8f"><code>a011765</code></a> Isolate the attack tests with setUp and tearDown methods</li>
<li><a href="https://github.com/ipython/ipython/commit/c7a9470e540392c575aac46c3ee5cf4fe5123eb1"><code>c7a9470</code></a> Add some regression tests for this change</li>
<li><a href="https://github.com/ipython/ipython/commit/fd34cf5f1f6e243243c738c6e0cf62eb682c4d68"><code>fd34cf5</code></a> Swallow potential exceptions from showtraceback()</li>
<li>Additional commits viewable in <a href="https://github.com/ipython/ipython/compare/7.16.3...8.10.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ipython&package-manager=pip&previous-version=7.16.3&new-version=8.10.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/lightwave-lab/lightlab/network/alerts).

</details>
Bump joblib from 0.13.2 to 1.2.0
Bumps [joblib](https://github.com/joblib/joblib) from 0.13.2 to 1.2.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/joblib/joblib/blob/master/CHANGES.rst">joblib's changelog</a>.</em></p>
<blockquote>
<h2>Release 1.2.0</h2>
<ul>
<li>
<p>Fix a security issue where <code>eval(pre_dispatch)</code> could potentially run
arbitrary code. Now only basic numerics are supported.
<a href="https://github-redirect.dependabot.com/joblib/joblib/pull/1327">joblib/joblib#1327</a></p>
</li>
<li>
<p>Make sure that joblib works even when multiprocessing is not available,
for instance with Pyodide
<a href="https://github-redirect.dependabot.com/joblib/joblib/pull/1256">joblib/joblib#1256</a></p>
</li>
<li>
<p>Avoid unnecessary warnings when workers and main process delete
the temporary memmap folder contents concurrently.
<a href="https://github-redirect.dependabot.com/joblib/joblib/pull/1263">joblib/joblib#1263</a></p>
</li>
<li>
<p>Fix memory alignment bug for pickles containing numpy arrays.
This is especially important when loading the pickle with
<code>mmap_mode != None</code> as the resulting <code>numpy.memmap</code> object
would not be able to correct the misalignment without performing
a memory copy.
This bug would cause invalid computation and segmentation faults
with native code that would directly access the underlying data
buffer of a numpy array, for instance C/C++/Cython code compiled
with older GCC versions or some old OpenBLAS written in platform
specific assembly.
<a href="https://github-redirect.dependabot.com/joblib/joblib/pull/1254">joblib/joblib#1254</a></p>
</li>
<li>
<p>Vendor cloudpickle 2.2.0 which adds support for PyPy 3.8+.</p>
</li>
<li>
<p>Vendor loky 3.3.0 which fixes several bugs including:</p>
<ul>
<li>
<p>robustly forcibly terminating worker processes in case of a crash
(<a href="https://github-redirect.dependabot.com/joblib/joblib/pull/1269">joblib/joblib#1269</a>);</p>
</li>
<li>
<p>avoiding leaking worker processes in case of nested loky parallel
calls;</p>
</li>
<li>
<p>reliability spawn the correct number of reusable workers.</p>
</li>
</ul>
</li>
</ul>
<h2>Release 1.1.0</h2>
<ul>
<li>
<p>Fix byte order inconsistency issue during deserialization using joblib.load
in cross-endian environment: the numpy arrays are now always loaded to
use the system byte order, independently of the byte order of the system
that serialized the pickle.
<a href="https://github-redirect.dependabot.com/joblib/joblib/pull/1181">joblib/joblib#1181</a></p>
</li>
<li>
<p>Fix joblib.Memory bug with the <code>ignore</code> parameter when the cached function
is a decorated function.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/joblib/joblib/commit/5991350e03493fbf27bb596429a935e0c40fb536"><code>5991350</code></a> Release 1.2.0</li>
<li><a href="https://github.com/joblib/joblib/commit/3fa218887770467695573e37e1c7179fd1b5065d"><code>3fa2188</code></a> MAINT cleanup numpy warnings related to np.matrix in tests (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1340">#1340</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/cea26ff2080dc4e9b51957e57994f48351086193"><code>cea26ff</code></a> CI test the future loky-3.3.0 branch (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1338">#1338</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/8aca6f4fc29c36e011201bbfe2da227b58da55e3"><code>8aca6f4</code></a> MAINT: remove pytest.warns(None) warnings in pytest 7 (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1264">#1264</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/067ed4f7cc88aef0f4160d6ef7155d40767fee08"><code>067ed4f</code></a> XFAIL test_child_raises_parent_exits_cleanly with multiprocessing (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1339">#1339</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/ac4ebd540840f92f2c12f47ad001b555d2bb1ce2"><code>ac4ebd5</code></a> MAINT add back pytest warnings plugin (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1337">#1337</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/a23427d1700e32d4fc5d49c16d72e3f3c24f65f9"><code>a23427d</code></a> Test child raises parent exits cleanly more reliable on macos (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1335">#1335</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/ac0969194aea9c9282a7532cfcda9746bc3b379b"><code>ac09691</code></a> [MAINT] various test updates (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1334">#1334</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/4a314b152fe0b71b53b6092ed67be528ec81392e"><code>4a314b1</code></a> Vendor loky 3.2.0 (<a href="https://github-redirect.dependabot.com/joblib/joblib/issues/1333">#1333</a>)</li>
<li><a href="https://github.com/joblib/joblib/commit/bdf47e95c7204499397f0cd9ef6b3198c71976ce"><code>bdf47e9</code></a> Make test_parallel_with_interactively_defined_functions_default_backend timeo...</li>
<li>Additional commits viewable in <a href="https://github.com/joblib/joblib/compare/0.13.2...1.2.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=joblib&package-manager=pip&previous-version=0.13.2&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/lightwave-lab/lightlab/network/alerts).

</details>
Compatibility problem with Windows, specially Win 10
Hi,
Nice framework. It could be really helpful in building up an automatic lab and ease the workload of students, and technicians.
I suppose you mostly use linux while you develop this modul. Just curious that is there any compatibility issue and solution for running this module on Windows platform? Thank you so much.
XG
clean code
improve code


Discussion / hints:


We actually use Macs and linux mostly. I believe lightlab is compatible with windows, but special care must be taken while installing a GPIB driver for pyvisa such as NI-VISA. If you can get pyvisa to work, and if you have python 3.6+, you should be able to use lightlab. I believe there are some installation instructions for windows users in the documentation. Lightlab.readthedocs.io

Thanks for your interest.

From: kevinclily <notifications@github.com>
Reply-To: lightwave-lab/lightlab <reply@reply.github.com>
Date: Wednesday, February 12, 2020 at 10:37 AM
To: lightwave-lab/lightlab <lightlab@noreply.github.com>
Cc: Subscribed <subscribed@noreply.github.com>
Subject: [lightwave-lab/lightlab] Compatibility problem with Windows, specially Win 10 (#25)


Hi,
Nice framework. It could be really helpful in building up an automatic lab and ease the workload of students, and technicians.
I suppose you mostly use linux while you develop this modul. Just curious that is there any compatibility issue and solution for running this module on Windows platform? Thank you so much.
XG

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub<https://github.com/lightwave-lab/lightlab/issues/25?email_source=notifications&email_token=AAIJZEZ6E436RNGIEK4SUCLRCQJRBA5CNFSM4KT4B7G2YY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4IM7G23Q>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AAIJZE5APNBHPCAVMEZ2S63RCQJRBANCNFSM4KT4B7GQ>.

Thanks Joaquin!

Fix patch:
diff --git a/dev-requirements.txt b/dev-requirements.txt
deleted file mode 100644
index 4947a4db..00000000
--- a/dev-requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-jupyter==1.0.0
-mock==2.0.0
-nbstripout==0.3.1
-pipdeptree==0.13.0
-pyserial==3.4
-python-dateutil==2.6.1
-pytz==2017.3
-pyusb==1.0.2
-recommonmark==0.4.0
-texttable==1.4.0
-wheel==0.31.1
diff --git a/.gitlab-ci.yml b/legacy/.gitlab-ci.yml
similarity index 100%
rename from .gitlab-ci.yml
rename to legacy/.gitlab-ci.yml
diff --git a/.travis.yml b/legacy/.travis.yml
similarity index 100%
rename from .travis.yml
rename to legacy/.travis.yml
diff --git a/lightlab/equipment/abstract_drivers/TekScopeAbstract.py b/lightlab/equipment/abstract_drivers/TekScopeAbstract.py
index faff90ff..2a6e919a 100644
--- a/lightlab/equipment/abstract_drivers/TekScopeAbstract.py
+++ b/lightlab/equipment/abstract_drivers/TekScopeAbstract.py
@@ -67,10 +67,8 @@ def timebaseConfig(self, avgCnt=None, duration=None, position=None, nPts=None):
             self.setConfigParam('DATA:START', 1)
             self.setConfigParam('DATA:STOP', nPts)
 
-        presentSettings = {
-            'avgCnt': self.getConfigParam('ACQUIRE:NUMAVG', forceHardware=True)
-        }
-
+        presentSettings = dict()
+        presentSettings['avgCnt'] = self.getConfigParam('ACQUIRE:NUMAVG', forceHardware=True)
         presentSettings['duration'] = self.getConfigParam('HORIZONTAL:MAIN:SCALE', forceHardware=True)
         presentSettings['position'] = self.getConfigParam('HORIZONTAL:MAIN:POSITION', forceHardware=True)
         presentSettings['nPts'] = self.getConfigParam(self._recLenParam, forceHardware=True)
@@ -103,19 +101,13 @@ def acquire(self, chans=None, timeout=None, **kwargs):
 
         for c in chans:
             if c > self.totalChans:
-                raise Exception(
-                    (
-                        f'Received channel: {str(c)}'
-                        + '. Max channels of this scope is '
-                    )
-                    + str(self.totalChans)
-                )
-
+                raise Exception('Received channel: ' + str(c) +
+                                '. Max channels of this scope is ' + str(self.totalChans))
 
         # Channel select
         for ich in range(1, 1 + self.totalChans):
             thisState = 1 if ich in chans else 0
-            self.setConfigParam(f'SELECT:CH{str(ich)}', thisState)
+            self.setConfigParam('SELECT:CH' + str(ich), thisState)
 
         isSampling = kwargs.get('avgCnt', 0) == 1
         self._setupSingleShot(isSampling)
@@ -184,11 +176,12 @@ def __transferData(self, chan):
             Todo:
                 Make this binary transfer to go even faster
         '''
-        chStr = f'CH{str(chan)}'
+        chStr = 'CH' + str(chan)
         self.setConfigParam('DATA:ENCDG', 'ASCII')
         self.setConfigParam('DATA:SOURCE', chStr)
 
-        return self.query_ascii_values('CURV?')
+        voltRaw = self.query_ascii_values('CURV?')
+        return voltRaw
 
     def __scaleData(self, voltRaw):
         ''' Scale to second and voltage units.
@@ -209,10 +202,7 @@ def __scaleData(self, voltRaw):
                 YZERO, the reference voltage, YOFF, the offset position, and
                 YSCALE, the conversion factor between position and voltage.
         '''
-        get = lambda param: float(
-            self.getConfigParam(f'WFMOUTPRE:{param}', forceHardware=True)
-        )
-
+        get = lambda param: float(self.getConfigParam('WFMOUTPRE:' + param, forceHardware=True))
         voltage = (np.array(voltRaw) - get('YOFF')) \
             * get(self._yScaleParam) \
             + get('YZERO')
@@ -273,13 +263,10 @@ def setMeasurement(self, measIndex, chan, measType):
         '''
         if measIndex == 0:
             raise ValueError('measIndex is 1-indexed')
-        measSubmenu = f'MEASUREMENT:MEAS{str(measIndex)}:'
-        self.setConfigParam(
-            measSubmenu + self._measurementSourceParam, f'CH{str(chan)}'
-        )
-
-        self.setConfigParam(f'{measSubmenu}TYPE', measType.upper())
-        self.setConfigParam(f'{measSubmenu}STATE', 1)
+        measSubmenu = 'MEASUREMENT:MEAS' + str(measIndex) + ':'
+        self.setConfigParam(measSubmenu + self._measurementSourceParam, 'CH' + str(chan))
+        self.setConfigParam(measSubmenu + 'TYPE', measType.upper())
+        self.setConfigParam(measSubmenu + 'STATE', 1)
 
     def measure(self, measIndex):
         '''
@@ -289,8 +276,8 @@ def measure(self, measIndex):
             Returns:
                 (float)
         '''
-        measSubmenu = f'MEASUREMENT:MEAS{str(measIndex)}:'
-        return float(self.getConfigParam(f'{measSubmenu}VALUE', forceHardware=True))
+        measSubmenu = 'MEASUREMENT:MEAS' + str(measIndex) + ':'
+        return float(self.getConfigParam(measSubmenu + 'VALUE', forceHardware=True))
 
     def autoAdjust(self, chans):
         ''' Adjusts offsets and scaling so that waveforms are not clipped '''
@@ -298,7 +285,7 @@ def autoAdjust(self, chans):
         self.saveConfig(dest='+autoAdjTemp', subgroup='MEASUREMENT')
 
         for ch in chans:
-            chStr = f'CH{str(ch)}'
+            chStr = 'CH' + str(ch)
 
             # Set up measurements
             self.setMeasurement(1, ch, 'pk2pk')
@@ -312,8 +299,8 @@ def autoAdjust(self, chans):
                 pk2pk = self.measure(1)
                 mean = self.measure(2)
 
-                span = float(self.getConfigParam(f'{chStr}:SCALE'))
-                offs = float(self.getConfigParam(f'{chStr}:OFFSET'))
+                span = float(self.getConfigParam(chStr + ':SCALE'))
+                offs = float(self.getConfigParam(chStr + ':OFFSET'))
 
                 # Check if scale is correct within the tolerance
                 newSpan = None
@@ -323,7 +310,7 @@ def autoAdjust(self, chans):
                 elif pk2pk > 0.8 * span:
                     newSpan = 2 * span
                 if newSpan < 0.1 or newSpan > 100:
-                    raise Exception(f'Scope channel {chStr} could not be adjusted.')
+                    raise Exception('Scope channel ' + chStr + ' could not be adjusted.')
 
                 # Check if offset is correct within the tolerance
                 if abs(mean) > 0.05 * span:
@@ -334,8 +321,8 @@ def autoAdjust(self, chans):
                     break
 
                 # Adjust settings
-                self.setConfigParam(f'{chStr}:SCALE', newSpan / 10)
-                self.setConfigParam(f'{chStr}:OFFSET', newOffs)
+                self.setConfigParam(chStr + ':SCALE', newSpan / 10)
+                self.setConfigParam(chStr + ':OFFSET', newOffs)
 
         # Recover the measurement setup from before adjustment
         self.loadConfig(source='+autoAdjTemp', subgroup='MEASUREMENT')
diff --git a/lightlab/equipment/abstract_drivers/configurable.py b/lightlab/equipment/abstract_drivers/configurable.py
index 85931e80..821026e7 100644
--- a/lightlab/equipment/abstract_drivers/configurable.py
+++ b/lightlab/equipment/abstract_drivers/configurable.py
@@ -1,7 +1,7 @@
 from lightlab import visalogger as logger
 from pyvisa import VisaIOError
 from contextlib import contextmanager
-import dpath.util
+import dpath
 import json
 from numpy import floor
 from pathlib import Path
@@ -32,7 +32,7 @@ class TekConfig(object):
 
     def __init__(self, initDict=None):
         if initDict is None:
-            initDict = {}
+            initDict = dict()
         self.dico = initDict.copy()
 
     def __str__(self):
@@ -55,18 +55,21 @@ def get(self, cStr, asCmd=True):
                 asCmd (bool): if true, returns a tuple representing a command. Otherwise returns just the value
         '''
         try:
-            val = dpath.util.get(self.dico, cStr, separator=self.separator)
+            val = dpath.get(self.dico, cStr, separator=self.separator)
         except KeyError:
-            raise KeyError(f'{cStr} is not present in this TekConfig instance')
+            raise KeyError(cStr + ' is not present in this TekConfig instance')
         if type(val) is dict and '&' in val.keys():
             val = val['&']
-        return (cStr, str(val)) if asCmd else val
+        if not asCmd:
+            return val
+        else:
+            return (cStr, str(val))
 
     def set(self, cStr, val):
         ''' Takes the value only, not a dictionary '''
         # First check that it does not exist as a subdir
         try:
-            ex = dpath.util.get(self.dico, cStr, separator=self.separator)
+            ex = dpath.get(self.dico, cStr, separator=self.separator)
         except KeyError:
             # doesn't exist, we are good to go
             pass
@@ -76,20 +79,20 @@ def set(self, cStr, val):
                 cStr = cStr + self.separator + '&'
 
         cmd = (cStr, val)
-        success = dpath.util.set(self.dico, *cmd, separator=self.separator)
+        success = dpath.set(self.dico, *cmd, separator=self.separator)
         if success != 1:  # it doesn't exist yet
             try:
-                dpath.util.new(self.dico, *cmd, separator=self.separator)
+                dpath.new(self.dico, *cmd, separator=self.separator)
             except (ValueError, dpath.exceptions.PathNotFound):
                 # We probably have an integer leaf where we would also like to have a directory
                 parent = self.separator.join(cmd[0].split(self.separator)[:-1])
                 try:
                     oldV = self.get(parent, asCmd=False)
                 except KeyError:
-                    print(f'dpath did not take {cmd}')
+                    print('dpath did not take ' + str(cmd))
                     raise
-                dpath.util.set(self.dico, parent, {'&': oldV}, separator=self.separator)
-                dpath.util.new(self.dico, *cmd, separator=self.separator)
+                dpath.set(self.dico, parent, {'&': oldV}, separator=self.separator)
+                dpath.new(self.dico, *cmd, separator=self.separator)
 
     def getList(self, subgroup='', asCmd=True):
         ''' Deep crawler that goes in and generates a command for every leaf.
@@ -102,7 +105,7 @@ def getList(self, subgroup='', asCmd=True):
                 list: list of valid commands (cstr, val) on the subgroup subdirectory
         '''
         cList = []
-        children = dpath.util.search(
+        children = dpath.search(
             self.dico, f'{subgroup}*', yielded=True, separator=self.separator
         )
 
@@ -116,13 +119,14 @@ def getList(self, subgroup='', asCmd=True):
                 cList += self.getList(subgroup=cmd[0] + self.separator)
         if asCmd:
             return cList
-        writeList = [None] * len(cList)
-        for i, cmd in enumerate(cList):
-            cStr, val = cmd
-            if cStr[-1] == '&':  # check for tokens
-                cStr = cStr[:-2]
-            writeList[i] = f'{cStr} {str(val)}'
-        return writeList
+        else:
+            writeList = [None] * len(cList)
+            for i, cmd in enumerate(cList):
+                cStr, val = cmd
+                if cStr[-1] == '&':  # check for tokens
+                    cStr = cStr[:-2]
+                writeList[i] = cStr + ' ' + str(val)
+            return writeList
 
     def setList(self, cmdList):
         ''' The inverse of getList '''
@@ -144,7 +148,7 @@ def transfer(self, source, subgroup=''):
         elif type(source) is type(self):
             sCon = source
         else:
-            raise Exception(f'Invalid source for transfer. Got {str(type(source))}')
+            raise Exception('Invalid source for transfer. Got ' + str(type(source)))
         commands = sCon.getList(subgroup=subgroup)
         self.setList(commands)
         return self
@@ -169,7 +173,7 @@ def __parseShorthand(cls, setResponse):
         cmdGrp = None
         for i in range(len(pairs)):
             words = pairs[i].split(' ')
-            cmdLeaf, val = words[:2]
+            cmdLeaf, val = words[0:2]
             if len(words) > 2:
                 print('Warning 2-value returns not handled by TekConfig class. Ignoring...')
                 print(*words)
@@ -192,9 +196,10 @@ def fromSETresponse(cls, setResponse, subgroup=''):
         full.setList(commandList)
         if subgroup == '':
             return full
-        ret = cls()
-        ret.transfer(full, subgroup=subgroup)
-        return ret
+        else:
+            ret = cls()
+            ret.transfer(full, subgroup=subgroup)
+            return ret
 
     def save(self, fname, subgroup='', overwrite=False):
         ''' Saves dictionary parameters in json format. Merges if there's something already there, unless overwrite is True.
@@ -247,7 +252,8 @@ def __init__(self, headerIsOptional=True, verboseIsOptional=False, precedingColo
         self.colon = precedingColon
         self.space = interveningSpace
 
-        self.config = {'default': None}
+        self.config = dict()
+        self.config['default'] = None
         self.config['init'] = TekConfig()
         self.config['live'] = TekConfig()
         self.separator = self.config['live'].separator
@@ -347,7 +353,8 @@ def getDefaultFilename(self):
                 (str): the default filename
         '''
         info = self.instrID().split(',')
-        return defaultFileDir / '-'.join(info[:3]) + '.json'
+        deffile = defaultFileDir / '-'.join(info[:3]) + '.json'
+        return deffile
 
     def saveConfig(self, dest='+user', subgroup='', overwrite=False):
         '''
@@ -441,14 +448,17 @@ def _getHardwareConfig(self, cStrList):
                 cStr = cStr[:-2]
 
             try:
-                ret = self.query(f'{cStr}?')
+                ret = self.query(cStr + '?')
             except VisaIOError:
                 logger.error('Problematic parameter was %s.\n'
                              'Likely it does not exist in this instrument command structure.', cStr)
                 raise
             logger.debug('Queried %s, got %s', cStr, ret)
 
-            val = ret.split(' ')[-1] if self.header else ret
+            if self.header:
+                val = ret.split(' ')[-1]
+            else:
+                val = ret
             # Type detection
             try:
                 val = float(val)
@@ -501,9 +511,12 @@ def generateDefaults(self, filename=None, overwrite=False):
         cfgBuild = TekConfig()
 
         for cmd in allSetCmds:
-            cStr = cmd[0] if cmd[0][-1] != '&' else cmd[0][:-2]
+            if cmd[0][-1] != '&':  # handle the sibling subdir token
+                cStr = cmd[0]
+            else:
+                cStr = cmd[0][:-2]
             try:
-                val = self.query(f'{cStr}?', withTimeout=1000)
+                val = self.query(cStr + '?', withTimeout=1000)
                 cfgBuild.set(cStr, val)
                 logger.info(cStr, '<--', val)
             except VisaIOError:
diff --git a/lightlab/equipment/abstract_drivers/electrical_sources.py b/lightlab/equipment/abstract_drivers/electrical_sources.py
index e48357a0..04130bc5 100644
--- a/lightlab/equipment/abstract_drivers/electrical_sources.py
+++ b/lightlab/equipment/abstract_drivers/electrical_sources.py
@@ -42,7 +42,7 @@ def _checkMode(cls, mode):
         ''' Returns mode in lower case
         '''
         if mode not in cls.supportedModes:
-            raise TypeError(f'Invalid mode: {str(mode)}. Valid: {str(cls.supportedModes)}')
+            raise TypeError('Invalid mode: ' + str(mode) + '. Valid: ' + str(cls.supportedModes))
 
         return mode.lower()
 
@@ -58,7 +58,7 @@ def val2baseUnit(cls, value, mode):
         valueWasDict = isinstance(value, dict)
         if not valueWasDict:
             value = {-1: value}
-        baseVal = {}
+        baseVal = dict()
         for ch, vEl in value.items():
             if mode == 'baseunit':
                 baseVal[ch] = vEl
@@ -72,7 +72,10 @@ def val2baseUnit(cls, value, mode):
                 baseVal[ch] = cls.val2baseUnit(np.sign(vEl) * np.sqrt(abs(vEl)), 'amp')
             elif mode == 'mwperohm':
                 baseVal[ch] = cls.val2baseUnit(vEl / 1e3, 'wattperohm')
-        return baseVal if valueWasDict else baseVal[-1]
+        if valueWasDict:
+            return baseVal
+        else:
+            return baseVal[-1]
 
     @classmethod
     def baseUnit2val(cls, baseVal, mode):
@@ -86,7 +89,7 @@ def baseUnit2val(cls, baseVal, mode):
         baseValWasDict = isinstance(baseVal, dict)
         if not baseValWasDict:
             baseVal = {-1: baseVal}
-        value = {}
+        value = dict()
         for ch, bvEl in baseVal.items():
             if mode == 'baseunit':
                 value[ch] = bvEl
@@ -100,7 +103,10 @@ def baseUnit2val(cls, baseVal, mode):
                 value[ch] = np.sign(bvEl) * (cls.baseUnit2val(bvEl, 'amp')) ** 2
             elif mode == 'mwperohm':
                 value[ch] = cls.baseUnit2val(bvEl, 'wattperohm') * 1e3
-        return value if baseValWasDict else value[-1]
+        if baseValWasDict:
+            return value
+        else:
+            return value[-1]
 
 
 class MultiChannelSource(object):
@@ -115,16 +121,15 @@ class MultiChannelSource(object):
     def __init__(self, useChans=None, **kwargs):
         if useChans is None:
             logger.warning('No useChans specified for MultichannelSource')
-            useChans = []
+            useChans = list()
         self.useChans = useChans
         self.stateDict = dict([ch, 0] for ch in self.useChans)
 
         # Check that the requested channels are available to be blocked out
-        if self.maxChannel is not None and any(
-            ch > self.maxChannel - 1 for ch in self.useChans
-        ):
-            raise ChannelError(
-                'Requested channel is more than there are available')
+        if self.maxChannel is not None:
+            if any(ch > self.maxChannel - 1 for ch in self.useChans):
+                raise ChannelError(
+                    'Requested channel is more than there are available')
         super().__init__(**kwargs)
 
     @property
diff --git a/lightlab/equipment/abstract_drivers/multimodule_configurable.py b/lightlab/equipment/abstract_drivers/multimodule_configurable.py
index b272e995..502fbce8 100644
--- a/lightlab/equipment/abstract_drivers/multimodule_configurable.py
+++ b/lightlab/equipment/abstract_drivers/multimodule_configurable.py
@@ -32,7 +32,7 @@ def __selectSelf(self):
         ''' Writes a selector message that contains
             its prefix and its channel number
         '''
-        self.bank.write(f'{self.selectPrefix} {self.channel}')
+        self.bank.write('{} {}'.format(self.selectPrefix, self.channel))
 
     def write(self, writeStr):
         ''' Regular write in the enclosing bank, except preceded by select self
@@ -78,18 +78,16 @@ def __init__(self, useChans=None, configModule_klass=Configurable, **kwargs):
         '''
         if useChans is None:
             logger.warning('No useChans specified for MultiModuleConfigurable')
-            useChans = []
+            useChans = list()
         self.useChans = useChans
         # Check that the requested channels are available to be blocked out
-        if self.maxChannel is not None and any(
-            ch > self.maxChannel - 1 for ch in self.useChans
-        ):
-            raise ChannelError('Requested channel is more than there are available')
-
-        self.modules = [
-            configModule_klass(channel=chan, bank=self) for chan in self.useChans
-        ]
+        if self.maxChannel is not None:
+            if any(ch > self.maxChannel - 1 for ch in self.useChans):
+                raise ChannelError('Requested channel is more than there are available')
 
+        self.modules = []
+        for chan in self.useChans:
+            self.modules.append(configModule_klass(channel=chan, bank=self))
         super().__init__(**kwargs)
 
     def getConfigArray(self, cStr):
@@ -102,8 +100,11 @@ def getConfigArray(self, cStr):
                 (np.ndarray): values for all modules,
                 ordered based on the ordering of ``useChans``
         '''
-        retVals = [module.getConfigParam(cStr) for module in self.modules]
-        return np.array(retVals)
+        retVals = []
+        for module in self.modules:
+            retVals.append(module.getConfigParam(cStr))
+        retArr = np.array(retVals)
+        return retArr
 
     def setConfigArray(self, cStr, newValArr, forceHardware=False):
         ''' Iterate over modules setting the parameter to
@@ -121,16 +122,9 @@ def setConfigArray(self, cStr, newValArr, forceHardware=False):
                 (bool): did any require hardware write?
         '''
         if len(newValArr) != len(self.modules):
-            raise ChannelError(
-                (
-                    (
-                        'Wrong number of channels in array. '
-                        + f'Got {len(newValArr)}, '
-                    )
-                    + f'Expected {len(self.useChans)}.'
-                )
-            )
-
+            raise ChannelError('Wrong number of channels in array. ' +
+                               'Got {}, '.format(len(newValArr)) +
+                               'Expected {}.'.format(len(self.useChans)))
         bankWroteToHardware = False
         for module, val in zip(self.modules, newValArr):
             moduleWroteToHardware = module.setConfigParam(cStr, val, forceHardware=forceHardware)
@@ -146,7 +140,7 @@ def getConfigDict(self, cStr):
                 (dict): parameter on all the channels, keyed by channel number
         '''
         stateArr = self.getConfigArray(cStr)
-        dictOfStates = {}
+        dictOfStates = dict()
         for ch in self.useChans:
             virtualIndex = self.useChans.index(ch)
             dictOfStates[ch] = stateArr[virtualIndex]
@@ -164,13 +158,9 @@ def setConfigDict(self, cStr, newValDict, forceHardware=False):
         '''
         for chan in newValDict.keys():
             if chan not in self.useChans:
-                raise ChannelError(
-                    (
-                        ('Channel index not blocked out. ' + f'Requested {chan}, ')
-                        + f'Available {self.useChans}.'
-                    )
-                )
-
+                raise ChannelError('Channel index not blocked out. ' +
+                                   'Requested {}, '.format(chan) +
+                                   'Available {}.'.format(self.useChans))
         setArrayBuilder = self.getConfigArray(cStr)
         for iCh, chan in enumerate(self.useChans):
             if chan in newValDict.keys():
diff --git a/lightlab/equipment/abstract_drivers/power_meters.py b/lightlab/equipment/abstract_drivers/power_meters.py
index b85837cd..b58310d5 100644
--- a/lightlab/equipment/abstract_drivers/power_meters.py
+++ b/lightlab/equipment/abstract_drivers/power_meters.py
@@ -15,11 +15,9 @@ def validateChannel(self, channel):
         '''
         if channel not in self.channelDescriptions.keys():
             raise ChannelError(
-                (
-                    'Not a valid PowerMeter channel. Use '
-                    + ' '.join(f'{k} ({v})' for k, v in self.channelDescriptions)
-                )
-            )
+                'Not a valid PowerMeter channel. Use ' + ' '.join(
+                    ('{} ({})'.format(k, v)
+                     for k, v in self.channelDescriptions)))
 
     def powerLin(self, channel=1):
         return 10 ** (self.powerDbm(channel) / 10)
diff --git a/lightlab/equipment/lab_instruments/Agilent_33220_FG.py b/lightlab/equipment/lab_instruments/Agilent_33220_FG.py
index ffde5583..ed56aff0 100644
--- a/lightlab/equipment/lab_instruments/Agilent_33220_FG.py
+++ b/lightlab/equipment/lab_instruments/Agilent_33220_FG.py
@@ -28,9 +28,9 @@ def startup(self):
         # self.write('D0')  # enable output
 
     def enable(self, enaState=None):
+        wordMap = {True: 'ON', False: 'OFF'}
         trueWords = [True, 1, '1', 'ON']
         if enaState is not None:
-            wordMap = {True: 'ON', False: 'OFF'}
             self.setConfigParam('OUTP', wordMap[enaState])
         return self.getConfigParam('OUTP') in trueWords
 
@@ -50,7 +50,7 @@ def waveform(self, newWave=None):
                     self.setConfigParam('FUNC', tok.upper())
                     break
             else:
-                raise ValueError(f'{newWave} is not a valid waveform: {tokens}')
+                raise ValueError(newWave + ' is not a valid waveform: ' + str(tokens))
         return self.getConfigParam('FUNC').lower()
 
     def setArbitraryWaveform(self, wfm):
@@ -109,6 +109,5 @@ def duty(self, duty=None):
                 self.setConfigParam('FUNC:RAMP:SYMMETRY', duty)
             return self.getConfigParam('FUNC:RAMP:SYMMETRY')
         else:
-            raise ValueError(
-                f'Duty cycles are not supported with the currently selected type of waveform ({self.waveform()})'
-            )
+            raise ValueError('Duty cycles are not supported with the currently selected '
+                             'type of waveform ({})'.format(self.waveform()))
diff --git a/lightlab/equipment/lab_instruments/Agilent_N5183A_VG.py b/lightlab/equipment/lab_instruments/Agilent_N5183A_VG.py
index c4051c60..371bbab3 100644
--- a/lightlab/equipment/lab_instruments/Agilent_N5183A_VG.py
+++ b/lightlab/equipment/lab_instruments/Agilent_N5183A_VG.py
@@ -32,12 +32,12 @@ def amplitude(self, amp=None):
         '''
         if amp is not None:
             if amp > 15:
-                print(f'Warning: Agilent N5183 ony goes up to +15dBm, given {amp}dBm.')
+                print('Warning: Agilent N5183 ony goes up to +15dBm, given {}dBm.'.format(amp))
                 amp = 15
             if amp < -20:
-                print(f'Warning: Agilent N5183 ony goes down to -20dBm, given {amp}dBm.')
+                print('Warning: Agilent N5183 ony goes down to -20dBm, given {}dBm.'.format(amp))
                 amp = -20
-            self.setConfigParam('POW:AMPL', f'{amp} dBm')
+            self.setConfigParam('POW:AMPL', '{} dBm'.format(amp))
         retStr = self.getConfigParam('POW:AMPL')
         return float(retStr.split(' ')[0])
 
diff --git a/lightlab/equipment/lab_instruments/Agilent_N5222A_NA.py b/lightlab/equipment/lab_instruments/Agilent_N5222A_NA.py
index f64d68ba..f5544640 100644
--- a/lightlab/equipment/lab_instruments/Agilent_N5222A_NA.py
+++ b/lightlab/equipment/lab_instruments/Agilent_N5222A_NA.py
@@ -52,10 +52,10 @@ def amplitude(self, amp=None):
         '''
         if amp is not None:
             if amp > 30:
-                print(f'Warning: PNA ony goes up to +30dBm, given {amp}dBm.')
+                print('Warning: PNA ony goes up to +30dBm, given {}dBm.'.format(amp))
                 amp = 30
             if amp < -30:
-                print(f'Warning: R&S ony goes down to -30dBm, given {amp}dBm.')
+                print('Warning: R&S ony goes down to -30dBm, given {}dBm.'.format(amp))
                 amp = -30
             self.setConfigParam('SOUR:POW', amp)
         return self.getConfigParam('SOUR:POW')
@@ -74,7 +74,7 @@ def frequency(self, freq=None):
         '''
         if freq is not None:
             if freq > 26e9:
-                print(f'Warning: Agilent N5183 ony goes up to 40GHz, given {freq / 1e9}GHz.')
+                print('Warning: Agilent N5183 ony goes up to 40GHz, given {}GHz.'.format(freq / 1e9))
                 freq = 26e9
             if freq == self.getConfigParam('SENS:FREQ:CW'):
                 return freq
@@ -145,11 +145,11 @@ def normalize(self):
         pass
 
     def triggerSetup(self, useAux=None, handshake=None, isSlave=False):
-        prefix = f'TRIG:CHAN{self.chanNum}:AUX{self.auxTrigNum}'
-        self.setConfigParam(f'{prefix}:INT', 'SWE')
-        self.setConfigParam(f'{prefix}:POS', 'BEF')
+        prefix = 'TRIG:CHAN{}:AUX{}'.format(self.chanNum, self.auxTrigNum)
+        self.setConfigParam(prefix + ':INT', 'SWE')
+        self.setConfigParam(prefix + ':POS', 'BEF')
         self.setConfigParam('TRIG:SOUR', 'EXT' if isSlave else 'IMM')
-        self.__enaBlock(f'{prefix}:HAND', handshake)
+        self.__enaBlock(prefix + ':HAND', handshake)
         return self.__enaBlock(prefix, useAux)
 
     def getSwpDuration(self, forceHardware=False):
@@ -160,7 +160,7 @@ def measurementSetup(self, measType='S21', chanNum=None):
             chanNum = self.chanNum
         traceNum = chanNum
         # First let's see the measurements already on this channel
-        retStr = self.query(f'CALC{chanNum}:PAR:CAT:EXT?').strip('"')
+        retStr = self.query('CALC{}:PAR:CAT:EXT?'.format(chanNum)).strip('"')
         if retStr == 'NO CATALOG':
             activeMeasTypes = []
             activeMeasNames = []
@@ -168,31 +168,24 @@ def measurementSetup(self, measType='S21', chanNum=None):
             activeMeasTypes = retStr.split(',')[1::2]
             activeMeasNames = retStr.split(',')[::2]
 
-        newMeasName = f'ANT{chanNum}_{measType}'
+        newMeasName = 'ANT{}_{}'.format(chanNum, measType)
         if len(activeMeasTypes) == 1 and measType == activeMeasTypes[0] and newMeasName == activeMeasNames[0]:
             # It is already set up
             changed = False
         else:
             # Clear them
             for mName in activeMeasNames:
-                self.write(f"CALC{chanNum}:PAR:DEL '{mName}'")
+                self.write("CALC{}:PAR:DEL '{}'".format(chanNum, mName))
             # make a new measurement
-            self.setConfigParam(
-                f"CALC{chanNum}:PAR:EXT",
-                f"'{newMeasName}', '{measType}'",
-                forceHardware=True,
-            )
-
-            self.setConfigParam(
-                f'DISP:WIND:TRACE{traceNum}:FEED',
-                f"'{newMeasName}'",
-                forceHardware=True,
-            )
-
+            self.setConfigParam("CALC{}:PAR:EXT".format(chanNum), "'{}', '{}'".format(
+                newMeasName, measType), forceHardware=True)
+            self.setConfigParam('DISP:WIND:TRACE{}:FEED'.format(traceNum),
+                                "'{}'".format(newMeasName), forceHardware=True)
             changed = True
-        self.setConfigParam(
-            f'CALC{self.chanNum}:PAR:MNUM', self.chanNum, forceHardware=changed
-        )
+        self.setConfigParam('CALC{}:PAR:MNUM'.format(self.chanNum),
+                            self.chanNum, forceHardware=changed)
+
+        retStr = self.query('CALC{}:PAR:CAT:EXT?'.format(chanNum)).strip('"')
         # self.setConfigParam('CALC{}:PAR:SEL'.format(self.chanNum), self.chanNum, forceHardware=changed)
         # wait for changes to take effect
         # This could be improved by something like *OPC? corresponding to the end
@@ -211,7 +204,7 @@ def spectrum(self):
         self.setConfigParam('FORM', 'ASC')
 
         self.open()
-        dbm = self.query_ascii_values(f'CALC{self.chanNum}:DATA? FDATA')
+        dbm = self.query_ascii_values('CALC{}:DATA? FDATA'.format(self.chanNum))
         self.close()
 
         fStart = float(self.getConfigParam('SENS:FREQ:STAR'))
@@ -231,7 +224,7 @@ def multiSpectra(self, nSpect=1, livePlot=False):
                 display.clear_output()
                 display.display(plt.gcf())
             else:
-                print(f'Took spectrum {iSpect + 1} of {nSpect}')
+                print('Took spectrum {} of {}'.format(iSpect + 1, nSpect))
         print('done.')
         return bund
 
diff --git a/lightlab/equipment/lab_instruments/Anritsu_MP1763B_PPG.py b/lightlab/equipment/lab_instruments/Anritsu_MP1763B_PPG.py
index d40bc745..d936a571 100644
--- a/lightlab/equipment/lab_instruments/Anritsu_MP1763B_PPG.py
+++ b/lightlab/equipment/lab_instruments/Anritsu_MP1763B_PPG.py
@@ -54,14 +54,17 @@ def setPattern(self, bitArray):
             intVal = np.sum(bt * 2 ** np.arange(8))
             pStr += chr(intVal)
             # Switch to other endian
-            ind = i + 1 if i % 2 == 0 else i - 1
+            if i % 2 == 0:
+                ind = i + 1
+            else:
+                ind = i - 1
             intList[ind] = intVal
         byteList = bytes(intList)
 
         self.setConfigParam('PTS', 1)  # We only care to set data patterns
         # self.setConfigParam('DLN', 8*nBytes, forceHardware=True)
         self.setConfigParam('DLN', len(bitArray), forceHardware=True)
-        preamble = f'WRT {nBytes}, 0'
+        preamble = 'WRT {}, 0'.format(nBytes)
         self.write(preamble)
         self.open()
         self.mbSession.write_raw(byteList)
@@ -97,7 +100,8 @@ def syncSource(self, src=None):
             try:
                 iTok = tokens.index(src)
             except ValueError:
-                raise ValueError(f'{src} is not a valid sync source: {tokens}')
+                raise ValueError(
+                    src + ' is not a valid sync source: ' + str(tokens))
             self.setConfigParam('SOP', iTok)
         return tokens[int(self.getConfigParam('SOP'))]
 
@@ -226,15 +230,15 @@ def PRBS_pattern(cls, order, mark_ratio=0.5):
             polynomial = 0b1000000000000011  # 1+X14+X15
             seed = 0b000000000000001
         elif order == 20:
-            polynomial = (1 << 20) + (1 << 20 - 3) + (1 << 0)
+            polynomial = (1 << 20) + (1 << 20 - 3) + (1 << 20 - 20)  # 1+X3+X20
             seed = 0b00111000111000111000
         elif order == 23:
-            polynomial = (1 << 23) + (1 << 23 - 18) + (1 << 0)
+            polynomial = (1 << 23) + (1 << 23 - 18) + (1 << 23 - 23)  # 1+X18+X23
             seed = 0b1111100 << 16
         elif order == 31:
-            polynomial = (1 << 31) + (1 << 31 - 28) + (1 << 0)
+            polynomial = (1 << 31) + (1 << 31 - 28) + (1 << 31 - 31)  # 1+X28+X31
             seed = 0b1110000 << 24
         else:
-            raise NotImplementedError(f"PRBS{order} not implemented.")
+            raise NotImplementedError("PRBS{} not implemented.".format(order))
 
         return prbs_pattern(polynomial, seed)
diff --git a/lightlab/equipment/lab_instruments/Apex_AP2440A_OSA.py b/lightlab/equipment/lab_instruments/Apex_AP2440A_OSA.py
index 19486ee9..215e6727 100644
--- a/lightlab/equipment/lab_instruments/Apex_AP2440A_OSA.py
+++ b/lightlab/equipment/lab_instruments/Apex_AP2440A_OSA.py
@@ -99,7 +99,10 @@ def _query(self, queryStr):
     def query(self, queryStr, expected_talker=None):
         ret = self._query(queryStr)
         if expected_talker is not None:
-            log_function = logger.warning if ret != expected_talker else logger.debug
+            if ret != expected_talker:
+                log_function = logger.warning
+            else:
+                log_function = logger.debug
             log_function("'%s' returned '%s', expected '%s'", queryStr, ret, str(expected_talker))
         else:
             logger.debug("'%s' returned '%s'", queryStr, ret)
@@ -155,8 +158,8 @@ def wlRange(self, newRange):
         newRangeClipped = np.clip(newRange, a_min=1505.765, a_max=1572.418)
         if np.any(newRange != newRangeClipped):
             print('Warning: Requested OSA wlRange out of range. Got', newRange)
-        self.write(f'SPSTRTWL{str(np.max(newRangeClipped))}')
-        self.write(f'SPSTOPWL{str(np.min(newRangeClipped))}')
+        self.write('SPSTRTWL' + str(np.max(newRangeClipped)))
+        self.write('SPSTOPWL' + str(np.min(newRangeClipped)))
         self.__wlRange = newRangeClipped
 
     def triggerAcquire(self):
@@ -202,17 +205,18 @@ def spectrum(self, average_count=1):
         """Take a new sweep and return the new data. This is the primary user function of this class
         """
 
-        if type(average_count) != int or average_count <= 0:
-            raise RuntimeError(
-                f'average_count must be positive integer, used {average_count}'
-            )
-
+        if not (type(average_count) == int and average_count > 0):
+            raise RuntimeError('average_count must be positive integer, used {}'.format(average_count))
 
         for i in range(average_count):
             self.triggerAcquire()
             nm, dbm = self.transferData()
 
-            dbmAvg = dbm / average_count if i == 0 else dbmAvg + dbm / average_count
+            if i == 0:
+                dbmAvg = dbm / average_count
+            else:
+                dbmAvg = dbmAvg + dbm / average_count
+
         return Spectrum(nm, dbmAvg, inDbm=True)
 
     # TLS access methods currently not implemented
@@ -233,7 +237,7 @@ def tlsEnable(self, newState=None):
             if isinstance(newState, bool):
                 newState = (newState != 0)
             writeVal = '1' if newState else '0'
-            self.write(f'TLSON {writeVal}')
+            self.write('TLSON ' + writeVal)
 
     @property
     def tlsWl(self):
@@ -246,4 +250,4 @@ def tlsWl(self, newState=None):
         """newState is a float in units of nm
         """
         if newState:
-            self.write(f'TLSwl {str(newState)}')
+            self.write('TLSwl ' + str(newState))
diff --git a/lightlab/equipment/lab_instruments/Aragon_BOSA_400.py b/lightlab/equipment/lab_instruments/Aragon_BOSA_400.py
index c0a54023..f3a75c9d 100644
--- a/lightlab/equipment/lab_instruments/Aragon_BOSA_400.py
+++ b/lightlab/equipment/lab_instruments/Aragon_BOSA_400.py
@@ -2,7 +2,7 @@
 
 import numpy as np
 from lightlab.util.data import Spectrum
-import visa
+import pyvisa as visa
 import time
 import logging
 import struct
@@ -30,12 +30,12 @@ def patch_startup(func):
     def new_func(self, *args, **kwargs):
         func(self, *args, **kwargs)
         with self.connected():
-            self.send('++eot_enable 1')  # Append user defined character when EOI detected 
+            self.send('++eot_enable 1')  # Append user defined character when EOI detected
             self.send('++eot_char 10')  # Append \n (ASCII 10) when EOI is detected
     return new_func
 
 class Aragon_BOSA_400 (VISAInstrumentDriver):
-    
+
     __apps = np.array(['BOSA', 'TLS', 'CA', 'MAIN'])
     __wlRange = None
     __currApp = None
@@ -51,10 +51,11 @@ def __init__(self, name='BOSA 400 OSA', address=None, **kwargs):
         kwargs['tempSess'] = kwargs.pop('tempSess', True)
         VISAInstrumentDriver.__init__(self, name=name, address=address, **kwargs)
         self.interface = self._session_object
-        
-        if True: # TODO: detect if using prologix
-            old_startup = self.interface._prologix_rm.startup
-            self.interface._prologix_rm.startup = patch_startup(old_startup)
+        if address:
+            using_prologix = address.startswith('prologix://')
+            if using_prologix:
+                old_startup = self.interface._prologix_rm.startup
+                self.interface._prologix_rm.startup = patch_startup(old_startup)
 
     def stop(self):
         self.__currApp = str(self.ask('INST:STAT:MODE?'))
@@ -62,7 +63,7 @@ def stop(self):
             self.write('SENS:SWITCH OFF')
         else:
             self.write('INST:STAT:RUN 0')
-        
+
     def start(self):
         self.__currApp = str(self.ask('INST:STAT:MODE?'))
         if (self.__currApp == 'TLS'):
@@ -100,7 +101,7 @@ def read(self):
         log.debug("All data readed!")
         log.debug("Data received: " + message)
         return message
-        
+
     def ask(self, command):
 
         """ writes and reads data"""
@@ -108,12 +109,12 @@ def ask(self, command):
         data = ""
         data = self.query(command)
         return data
-        
+
     def application(self, app=None):
         if app is not None and app in self.__apps:
             try:
                 if app != 'MAIN':
-                    if self.__currApp is not 'MAIN':
+                    if self.__currApp != 'MAIN':
                         self.application('MAIN')
                     self.write('INST:STAT:MODE ' + str(app))
                     time.sleep(1)
@@ -163,7 +164,7 @@ def ask_TRACE_ASCII(self):
         data = self.query("TRAC?", withTimeout)
 #         data = self.query("IDN?")
         return data
-        
+
     def ask_TRACE_REAL(self):
         data = ""
         self.write("FORM REAL")
@@ -171,11 +172,11 @@ def ask_TRACE_REAL(self):
         self.write("TRAC?")
         data = self.read_TRACE_REAL_GPIB(NumPoints)
         return data
-        
+
     def read_TRACE_ASCII(self):
 
         """ read something from device"""
-        
+
         log.debug("Reading data using GPIB interface...")
         while(1):
             try:
@@ -208,7 +209,7 @@ def read_TRACE_REAL_GPIB(self,numPoints):
                 print(e)
                 raise e
         return Trace
-        
+
     def spectrum(self, form='REAL'):
         x=list()
         y=list()
@@ -242,7 +243,7 @@ def CAParam(self, avgCount='CONT', sMode='HR', noiseZero=False):
                 raise e
         else:
             log.exception("\nFor average count, please choose from ['4','8','12','32','CONT']\nFor speed mode, please choose from ['HR','HS']")
-    
+
     def CAInput(self, meas='IL', pol='1'):
         if meas in self.__CAmeasurement and pol in self.__CAPolarization:
             try:
@@ -254,7 +255,7 @@ def CAInput(self, meas='IL', pol='1'):
                 raise e
         else:
             print("\nFor measurement type, please choose from ['IL', 'RL', 'IL&RL']\nFor polarization, please choose from ['1', '2', 'INDEP', 'SIMUL']")
-    
+
     def TLSwavelength(self, waveLength=None):
         if waveLength is not None and self.__currApp == 'TLS':
             try:
diff --git a/lightlab/equipment/lab_instruments/Aragon_BOSA_400_Queens.py b/lightlab/equipment/lab_instruments/Aragon_BOSA_400_Queens.py
index 19edc36e..2cb6c8bb 100644
--- a/lightlab/equipment/lab_instruments/Aragon_BOSA_400_Queens.py
+++ b/lightlab/equipment/lab_instruments/Aragon_BOSA_400_Queens.py
@@ -2,7 +2,7 @@
 
 import numpy as np
 from lightlab.util.data import Spectrum
-import visa
+import pyvisa as visa
 import time
 import logging
 import struct
@@ -25,7 +25,7 @@
     log.addHandler(ch)
 
 class Aragon_BOSA_400_Queens (VISAInstrumentDriver):
-    
+
     __apps = np.array(['BOSA', 'TLS', 'CA', 'MAIN'])
     __wlRange = None
     __currApp = None
@@ -44,14 +44,14 @@ def __del__(self):
             self.interface.close()
         except Exception as e:
             logger.warning("Could not close instrument correctly: exception %r", e.message)
-            
+
     def stop(self):
         self.__currApp = str(self.ask('INST:STAT:MODE?'))
         if (self.__currApp == 'TLS'):
             self.write('SENS:SWITCH OFF')
         else:
             self.write('INST:STAT:RUN 0')
-        
+
     def start(self):
         self.__currApp = str(self.ask('INST:STAT:MODE?'))
         if (self.__currApp == 'TLS'):
@@ -90,7 +90,7 @@ def read(self):
         log.debug("All data readed!")
         log.debug("Data received: " + message)
         return message
-        
+
     def ask(self, command):
 
         """ writes and reads data"""
@@ -99,7 +99,7 @@ def ask(self, command):
         self.write(command)
         data = self.read()
         return data
-        
+
     def application(self, app=None):
         if app is not None and app in self.__apps:
             try:
@@ -152,7 +152,7 @@ def ask_TRACE_ASCII(self):
         self.write("TRAC?")
         data = self.read_TRACE_ASCII()
         return data
-        
+
 #     def ask_TRACE_REAL(self):
 #         data = ""
 #         self.write("FORM REAL")
@@ -160,11 +160,11 @@ def ask_TRACE_ASCII(self):
 #         self.write("TRAC?")
 #         data = self.read_TRACE_REAL_GPIB(NumPoints)
 #         return data
-        
+
     def read_TRACE_ASCII(self):
 
         """ read something from device"""
-        
+
         log.debug("Reading data using GPIB interface...")
         while(1):
             try:
@@ -199,7 +199,7 @@ def read_TRACE_ASCII(self):
 #                 print(e)
 #                 raise e
 #         return Trace
-        
+
     def spectrum(self, form='ASCII'):
         x=list()
         y=list()
@@ -231,7 +231,7 @@ def CAParam(self, avgCount='CONT', sMode='HR', noiseZero=False):
                 raise e
         else:
             log.exception("\nFor average count, please choose from ['4','8','12','32','CONT']\nFor speed mode, please choose from ['HR','HS']")
-    
+
     def CAInput(self, meas='IL', pol='1'):
         if meas in self.__CAmeasurement and pol in self.__CAPolarization:
             try:
@@ -243,7 +243,7 @@ def CAInput(self, meas='IL', pol='1'):
                 raise e
         else:
             print("\nFor measurement type, please choose from ['IL', 'RL', 'IL&RL']\nFor polarization, please choose from ['1', '2', 'INDEP', 'SIMUL']")
-    
+
     def TLSwavelength(self, waveLength=None):
         if waveLength is not None and self.__currApp == 'TLS':
             try:
diff --git a/lightlab/equipment/lab_instruments/HP_8116A_FG.py b/lightlab/equipment/lab_instruments/HP_8116A_FG.py
index a9faf12e..3d03dbbc 100644
--- a/lightlab/equipment/lab_instruments/HP_8116A_FG.py
+++ b/lightlab/equipment/lab_instruments/HP_8116A_FG.py
@@ -73,7 +73,8 @@ def waveform(self, newWave=None):
             try:
                 iTok = tokens.index(newWave)
             except ValueError:
-                raise ValueError(f'{newWave} is not a valid sync source: {tokens}')
+                raise ValueError(
+                    newWave + ' is not a valid sync source: ' + str(tokens))
             self.setConfigParam('W', iTok)
         return tokens[int(self.getConfigParam('W'))]
 
@@ -98,14 +99,14 @@ def amplAndOffs(self, amplOffs=None):
         amplitude = np.clip(amplitude, *self.amplitudeRange)
 
         if amplitude is not None:
-            self.setConfigParam('AMP', f'{amplitude} V')
+            self.setConfigParam('AMP', '{} V'.format(amplitude))
         try:
             ampl = float(self.getConfigParam('AMP').split(' ')[0])
         except VisaIOError:
             logger.error('unable to get the amplitude')
             ampl = None
         if offset is not None:
-            self.setConfigParam('OFS', f'{offset} V')
+            self.setConfigParam('OFS', '{} V'.format(offset))
         try:
             offs = float(self.getConfigParam('OFS').split(' ')[0])
         except VisaIOError:
@@ -116,6 +117,6 @@ def amplAndOffs(self, amplOffs=None):
     def duty(self, duty=None):
         ''' duty is in percentage '''
         if duty is not None:
-            self.setConfigParam('DTY', f'{duty} %')
+            self.setConfigParam('DTY', '{} %'.format(duty))
 
         return self.getConfigParam('DTY')
diff --git a/lightlab/equipment/lab_instruments/HP_8152A_PM.py b/lightlab/equipment/lab_instruments/HP_8152A_PM.py
index 40f005ce..4a3f0651 100644
--- a/lightlab/equipment/lab_instruments/HP_8152A_PM.py
+++ b/lightlab/equipment/lab_instruments/HP_8152A_PM.py
@@ -55,9 +55,9 @@ def proccessWeirdRead(readString):
         # there is an ambiguity when there are two digits, so we assume they are both intended
         onesHundredthsVals = [0] * 2
         for i, s in enumerate(onesHundredthsStrs):
-            if len(s) in {1, 2}:
+            if len(s) in [1, 2]:
                 onesHundredthsVals[i] = float(s)
-            elif len(s) in {3, 4}:  # at least one has been repeated, so skip the middle (3) or seconds (4)
+            elif len(s) in [3, 4]:  # at least one has been repeated, so skip the middle (3) or seconds (4)
                 onesHundredthsVals[i] = float(s[::2])
             else:
                 raise ValueError('Too many digits one one side of the decimal point')
@@ -67,11 +67,14 @@ def proccessWeirdRead(readString):
             val *= -1
         return str(val)
 
-    def robust_query(self, *args, **kwargs):    # pylint: disable=arguments-differ
+    def robust_query(self, *args, **kwargs):  # pylint: disable=arguments-differ
         ''' Conditionally check for read character doubling
         '''
         retRaw = self.query(*args, **kwargs)  # pylint: disable=arguments-differ
-        return self.proccessWeirdRead(retRaw) if self.doReadDoubleCheck else retRaw
+        if self.doReadDoubleCheck:
+            return self.proccessWeirdRead(retRaw)
+        else:
+            return retRaw
 
     def powerDbm(self, channel=1):
         ''' The detected optical power in dB on the specified channel
@@ -85,7 +88,7 @@ def powerDbm(self, channel=1):
         self.validateChannel(channel)
         trial = 0
         while trial < 10:  # Sometimes it gets out of range, so we have to try a few times
-            self.write(f'CH{str(channel)}')
+            self.write('CH' + str(channel))
             powStr = self.robust_query('TRG')
             v = float(powStr)
             if abs(v) < 999:  # check if it's reasonable
@@ -94,5 +97,6 @@ def powerDbm(self, channel=1):
                 # continue
                 trial += 1
         else:
-            raise Exception(f'Power meter values are unreasonable. Got {v}')
+            raise Exception('Power meter values are unreasonable.'
+                            ' Got {}'.format(v))
         return v
diff --git a/lightlab/equipment/lab_instruments/HP_8156A_VA.py b/lightlab/equipment/lab_instruments/HP_8156A_VA.py
index 1f7d8f56..a4706041 100644
--- a/lightlab/equipment/lab_instruments/HP_8156A_VA.py
+++ b/lightlab/equipment/lab_instruments/HP_8156A_VA.py
@@ -63,7 +63,7 @@ def attenLin(self, newAttenLin):
     def sendToHardware(self, sleepTime=None):
         if sleepTime is None:
             sleepTime = self.safeSleepTime
-        self.write(f'INP:ATT {str(self.attenDB)}DB')
+        self.write('INP:ATT ' + str(self.attenDB) + 'DB')
         time.sleep(sleepTime)  # Let it settle
 
     @property
diff --git a/lightlab/equipment/lab_instruments/HP_8157A_VA.py b/lightlab/equipment/lab_instruments/HP_8157A_VA.py
index de1e30ce..aca52331 100644
--- a/lightlab/equipment/lab_instruments/HP_8157A_VA.py
+++ b/lightlab/equipment/lab_instruments/HP_8157A_VA.py
@@ -78,19 +78,19 @@ def attenLin(self, newAttenLin):
     def sendToHardware(self, sleepTime=None):
         if sleepTime is None:
             sleepTime = self.safeSleepTime
-        self.write(f'ATT {str(self.attenDB)}DB')
+        self.write('ATT ' + str(self.attenDB) + 'DB')
         time.sleep(sleepTime)  # Let it settle
 
     @calibration.setter
     def calibration(self, cal_factor, sleepTime=None):   # cal_factor is in dB
         if sleepTime is None:
             sleepTime = self.safeSleepTime
-        self.write(f'CAL {str(cal_factor)}DB')
+        self.write('CAL ' + str(cal_factor) + 'DB')
         time.sleep(sleepTime)  # Let it settle
 
     @wavelength.setter
     def wavelength(self, wl, sleepTime=None):   # wl can be in m, mm, um, or nm. here we choose nm.
         if sleepTime is None:
             sleepTime = self.safeSleepTime
-        self. write(f'WVL{str(wl)}NM')
+        self. write('WVL' + str(wl) + 'NM')
         time.sleep(sleepTime)
diff --git a/lightlab/equipment/lab_instruments/ILX_7900B_LS.py b/lightlab/equipment/lab_instruments/ILX_7900B_LS.py
index 9b829b16..fb0cfd26 100644
--- a/lightlab/equipment/lab_instruments/ILX_7900B_LS.py
+++ b/lightlab/equipment/lab_instruments/ILX_7900B_LS.py
@@ -56,7 +56,7 @@ def __init__(self, name='The laser source', address=None, useChans=None, **kwarg
             useChans = kwargs.pop('dfbChans')
         if useChans is None:
             logger.warning('No useChans specified for ILX_7900B_LS')
-            useChans = []
+            useChans = list()
         VISAInstrumentDriver.__init__(self, name=name, address=address, **kwargs)
         MultiModuleConfigurable.__init__(self, useChans=useChans, configModule_klass=ILX_Module)
 
@@ -83,9 +83,8 @@ def setConfigArray(self, cStr, newValArr, forceHardware=False):
             This adds sleep functionality, only when there is a change,
             for an amount determined by the ``sleepOn`` class attribute.
         '''
-        if wroteToHardware := super().setConfigArray(
-            cStr, newValArr, forceHardware=forceHardware
-        ):
+        wroteToHardware = super().setConfigArray(cStr, newValArr, forceHardware=forceHardware)
+        if wroteToHardware:
             print('DFB settling for', self.sleepOn[cStr], 'seconds.')
             time.sleep(self.sleepOn[cStr])
             print('done.')
@@ -109,7 +108,8 @@ def enableState(self, newState):
 
         for ena in newState:
             if ena not in [0, 1]:
-                raise ValueError(('Laser states can only be 0 or 1. ' + f'Got {newState}'))
+                raise ValueError('Laser states can only be 0 or 1. ' +
+                                 'Got {}'.format(newState))
         self.setConfigArray('OUT', newState)
 
     def setChannelEnable(self, chanEnableDict):
diff --git a/lightlab/equipment/lab_instruments/Keithley_2400_SM.py b/lightlab/equipment/lab_instruments/Keithley_2400_SM.py
index 1f79f4fc..d7f547cf 100644
--- a/lightlab/equipment/lab_instruments/Keithley_2400_SM.py
+++ b/lightlab/equipment/lab_instruments/Keithley_2400_SM.py
@@ -49,12 +49,15 @@ def setPort(self, port):
             self.setConfigParam('ROUT:TERM', 'REAR')
 
     def __setSourceMode(self, isCurrentSource):
-        sourceStr, meterStr = ('CURR', 'VOLT') if isCurrentSource else ('VOLT', 'CURR')
+        if isCurrentSource:
+            sourceStr, meterStr = ('CURR', 'VOLT')
+        else:
+            sourceStr, meterStr = ('VOLT', 'CURR')
         self.setConfigParam('SOURCE:FUNC', sourceStr)
-        self.setConfigParam(f'SOURCE:{sourceStr}:MODE', 'FIXED')
+        self.setConfigParam('SOURCE:{}:MODE'.format(sourceStr), 'FIXED')
         self.setConfigParam('SENSE:FUNCTION:OFF:ALL')
-        self.setConfigParam('SENSE:FUNCTION:ON', f'"{meterStr}"')
-        self.setConfigParam(f'SENSE:{meterStr}:RANGE:AUTO', 'ON')
+        self.setConfigParam('SENSE:FUNCTION:ON', '"{}"'.format(meterStr))
+        self.setConfigParam('SENSE:{}:RANGE:AUTO'.format(meterStr), 'ON')
         self.setConfigParam('RES:MODE', 'MAN')  # Manual resistance ranging
 
     def setVoltageMode(self, protectionCurrent=0.05):
diff --git a/lightlab/equipment/lab_instruments/Keithley_2606B_SMU.py b/lightlab/equipment/lab_instruments/Keithley_2606B_SMU.py
index bd4f2ab4..baa0cb64 100644
--- a/lightlab/equipment/lab_instruments/Keithley_2606B_SMU.py
+++ b/lightlab/equipment/lab_instruments/Keithley_2606B_SMU.py
@@ -141,7 +141,10 @@ def _query(self, queryStr):
     def query(self, queryStr, expected_talker=None):
         ret = self._query(queryStr)
         if expected_talker is not None:
-            log_function = logger.warning if ret != expected_talker else logger.debug
+            if ret != expected_talker:
+                log_function = logger.warning
+            else:
+                log_function = logger.debug
             log_function(
                 "'%s' returned '%s', expected '%s'", queryStr, ret, str(expected_talker)
             )
@@ -164,7 +167,9 @@ def smu_string(self):
         elif self.channel.upper() == "B":
             return "smub"
         else:
-            raise RuntimeError(f"Unexpected channel: {self.channel}, should be 'A' or 'B'")
+            raise RuntimeError(
+                "Unexpected channel: {}, should be 'A' or 'B'".format(self.channel)
+            )
 
     @property
     def smu_full_string(self):
@@ -172,7 +177,7 @@ def smu_full_string(self):
 
     def query_print(self, query_string, expected_talker=None):
         time.sleep(0.01)
-        query_string = f"print({query_string})"
+        query_string = "print(" + query_string + ")"
         return self.query(query_string, expected_talker=expected_talker)
 
     def smu_reset(self):
@@ -232,7 +237,13 @@ def startup(self):
 
     def set_sense_mode(self, sense_mode="local"):
         ''' Set sense mode. Defaults to local sensing. '''
-        sense_mode = 1 if sense_mode == "remote" else 0
+        if sense_mode == "remote":
+            sense_mode = 1  # 1 or smuX.SENSE_REMOTE: Selects remote sense (4-wire)
+        elif sense_mode == "local":
+            sense_mode = 0  # 0 or smuX.SENSE_LOCAL: Selects local sense (2-wire)
+        else:
+            sense_mode = 0  # 0 or smuX.SENSE_LOCAL: Selects local sense (2-wire)
+
         self.write("{smuX}.sense = {sense_mode}".format(smuX=self.smu_full_string, sense_mode=sense_mode))
 
     # SourceMeter Essential methods
diff --git a/lightlab/equipment/lab_instruments/Lakeshore_Model336.py b/lightlab/equipment/lab_instruments/Lakeshore_Model336.py
index 5fa373f3..b28dde50 100644
--- a/lightlab/equipment/lab_instruments/Lakeshore_Model336.py
+++ b/lightlab/equipment/lab_instruments/Lakeshore_Model336.py
@@ -102,10 +102,10 @@ def instrID(self):
         except socket.error:
             logger.error('Lakeshore communication test failed. Sent: \'*IDN?\'')
             raise
-            
+
     def reset(self):
         self.write('*RST')
-        
+
 #     def setHeaterRange(self, output, range_val):
 #         outputs = [1,2,3,4]
 #         range_vals_12 = [0,1,2,3]
@@ -119,43 +119,43 @@ def reset(self):
 #         else:
 #             print('RANGE {},{}\r\n'.format(output, range_val))
 #             return self.write('RANGE {},{}\r\n'.format(output, range_val))
-        
+
     def getHeaterRange(self, output):
         outputs = [1,2,3,4]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 1, 2, 3, or 4'.format(output))
         else:
             return self.query('RANGE? {0}'.format(output))
-        
+
     def analogOutParamQuery(self, output):
         outputs = [3,4]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 3, or 4'.format(output))
         else:
             return self.query('ANALOG? {0}'.format(output))
-        
+
     def getHeaterOutputQuery(self, output):
         outputs = [1,2]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 1, or 2'.format(output))
         else:
             return self.query('HTR? {}'.format(output))
-        
+
     def heaterSetup(self, output):
         outputs = [1,2]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 1, or 2'.format(output))
         else:
             return self.query('HTRSET? {}'.format(output))
-        
-     def heaterSet(self, output, resistance, maxcurrent, maxcurrentuser, currentPower):
-         outputs = [1,2]
-            resistance = [1,2]
-            maxcurrent = [0,1,2,3,4]
-            currentPower = [1,2]
-            
-        
-         if output not in outputs:
+
+    def heaterSet(self, output, resistance, maxcurrent, maxcurrentuser, currentPower):
+        outputs = [1,2]
+        resistance = [1,2]
+        maxcurrent = [0,1,2,3,4]
+        currentPower = [1,2]
+
+
+        if output not in outputs:
              raise ValueError('output is {} but must be integer 1, or 2'.format(output))
         elif resistance not in resistance:
             raise ValueError('resistance is {} but must be integer 1, or 2'.format(output))
@@ -163,59 +163,58 @@ def heaterSet(self, output, resistance, maxcurrent, maxcurrentuser, currentPower
             raise ValueError('resistance is {} but must be integer 0, 1, 2, 3, or 4'.format(output))
         elif currentPower not in currentPower:
             raise ValueError('resistance is {} but must be integer 1, or 2'.format(output))
-            
-        elif (maxcurrent not 0): 
+        elif (maxcurrent != 0):
             maxcurrentuser = 0
-        elif (maxcurrent = 0)
-            maxcurrentuser = maxcurrentuser 
-         else:
+        elif (maxcurrent == 0):
+            maxcurrentuser = maxcurrentuser
+        else:
              return self.query('HTRSET {} {} {} {} {}'.format(output, resistance, maxcurrent, maxcurrentuser, currentPower))
-        
+
     def heaterStatus(self, output):
         outputs = [1,2]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 1, or 2'.format(output))
         else:
             return self.query('HTRST? {}'.format(output))
-        
+
     def getOutputMode(self, output):
         outputs = [1,2,3,4]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 1, 2, 3, or 4'.format(output))
         else:
             return self.query('OUTMODE? {}'.format(output))
-        
+
     def getAnalogOutData(self, output):
         outputs = [3,4]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 3, or 4'.format(output))
         else:
             return self.query('AOUT? {}'.format(output))
-        
+
     def getCelciusReading(self, output):
         outputs = ["A","B","C","D"]
         if output not in outputs:
             raise ValueError('output is {} but must be A, B, C, D'.format(output))
         else:
             return self.query('CRDG? {}'.format(output))
-    
+
     def tempLimitQuery(self, output):
         outputs = ["A","B","C","D"]
         if output not in outputs:
             raise ValueError('output is {} but must be A, B, C, D'.format(output))
         else:
             return self.query('TLIMIT? {}'.format(output))
-        
+
         #page 130,,  data points curve?
-        
-    
-        
+
+
+
     def warmupSupply(self, output):
         outputs = [3,4]
         if output not in outputs:
             raise ValueError('output is {} but must be integer 3, or 4'.format(output))
         else:
             return self.query('WARMUP? {}'.format(output))
-        
+
     def junctionTempQuery(self):
         self.query('TEMP?')
\ No newline at end of file
diff --git a/lightlab/equipment/lab_instruments/NI_PCI_6723.py b/lightlab/equipment/lab_instruments/NI_PCI_6723.py
index 85d4cb0e..9c9d54dc 100644
--- a/lightlab/equipment/lab_instruments/NI_PCI_6723.py
+++ b/lightlab/equipment/lab_instruments/NI_PCI_6723.py
@@ -93,7 +93,10 @@ def _query(self, queryStr):
     def query(self, queryStr, expected_talker=None):
         ret = self._query(queryStr)
         if expected_talker is not None:
-            log_function = logger.warning if ret != expected_talker else logger.debug
+            if ret != expected_talker:
+                log_function = logger.warning
+            else:
+                log_function = logger.debug
             log_function("'%s' returned '%s', expected '%s'", queryStr, ret, str(expected_talker))
         else:
             logger.debug("'%s' returned '%s'", queryStr, ret)
@@ -112,19 +115,19 @@ def instrID(self):
         return 'Current Source'
 
     def tcpTest(self, num=2):
-        print(f'x = {str(num)}')
+        print('x = ' + str(num))
         # self.open()
-        ret = self.query(f'Test: {str(num)} {str(num + .5)}')
+        ret = self.query('Test: ' + str(num) + ' ' + str(num + .5))
         # self.close()
         retNum = [0] * len(ret.split())
         for i, s in enumerate(ret.split(' ')):
             retNum[i] = float(s) + .01
-        print(f'[x+1, x+1.5] = {str(retNum)}')
+        print('[x+1, x+1.5] = ' + str(retNum))
 
     def setChannelTuning(self, chanValDict, mode, waitTime=None):  # pylint: disable=arguments-differ
         oldState = self.getChannelTuning(mode)
         # Check range and convert to base units
-        chanBaseDict = {}
+        chanBaseDict = dict()
         for ch, val in chanValDict.items():
             try:
                 enforced = self.enforceRange(val, mode)
@@ -137,7 +140,7 @@ def setChannelTuning(self, chanValDict, mode, waitTime=None):  # pylint: disable
         super().setChannelTuning(chanBaseDict)
 
         # Was there a change
-        if oldState != self.getChannelTuning(mode):
+        if not oldState == self.getChannelTuning(mode):
             self.sendToHardware(waitTime)
         else:
             self.wake()
@@ -170,7 +173,7 @@ def sendToHardware(self, waitTime=None):
         # Then write a TCPIP packet
         writeStr = 'Set:'
         for v in fullVoltageState:
-            writeStr += f' {str(v)}'
+            writeStr += ' ' + str(v)
         # self.open()
         retStr = self.query(writeStr)
         # self.close()
diff --git a/lightlab/equipment/lab_instruments/RandS_SMBV100A_VG.py b/lightlab/equipment/lab_instruments/RandS_SMBV100A_VG.py
index b0c8f49f..a32bf6ac 100644
--- a/lightlab/equipment/lab_instruments/RandS_SMBV100A_VG.py
+++ b/lightlab/equipment/lab_instruments/RandS_SMBV100A_VG.py
@@ -41,10 +41,10 @@ def amplitude(self, amp=None):
         '''
         if amp is not None:
             if amp > 30:
-                print(f'Warning: R&S ony goes up to +30dBm, given {amp}dBm.')
+                print('Warning: R&S ony goes up to +30dBm, given {}dBm.'.format(amp))
                 amp = 15
             if amp < -145:
-                print(f'Warning: R&S ony goes down to -145dBm, given {amp}dBm.')
+                print('Warning: R&S ony goes down to -145dBm, given {}dBm.'.format(amp))
                 amp = -145
             self.setConfigParam('POW', amp)
         return self.getConfigParam('POW')
@@ -134,7 +134,7 @@ def setPattern(self, bitArray):
         self.setConfigParam('BB:DM:SOUR', 'PATT')
         bitArray = np.array(bitArray, dtype=int)
         onesAndZeros = ''.join([str(b) for b in bitArray])
-        pStr = f'#B{onesAndZeros},{len(bitArray)}'
+        pStr = '#B' + onesAndZeros + ',' + str(len(bitArray))
         self.setConfigParam('BB:DM:PATT', pStr)
 
     def digiMod(self, enaState=True, symbRate=None, amExtinct=None):
@@ -199,14 +199,11 @@ def carrierMod(self, enaState=True, typMod=None, deviation=None, modFreq=None):
 
         typMod = typMod.upper()
         if typMod not in allMods:
-            raise Exception(
-                f'{typMod} is not a valid kind of carrier modulation: am, fm, or pm'
-            )
-
+            raise Exception(typMod + ' is not a valid kind of carrier modulation: am, fm, or pm')
         # Generic parameter setup
-        self.setConfigParam(f'{typMod}:SOUR', 'INT')
+        self.setConfigParam(typMod + ':SOUR', 'INT')
         if typMod in ['PM', 'FM']:
-            self.setConfigParam(f'{typMod}:MODE', 'HDEV')
+            self.setConfigParam(typMod + ':MODE', 'HDEV')
         # Specifiable parameter setup
         if deviation is not None:
             self.setConfigParam(typMod, deviation)
@@ -216,9 +213,9 @@ def carrierMod(self, enaState=True, typMod=None, deviation=None, modFreq=None):
         if enaState is not None:
             for mod in allMods:
                 if mod != typMod:
-                    self.__enaBlock(f'{mod}:STAT', False)
+                    self.__enaBlock(mod + ':STAT', False)
         # Enabling
-        return self.__enaBlock(f'{typMod}:STAT', enaState)
+        return self.__enaBlock(typMod + ':STAT', enaState)
 
     def listEnable(self, enaState=True, freqs=None, amps=None, isSlave=False, dwell=None):
         ''' Sets up list mode.
@@ -260,9 +257,10 @@ def listEnable(self, enaState=True, freqs=None, amps=None, isSlave=False, dwell=
                 amps = amps * np.ones(len(freqs))
             elif np.isscalar(freqs):
                 freqs = freqs * np.ones(len(amps))
-            elif len(amps) != len(freqs):
-                raise ValueError(
-                    'amps and freqs must have equal lengths, or one/both can be scalars or None')
+            else:
+                if len(amps) != len(freqs):
+                    raise ValueError(
+                        'amps and freqs must have equal lengths, or one/both can be scalars or None')
             self.setConfigParam('LIST:FREQ', ', '.join([str(int(f)) for f in freqs]))
             self.setConfigParam('LIST:POW', ', '.join([str(int(a)) for a in amps]))
 
diff --git a/lightlab/equipment/lab_instruments/Tektronix_DPO4032_Oscope.py b/lightlab/equipment/lab_instruments/Tektronix_DPO4032_Oscope.py
index 8505f663..b2e7039d 100644
--- a/lightlab/equipment/lab_instruments/Tektronix_DPO4032_Oscope.py
+++ b/lightlab/equipment/lab_instruments/Tektronix_DPO4032_Oscope.py
@@ -32,10 +32,8 @@ def timebaseConfig(self, avgCnt=None, duration=None):
             self.setConfigParam('DATA:START', 1)
             self.setConfigParam('DATA:STOP', int(duration * 2.5e9))
 
-        presentSettings = {
-            'avgCnt': self.getConfigParam('ACQUIRE:NUMAVG', forceHardware=True)
-        }
-
+        presentSettings = dict()
+        presentSettings['avgCnt'] = self.getConfigParam('ACQUIRE:NUMAVG', forceHardware=True)
         presentSettings['duration'] = self.getConfigParam(
             'HORIZONTAL:MAIN:SCALE', forceHardware=True)
         # presentSettings['position'] = self.getConfigParam('HORIZONTAL:MAIN:POSITION', forceHardware=True)
@@ -61,10 +59,7 @@ def __scaleData(self, voltRaw):
                 YZERO, the reference voltage, YOFF, the offset position, and
                 YSCALE, the conversion factor between position and voltage.
         '''
-        get = lambda param: float(
-            self.getConfigParam(f'WFMOUTPRE:{param}', forceHardware=True)
-        )
-
+        get = lambda param: float(self.getConfigParam('WFMOUTPRE:' + param, forceHardware=True))
         voltage = (np.array(voltRaw) - get('YOFF')) \
             * get(self._yScaleParam) \
             + get('YZERO')
@@ -103,19 +98,13 @@ def acquire(self, chans=None, timeout=None, **kwargs):
 
         for c in chans:
             if c > self.totalChans:
-                raise Exception(
-                    (
-                        f'Received channel: {str(c)}'
-                        + '. Max channels of this scope is '
-                    )
-                    + str(self.totalChans)
-                )
-
+                raise Exception('Received channel: ' + str(c) +
+                                '. Max channels of this scope is ' + str(self.totalChans))
 
         # Channel select
         for ich in range(1, 1 + self.totalChans):
             thisState = 1 if ich in chans else 0
-            self.setConfigParam(f'SELECT:CH{str(ich)}', thisState)
+            self.setConfigParam('SELECT:CH' + str(ich), thisState)
 
         isSampling = kwargs.get('avgCnt', 0) == 1
         self._setupSingleShot(isSampling)
@@ -150,7 +139,7 @@ def __transferData(self, chan):
             Todo:
                 Make this binary transfer to go even faster
         '''
-        chStr = f'CH{str(chan)}'
+        chStr = 'CH' + str(chan)
         self.setConfigParam('DATA:ENCDG', 'ASCII')
         self.setConfigParam('DATA:SOURCE', chStr)
         self.open()
diff --git a/lightlab/equipment/lab_instruments/Tektronix_DSA8300_Oscope.py b/lightlab/equipment/lab_instruments/Tektronix_DSA8300_Oscope.py
index 0d7808cd..62f3c67b 100644
--- a/lightlab/equipment/lab_instruments/Tektronix_DSA8300_Oscope.py
+++ b/lightlab/equipment/lab_instruments/Tektronix_DSA8300_Oscope.py
@@ -68,5 +68,5 @@ def histogramStats(self, chan, nWfms=3, untriggered=False):
         stdDev = self.query('HIS:STAT:STD?')
         sigmaStats = np.zeros(3)
         for iSigma in range(3):
-            sigmaStats[iSigma] = self.query(f'HIS:STAT:SIGMA{iSigma + 1}?')
+            sigmaStats[iSigma] = self.query('HIS:STAT:SIGMA{}?'.format(iSigma + 1))
         return stdDev, sigmaStats
diff --git a/lightlab/equipment/lab_instruments/Tektronix_PPG3202.py b/lightlab/equipment/lab_instruments/Tektronix_PPG3202.py
index 803514b1..2cede0f2 100644
--- a/lightlab/equipment/lab_instruments/Tektronix_PPG3202.py
+++ b/lightlab/equipment/lab_instruments/Tektronix_PPG3202.py
@@ -32,11 +32,11 @@ def __setVoltage(self, chan=None, amp=None, offset=None):
         '''
         if amp is not None and chan in self.__channels:
             time.sleep(self.__waitTime)
-            cmd = str(f':VOLT{str(chan)}:POS {str(amp)}V')
+            cmd = str(':VOLT' + str(chan) + ':POS ' + str(amp) + 'V')
             self.setConfigParam(cmd, None, True)
         if offset is not None:
             time.sleep(self.__waitTime)
-            cmd = str(f':VOLT{str(chan)}:POS:OFFS {str(offset)}V')
+            cmd = str(':VOLT' + str(chan) + ':POS:OFFS ' + str(offset) + 'V')
             self.setConfigParam(cmd, None, True)
 
     def __setPatternType(self, chan=None, ptype=None):
@@ -48,7 +48,7 @@ def __setPatternType(self, chan=None, ptype=None):
                 logger.exception('Wrong Pattern Type!')
             else:
                 time.sleep(self.__waitTime)
-                cmd = str(f':DIG{str(chan)}:PATT:TYPE {str(ptype)}')
+                cmd = str(':DIG' + str(chan) + ':PATT:TYPE ' + str(ptype))
                 self.setConfigParam(cmd, None, True)
 
     def setDataRate(self, rate=None):
@@ -60,7 +60,7 @@ def setDataRate(self, rate=None):
                 logger.exception('Invalid Data Rate!')
             else:
                 time.sleep(self.__waitTime)
-                cmd = str(f':FREQ {str(rate)}e9')
+                cmd = str(':FREQ ' + str(rate) + 'e9')
                 self.setConfigParam(cmd, None, True)
 
     def setMainParam(self, chan=None, amp=None, offset=None, ptype=None):
@@ -76,7 +76,7 @@ def setClockDivider(self, div=None):
         if div is not None:
             if (div in self.__ClockDivider):
                 time.sleep(self.__waitTime)
-                cmd = str(f':OUTP:CLOC:DIV {str(div)}')
+                cmd = str(':OUTP:CLOC:DIV ' + str(div))
                 self.setConfigParam(cmd, None, True)
             else:
                 logger.exception('Wrong Clock Divider Value!')
@@ -84,7 +84,7 @@ def setClockDivider(self, div=None):
     def setDataMemory(self, chan=None, startAddr=None, bit=None, data=None):
         if chan is not None and chan in self.__channels:
             time.sleep(self.__waitTime)
-            cmd = str(f':DIG{str(chan)}:PATT:DATA {str(startAddr)},{str(bit)},{str(data)}')
+            cmd = str(':DIG' + str(chan) + ':PATT:DATA ' + str(startAddr) + ',' + str(bit) + ',' + str(data))
             self.setConfigParam(cmd, None, True)
         else:
             logger.exception('Please choose Channel 1 or 2!')
@@ -92,10 +92,7 @@ def setDataMemory(self, chan=None, startAddr=None, bit=None, data=None):
     def setHexDataMemory(self, chan=None, startAddr=None, bit=None, Hdata=None):
         if chan is not None and chan in self.__channels:
             time.sleep(self.__waitTime)
-            cmd = str(
-                f':DIG{str(chan)}:PATT:HDAT {str(startAddr)},{str(bit)},{str(Hdata)}'
-            )
-
+            cmd = str(':DIG' + str(chan) + ':PATT:HDAT ' + str(startAddr) + ',' + str(bit) + ',' + str(Hdata))
             self.setConfigParam(cmd, None, True)
         else:
             logger.exception('Please choose Channel 1 or 2!')
@@ -103,7 +100,7 @@ def setHexDataMemory(self, chan=None, startAddr=None, bit=None, Hdata=None):
     def channelOn(self, chan=None):
         if chan is not None and chan in self.__channels:
             time.sleep(self.__waitTime)
-            cmd = str(f':OUTP{str(chan)} ON')
+            cmd = str(':OUTP' + str(chan) + ' ON')
             self.setConfigParam(cmd, None, True)
         else:
             logger.exception('Please choose Channel 1 or 2!')
@@ -111,25 +108,25 @@ def channelOn(self, chan=None):
     def channelOff(self, chan=None):
         if chan is not None and chan in self.__channels:
             time.sleep(self.__waitTime)
-            cmd = str(f':OUTP{str(chan)} OFF')
+            cmd = str(':OUTP' + str(chan) + ' OFF')
             self.setConfigParam(cmd, None, True)
         else:
             logger.exception('Please choose Channel 1 or 2!')
 
     def getAmplitude(self, chan=None):
         if chan is not None and chan in self.__channels:
-            return self.query(f':VOLT{str(chan)}:POS?')
+            return self.query(':VOLT' + str(chan) + ':POS?')
 
     def getOffset(self, chan=None):
         if chan is not None and chan in self.__channels:
-            return self.query(f':VOLT{str(chan)}:POS:OFFS?')
+            return self.query(':VOLT' + str(chan) + ':POS:OFFS?')
 
     def getDataRate(self):
         return self.query(':FREQ?')
 
     def getPatternType(self, chan=None):
         if chan is not None and chan in self.__channels:
-            return self.query(f':DIG{str(chan)}:PATT:TYPE?')
+            return self.query(':DIG' + str(chan) + ':PATT:TYPE?')
 
     def getClockDivider(self):
         return self.query(':OUTP:CLOC:DIV?')
diff --git a/lightlab/equipment/lab_instruments/Tektronix_RSA6120B_RFSA.py b/lightlab/equipment/lab_instruments/Tektronix_RSA6120B_RFSA.py
index a130ac58..6c2e5d23 100644
--- a/lightlab/equipment/lab_instruments/Tektronix_RSA6120B_RFSA.py
+++ b/lightlab/equipment/lab_instruments/Tektronix_RSA6120B_RFSA.py
@@ -36,7 +36,10 @@ def getMeasurements(self):
                 (list[str]): tokens of currently active measurements
         '''
         actWinStr = self.getConfigParam('DISP:WIND:ACT:MEAS', forceHardware=True)
-        return [aws.strip('" ') for aws in actWinStr.split(',')]
+        activeWindows = []
+        for aws in actWinStr.split(','):
+            activeWindows.append(aws.strip('" '))
+        return activeWindows
 
     def setMeasurement(self, measType='SPEC', append=False):
         ''' Turns on a measurement type
@@ -89,6 +92,10 @@ def sgramTransfer(self, duration=1., nLines=100):
             raise Exception(
                 'Spectrogram is not being recorded. Did you forget to call sgramInit()?')
 
+        # def qg(subParam):
+            # for Quick Get '''
+            # return float(self.getConfigParam('SGR:' + subParam, forceHardware=True))
+
         # Create data structure with proper size
         trialLine = self.__sgramLines([0])[0]
         nFreqs = len(trialLine)
@@ -106,7 +113,7 @@ def sgramTransfer(self, duration=1., nLines=100):
         downsample = int(duration / estTimePerLine / nLines)
         if downsample < 1:
             nLines = int(duration / estTimePerLine)
-            print(f'Warning: line density too high. You will get {nLines} lines.')
+            print('Warning: line density too high. You will get {} lines.'.format(nLines))
             downsample = 1
         lineNos = np.arange(nLines, dtype=int) * downsample
 
@@ -126,7 +133,10 @@ def sgramTransfer(self, duration=1., nLines=100):
         fBasis = np.linspace(fStart, fStop, nFreqs)
         tBasis = np.linspace(0, duration, nLines)
 
-        return Spectrogram([fBasis, tBasis], sgramMat)
+        # Put this in some kind of 2-D measured function structure.
+        gram = Spectrogram([fBasis, tBasis], sgramMat)
+
+        return gram
 
     def __sgramLines(self, lineNos, container=None, debugEvery=None):
         if container is None:
@@ -135,7 +145,7 @@ def __sgramLines(self, lineNos, container=None, debugEvery=None):
         for i, lno in enumerate(lineNos):
             if debugEvery is not None and i % debugEvery == 0:
                 logger.debug('Transferring %s / %s', lno, lineNos[-1])
-            self.write(f'TRAC:SGR:SEL:LINE {lno}')
+            self.write('TRAC:SGR:SEL:LINE {}'.format(lno))
             for _ in range(2):  # Sometimes the query just fails so we try again
                 rawLine = self.mbSession.query_binary_values('FETCH:SGR?')
                 if len(rawLine) > 0:
@@ -215,5 +225,5 @@ def __setupMultiSpectrum(self, typAvg='average', nAvg=None):
             self.write('TRACE1:SPEC:COUN:RESET')
             self.write('TRACE1:SPEC:COUN:ENABLE')
         elif typAvg != 'NONE':
-            raise Exception(f'{typAvg} is not a valid type of averaging')
+            raise Exception(typAvg + ' is not a valid type of averaging')
         self.setConfigParam('TRACE1:SPEC:FUNC', typAvg)
diff --git a/lightlab/equipment/lab_instruments/__init__.py b/lightlab/equipment/lab_instruments/__init__.py
index f01767b1..6c8267d1 100644
--- a/lightlab/equipment/lab_instruments/__init__.py
+++ b/lightlab/equipment/lab_instruments/__init__.py
@@ -10,7 +10,10 @@ class BuggyHardware(Exception):
     ''' Not all instruments behave as they are supposed to.
         This might be lab specific. atait is not sure exactly how to deal with that.
     '''
-for _, modname, _ in pkgutil.walk_packages(path=__path__, prefix=f'{__name__}.'):
+
+
+for _, modname, _ in pkgutil.walk_packages(path=__path__,  # noqa
+                                           prefix=__name__ + '.'):
     _temp = importlib.import_module(modname)
     for k, v in _temp.__dict__.items():
         if k[0] != '_' and type(v) is not type:
@@ -20,3 +23,9 @@ class BuggyHardware(Exception):
                 continue
             if VISAInstrumentDriver in mro:
                 globals()[k] = v
+
+# Disable tests for the following packages
+experimental_instruments = [
+    'Aragon_BOSA_400_Queens',
+    'Lakeshore_Model336',
+]
\ No newline at end of file
diff --git a/lightlab/equipment/visa_bases/driver_base.py b/lightlab/equipment/visa_bases/driver_base.py
index ea997d3a..809ca598 100644
--- a/lightlab/equipment/visa_bases/driver_base.py
+++ b/lightlab/equipment/visa_bases/driver_base.py
@@ -105,8 +105,9 @@ def __init__(self, ip_address, port, timeout=2, termination=LF):
         self._termination = termination
 
     def _send(self, socket, value):
-        encoded_value = (f'{value}' + self._termination).encode('ascii')
-        return socket.sendall(encoded_value)
+        encoded_value = (('%s' % value) + self._termination).encode('ascii')
+        sent = socket.sendall(encoded_value)
+        return sent
 
     def _recv(self, socket, msg_length=2048):
         received_value = socket.recv(msg_length)
@@ -139,7 +140,9 @@ def connect(self):
                 elapsed_time_ms = 1e3 * (final_time - init_time)
                 logger.debug("Connected. Time elapsed: %s msec", '{:.2f}'.format(elapsed_time_ms))
                 self._socket = s
-        return self._socket
+            return self._socket
+        else:
+            return self._socket
 
     def disconnect(self):
         ''' If connected, disconnects and kills the socket.'''
diff --git a/lightlab/equipment/visa_bases/prologix_gpib.py b/lightlab/equipment/visa_bases/prologix_gpib.py
index bec91cec..b8ac0cf0 100644
--- a/lightlab/equipment/visa_bases/prologix_gpib.py
+++ b/lightlab/equipment/visa_bases/prologix_gpib.py
@@ -139,30 +139,32 @@ def _is_valid_ip_address(ip_address):
 
 
 def _validate_hostname(hostname):
-    return bool(_is_valid_hostname(hostname) or _is_valid_ip_address(hostname))
+    if _is_valid_hostname(hostname) or _is_valid_ip_address(hostname):
+        return True
+    else:
+        return False
 
 
 def _sanitize_address(address):
     '''Takes in an address of the form 'prologix://prologix_ip_address/gpib_primary_address[:gpib_secondary_address]'
     and returns prologix_ip_address, gpib_primary_address, gpib_secondary_address.
     If secondary address is not given, gpib_secondary_address = None'''
-    if not address.startswith('prologix://'):
-        raise RuntimeError(f'invalid address: {address}')
-    _, address = address.split('prologix://', maxsplit=1)
-    ip_address, gpib_address = address.split('/', maxsplit=1)
-    if not _validate_hostname(ip_address):
-        raise RuntimeError(f"invalid ip address: '{ip_address}'")
-    try:
-        if ':' in gpib_address:
-            gpib_pad, gpib_sad = gpib_address.split(':', maxsplit=1)
-            gpib_pad, gpib_sad = int(gpib_pad), int(gpib_sad)
-        else:
-            gpib_pad, gpib_sad = int(gpib_address), None
-    except ValueError:
-        raise RuntimeError(
-            f"invalid gpib format '{gpib_address}', should be like '10[:0]'"
-        )
-
+    if address.startswith('prologix://'):
+        _, address = address.split('prologix://', maxsplit=1)
+        ip_address, gpib_address = address.split('/', maxsplit=1)
+        if not _validate_hostname(ip_address):
+            raise RuntimeError("invalid ip address: '{}'".format(ip_address))
+        try:
+            if ':' in gpib_address:
+                gpib_pad, gpib_sad = gpib_address.split(':', maxsplit=1)
+                gpib_pad, gpib_sad = int(gpib_pad), int(gpib_sad)
+            else:
+                gpib_pad, gpib_sad = int(gpib_address), None
+        except ValueError:
+            raise RuntimeError(
+                "invalid gpib format '{}', should be like '10[:0]'".format(gpib_address))
+    else:
+        raise RuntimeError('invalid address: {}'.format(address))
     return ip_address, gpib_pad, gpib_sad
 
 
@@ -177,7 +179,7 @@ def __init__(self, address=None, tempSess=False):
         '''
 
         if type(address) != str:
-            raise RuntimeError(f"Invalid address: {address}")
+            raise RuntimeError("Invalid address: {}".format(address))
 
         self.tempSess = tempSess
         self.address = address
@@ -207,8 +209,9 @@ def spoll(self):
         '''Return status byte of the instrument.'''
 
         gpib_addr = self._prologix_gpib_addr_formatted()
-        spoll = self._prologix_rm.query(f'++spoll {gpib_addr}')
-        return int(spoll.rstrip())
+        spoll = self._prologix_rm.query('++spoll {}'.format(gpib_addr))
+        status_byte = int(spoll.rstrip())
+        return status_byte
 
     def LLO(self):
         '''This command disables front panel operation of the currently addressed instrument.'''
@@ -245,9 +248,9 @@ def termination(self, value):
         elif value == '':
             eos = 3
         else:
-            print(f"Invalid termination: {repr(value)}")
+            print("Invalid termination: {}".format(repr(value)))
         if eos is not None:
-            self._prologix_rm.send(f'++eos {eos}')
+            self._prologix_rm.send('++eos {}'.format(eos))
 
     def open(self):
         ''' Open connection with instrument.
@@ -265,7 +268,7 @@ def close(self):
 
     def write(self, writeStr):
         with self._prologix_rm.connected() as pconn:
-            pconn.send(f'++addr {self._prologix_gpib_addr_formatted()}')
+            pconn.send('++addr {}'.format(self._prologix_gpib_addr_formatted()))
             pconn.send(self._prologix_escape_characters(writeStr))
     
 #     def read(self, withTimeout=None):
@@ -290,7 +293,7 @@ def wait(self, bigMsTimeout=10000):
     def clear(self):
         '''This command sends the Selected Device Clear (SDC) message to the currently specified GPIB address.'''
         with self._prologix_rm.connected() as pconn:
-            pconn.send(f'++addr {self._prologix_gpib_addr_formatted()}')
+            pconn.send('++addr {}'.format(self._prologix_gpib_addr_formatted()))
             pconn.send('++clr')
             
     def query_raw_binary(self, queryStr, withTimeout=None):
@@ -299,7 +302,7 @@ def query_raw_binary(self, queryStr, withTimeout=None):
            are stripped. Also no decoding.'''
 
         with self._prologix_rm.connected() as pconn:
-            pconn.send(f'++addr {self._prologix_gpib_addr_formatted()}')
+            pconn.send('++addr {}'.format(self._prologix_gpib_addr_formatted()))
             pconn.send(self._prologix_escape_characters(queryStr))
 
         if withTimeout is not None:
diff --git a/lightlab/equipment/visa_bases/visa_driver.py b/lightlab/equipment/visa_bases/visa_driver.py
index d768c914..620d28fb 100644
--- a/lightlab/equipment/visa_bases/visa_driver.py
+++ b/lightlab/equipment/visa_bases/visa_driver.py
@@ -15,16 +15,11 @@ class _AttrGetter(object):
     # https://stackoverflow.com/questions/28861064/super-object-has-no-attribute-getattr-in-python3
 
     def __getattr__(self, name):
-        raise AttributeError(f"'{str(self)}' has no attribute '{name}'")
+        raise AttributeError("'{}' has no attribute '{}'".format(str(self), name))
 
 
-_InstrumentSessionBase_methods = [
-    name
-    for name, _ in inspect.getmembers(
-        InstrumentSessionBase,
-        lambda o: inspect.isfunction(o) or isinstance(o, property),
-    )
-]
+_InstrumentSessionBase_methods = list(name for name, _ in inspect.getmembers(
+    InstrumentSessionBase, lambda o: inspect.isfunction(o) or isinstance(o, property)))
 
 
 class InstrumentSession(_AttrGetter):
@@ -66,38 +61,37 @@ def close(self):
     def __getattr__(self, name):
         if name in ('_session_object'):
             return super().__getattr__(name)
-        try:
-            return_attr = getattr(self._session_object, name)
-        except AttributeError:
-            return_attr = super().__getattr__(name)  # pylint: disable=assignment-from-no-return
         else:
-            if name not in _InstrumentSessionBase_methods:
-                visalogger.warning("Access to %s.%s will be deprecated soon. "
-                                   "Please include it in InstrumentSessionBase. "
-                                   "", type(self._session_object).__name__, name)
-
-        return return_attr
+            try:
+                return_attr = getattr(self._session_object, name)
+            except AttributeError:
+                return_attr = super().__getattr__(name)  # pylint: disable=assignment-from-no-return
+            else:
+                if name not in _InstrumentSessionBase_methods:
+                    visalogger.warning("Access to %s.%s will be deprecated soon. "
+                                       "Please include it in InstrumentSessionBase. "
+                                       "", type(self._session_object).__name__, name)
+
+            return return_attr
 
     def __dir__(self):
         return set(super().__dir__() + list(_InstrumentSessionBase_methods))
 
     def __setattr__(self, name, value):
-        if (
-            name in ('_session_object')
-            or name not in 'address'
-            and not hasattr(self._session_object, name)
-        ):
+        if name in ('_session_object'):
             super().__setattr__(name, value)
-        elif name in 'address':
+        elif name in ('address'):
             super().__setattr__(name, value)
             if self._session_object is not None and self._session_object.address != value:
                 tempSess = self._session_object.tempSess
                 self.reinstantiate_session(address=value, tempSess=tempSess)
-        else:
+        elif hasattr(self._session_object, name):
             setattr(self._session_object, name, value)
             # also change in local dictionary if possible
             if name in self.__dict__:
                 self.__dict__[name] = value
+        else:
+            super().__setattr__(name, value)
 
 
 class IncompleteClass(Exception):
@@ -124,14 +118,8 @@ def __init__(cls, name, bases, dct):
             inst_klass = cls.instrument_category
             for essential in inst_klass.essentialMethods + inst_klass.essentialProperties:
                 if essential not in dir(cls):
-                    raise IncompleteClass(
-                        (
-                            cls.__name__
-                            + f' does not implement {essential}, '
-                            + f'which is essential for {inst_klass.__name__}'
-                        )
-                    )
-
+                    raise IncompleteClass(cls.__name__ + ' does not implement {}, '.format(essential) +
+                                          'which is essential for {}'.format(inst_klass.__name__))
         super().__init__(name, bases, dct)
 
     def __call__(cls, name=None, address=None, *args, **kwargs):
diff --git a/lightlab/laboratory/__init__.py b/lightlab/laboratory/__init__.py
index 5ccd461f..68cd097e 100644
--- a/lightlab/laboratory/__init__.py
+++ b/lightlab/laboratory/__init__.py
@@ -45,18 +45,22 @@ class Hashable(object):
 
     No instance variables starting with "__" will be serialized.
     """
-    context = jsonpickle.pickler.Pickler(
-        unpicklable=True, warn=True, keys=True)
 
-    def __eq__(self, other):
+
+    def __json_self(self):
+        context = jsonpickle.pickler.Pickler(
+            unpicklable=True, warn=True, keys=True)
         jsonpickle.set_encoder_options('json', sort_keys=True)
-        json_self = self.context.flatten(self, reset=True)
-        json_other = self.context.flatten(other, reset=True)
+        json_self = context.flatten(self, reset=True)
+        return json_self
+
+    def __eq__(self, other):
+        json_self = self.__json_self()
+        json_other = Hashable.__json_self(other)
         return json_self == json_other
 
     def __hash__(self):
-        jsonpickle.set_encoder_options('json', sort_keys=True)
-        json_self = self.context.flatten(self, reset=True)
+        json_self = self.__json_self()
         return hash(jsonpickle.json.encode(json_self))
 
     def __init__(self, **kwargs):
@@ -70,14 +74,18 @@ def __getstate__(self):
         serialization. Variables named as such are actually stored
         different in self.__dict__. Check PEP 8.
         '''
-        klassnames = [self.__class__.__name__.lstrip('_')]
-        klassnames.extend(base.__name__.lstrip('_') for base in self.__class__.mro())
+        klassnames = []
+        klassnames.append(self.__class__.__name__.lstrip('_'))
+
+        for base in self.__class__.mro():
+            klassnames.append(base.__name__.lstrip('_'))
+
         state = self.__dict__.copy()
         keys_to_delete = set()
         for key in state.keys():
             if isinstance(key, str):
                 for klassname in klassnames:
-                    if key.startswith(f"_{klassname}__"):
+                    if key.startswith("_{}__".format(klassname)):
                         keys_to_delete.add(key)
                     if key.startswith("__"):
                         keys_to_delete.add(key)
@@ -145,7 +153,7 @@ class NamedList(MutableSequence, Hashable):
     read_only = False
 
     def __init__(self, *args, read_only=False):  # pylint: disable=super-init-not-called
-        self.list = []
+        self.list = list()
         self.extend(list(args))
         self.read_only = read_only
 
@@ -169,13 +177,17 @@ def check(self, value):
             raise TypeError(f"{type(value)} does not have name.")
 
     def check_presence(self, name):
-        return [idx for idx, elem in enumerate(self) if elem.name == name]
+        matching_idxs = [idx for idx, elem in enumerate(
+            self) if elem.name == name]
+        return matching_idxs
 
     def __len__(self):
         return len(self.list)
 
     def __getitem__(self, i):
-        return self.dict[i] if isinstance(i, str) else self.list[i]
+        if isinstance(i, str):
+            return self.dict[i]
+        return self.list[i]
 
     def __delitem__(self, i):
         if self.read_only:
diff --git a/lightlab/laboratory/experiments.py b/lightlab/laboratory/experiments.py
index a2f9cea7..08f05f9f 100644
--- a/lightlab/laboratory/experiments.py
+++ b/lightlab/laboratory/experiments.py
@@ -44,7 +44,10 @@ class Experiment(Virtualizable):
 
     @property
     def lab(self):
-        return labstate.lab if self._lab is None else self._lab
+        if self._lab is None:
+            return labstate.lab
+        else:
+            return self._lab
 
     @lab.setter
     def lab(self, value):
@@ -67,11 +70,17 @@ def is_valid(self, reset=True):
 
     def __init__(self, instruments=None, devices=None, **kwargs):
         super().__init__()
-        self.instruments = instruments if instruments is not None else list()
-        self.instruments_requirements = []
-        self.devices = devices if devices is not None else list()
-        self.validate_exprs = []
-        self.connections = []
+        if instruments is not None:
+            self.instruments = instruments
+        else:
+            self.instruments = list()
+        self.instruments_requirements = list()
+        if devices is not None:
+            self.devices = devices
+        else:
+            self.devices = list()
+        self.validate_exprs = list()
+        self.connections = list()
 
         self.name = kwargs.pop("name", None)
         self.startup(**kwargs)
@@ -171,8 +180,9 @@ def registerConnections(self, *connections):
             def connection_present(connection=connection, connections=self.lab.connections):
                 if connection in connections:
                     return True
-                logger.error("Connection {} is not compatible with lab %s", connection)
-                return False
+                else:
+                    logger.error("Connection {} is not compatible with lab %s", connection)
+                    return False
             self.validate_exprs.append(connection_present)
 
     def validate(self):
@@ -194,37 +204,35 @@ def unlock(self):
 
     def __str__(self):
         if self.name is not None:
-            return f"Experiment {self.name}"
-        return f"Experiment {self.__class__.__name__}"
+            return "Experiment {}".format(self.name)
+        return "Experiment {}".format(self.__class__.__name__)
 
     def display(self):
-        lines = [f"{self}"]
+        lines = ["{}".format(self)]
         if self.valid:
             lines.append("Experiment is online!")
         else:
             lines.append("Experiment is offline.")
-        lines.extend(("===========", "Expected Instruments", "==========="))
+        lines.append("===========")
+        lines.append("Expected Instruments")
+        lines.append("===========")
         if len(self.instruments_requirements) > 0:
-            lines.extend(
-                [
-                    f"   {str(instrument)} in ({str(host)}, {str(bench)})"
-                    for instrument, host, bench in self.instruments_requirements
-                ]
-            )
-
+            lines.extend(["   {} in ({}, {})".format(str(instrument), str(host), str(bench))
+                          for instrument, host, bench
+                          in self.instruments_requirements])
         else:
             lines.append("   No instruments.")
-        lines.extend(("=======", "Expected Connections", "======="))
+        lines.append("=======")
+        lines.append("Expected Connections")
+        lines.append("=======")
         if len(self.connections) > 0:
             for connection in self.connections:
                 connection_items = list(connection.items())
                 from_dev, from_port = tuple(connection_items[0])
                 to_dev, to_port = tuple(connection_items[1])
 
-                lines.append(
-                    f"   {str(from_dev)}/{str(from_port)} <-> {str(to_dev)}/{str(to_port)}"
-                )
-
+                lines.append("   {}/{} <-> {}/{}".format(str(from_dev), str(from_port),
+                                                         str(to_dev), str(to_port)))
         else:
             lines.append("   No connections.")
         lines.append("***")
diff --git a/lightlab/laboratory/instruments/bases.py b/lightlab/laboratory/instruments/bases.py
index 457c3af6..3cf83345 100644
--- a/lightlab/laboratory/instruments/bases.py
+++ b/lightlab/laboratory/instruments/bases.py
@@ -53,7 +53,7 @@ def isLive(self):
         '''
         if self.hostname is not None:
             logger.debug("Pinging %s...", self.hostname)
-            response = os.system(f"ping -c 1 {self.hostname}")
+            response = os.system("ping -c 1 {}".format(self.hostname))
             if response != 0:
                 logger.warning("%s is not reachable via ping.", self)
             return response == 0
@@ -69,7 +69,7 @@ def _visa_prefix(self):
             Returns:
                 (str)
         '''
-        return f'visa://{self.hostname}/'
+        return 'visa://{}/'.format(self.hostname)
 
     def gpib_port_to_address(self, port, board=0):
         '''
@@ -80,7 +80,7 @@ def gpib_port_to_address(self, port, board=0):
             Returns:
                 (str): the address that can be used in an initializer
         '''
-        localSerialStr = f'GPIB{board}::{port}::INSTR'
+        localSerialStr = 'GPIB{}::{}::INSTR'.format(board, port)
         return self._visa_prefix() + localSerialStr
 
     def list_resources_info(self, use_cached=True):
@@ -96,13 +96,15 @@ def list_resources_info(self, use_cached=True):
         """
         if self.__cached_list_resources_info is None:
             use_cached = False
-        if not use_cached:
-            list_query = f"{self._visa_prefix()}?*::INSTR"
+        if use_cached:
+            return self.__cached_list_resources_info
+        else:
+            list_query = self._visa_prefix() + "?*::INSTR"
             rm = pyvisa.ResourceManager()
             logger.debug("Caching resource list in %s", self)
             self.__cached_list_resources_info = rm.list_resources_info(
                 query=list_query)
-        return self.__cached_list_resources_info
+            return self.__cached_list_resources_info
 
     def list_gpib_resources_info(self, use_cached=True):
         """ Like :meth:`list_resources_info`, but only returns gpib
@@ -137,17 +139,18 @@ def get_all_gpib_id(self, use_cached=True):
             use_cached = False
         if use_cached:
             return self.__cached_gpib_instrument_list
-        gpib_instrument_list = {}
-        logger.debug("Caching GPIB instrument list in %s", self)
-        for gpib_address in gpib_resources.keys():
-            visa_object = VISAObject(gpib_address, tempSess=True)
-            try:
-                instr_id = visa_object.instrID()
-                gpib_instrument_list[gpib_address] = instr_id
-            except pyvisa.VisaIOError as err:
-                logger.error(err)
-        self.__cached_gpib_instrument_list = gpib_instrument_list
-        return gpib_instrument_list
+        else:
+            gpib_instrument_list = dict()
+            logger.debug("Caching GPIB instrument list in %s", self)
+            for gpib_address in gpib_resources.keys():
+                visa_object = VISAObject(gpib_address, tempSess=True)
+                try:
+                    instr_id = visa_object.instrID()
+                    gpib_instrument_list[gpib_address] = instr_id
+                except pyvisa.VisaIOError as err:
+                    logger.error(err)
+            self.__cached_gpib_instrument_list = gpib_instrument_list
+            return gpib_instrument_list
 
     def findGpibAddressById(self, id_string_search, use_cached=True):
         """ Finds a gpib address using :meth:`get_all_gpib_id`, given
@@ -170,7 +173,8 @@ def findGpibAddressById(self, id_string_search, use_cached=True):
                 logger.info("Found %s in %s.", id_string_search, gpib_address)
                 return gpib_address
         logger.warning("%s not found in %s", id_string_search, self)
-        raise NotFoundError(f"{id_string_search} not found in {self}")
+        raise NotFoundError(
+            "{} not found in {}".format(id_string_search, self))
 
     def addInstrument(self, *instruments):
         r""" Adds an instrument to lab.instruments if it is not already present.
@@ -218,19 +222,17 @@ def checkInstrumentsLive(self):
         return all_live
 
     def __str__(self):
-        return f"Host {self.name}"
+        return "Host {}".format(self.name)
 
     def display(self):
         """ Displays the host's instrument table in a nice format."""
-        lines = [f"{self}", "===========", "Instruments", "==========="]
+        lines = ["{}".format(self)]
+        lines.append("===========")
+        lines.append("Instruments")
+        lines.append("===========")
         if len(self.instruments) > 0:
-            lines.extend(
-                [
-                    f"   {str(instrument)} ({str(instrument.host)})"
-                    for instrument in self.instruments
-                ]
-            )
-
+            lines.extend(["   {} ({})".format(str(instrument), str(instrument.host))
+                          for instrument in self.instruments])
         else:
             lines.append("   No instruments.")
         lines.append("***")
@@ -358,27 +360,27 @@ def removeDevice(self, *devices):
 
     def display(self):
         """ Displays the bench's table in a nice format."""
-        lines = [f"{self}", "===========", "Instruments", "==========="]
+        lines = ["{}".format(self)]
+        lines.append("===========")
+        lines.append("Instruments")
+        lines.append("===========")
         if len(self.instruments) > 0:
-            lines.extend(
-                [
-                    f"   {str(instrument)} ({str(instrument.host)})"
-                    for instrument in self.instruments
-                ]
-            )
-
+            lines.extend(["   {} ({})".format(str(instrument), str(instrument.host))
+                          for instrument in self.instruments])
         else:
             lines.append("   No instruments.")
-        lines.extend(("=======", "Devices", "======="))
+        lines.append("=======")
+        lines.append("Devices")
+        lines.append("=======")
         if len(self.devices) > 0:
-            lines.extend([f"   {str(device)}" for device in self.devices])
+            lines.extend(["   {}".format(str(device)) for device in self.devices])
         else:
             lines.append("   No devices.")
         lines.append("***")
         print("\n".join(lines))
 
     def __str__(self):
-        return f"Bench {self.name}"
+        return "Bench {}".format(self.name)
 
 
 class Instrument(Node):
@@ -441,11 +443,11 @@ def __dir__(self):
 
     @property
     def implementedOptionals(self):
-        return [
-            opAttr
-            for opAttr in self.optionalAttributes
-            if hasattr(self._driver_class, opAttr)
-        ]
+        implementedOptionals = list()
+        for opAttr in self.optionalAttributes:
+            if hasattr(self._driver_class, opAttr):
+                implementedOptionals.append(opAttr)
+        return implementedOptionals
 
     # These control feedthroughs to the driver
     def __getattr__(self, attrName):
@@ -475,9 +477,10 @@ def __setattr__(self, attrName, newVal):
                 + self.implementedOptionals:
             setattr(self.driver, attrName, newVal)
         else:
-            if attrName == 'address' and self.__driver_object is not None:
-                self.__driver_object.close()
-                self.__driver_object.address = newVal
+            if attrName == 'address':  # Reinitialize the driver
+                if self.__driver_object is not None:
+                    self.__driver_object.close()
+                    self.__driver_object.address = newVal
             super().__setattr__(mangle(attrName, self.__class__.__name__), newVal)
 
     def __delattr__(self, attrName):
@@ -534,10 +537,11 @@ def driver_class(self):
         This way the object knows how to instantiate a driver instance
         from the labstate.
         """
-        if self._driver_class is not None:
+        if self._driver_class is None:
+            logger.warning("Using default driver for %s.", self)
+            return DefaultDriver
+        else:
             return self._driver_class
-        logger.warning("Using default driver for %s.", self)
-        return DefaultDriver
 
     @property
     def driver_object(self):
@@ -546,7 +550,7 @@ def driver_object(self):
             try:
                 kwargs = self.driver_kwargs
             except AttributeError:  # Fall back to the jank version where we try to guess what is important
-                kwargs = {}
+                kwargs = dict()
                 for kwarg in ["useChans", "elChans", "dfbChans", "stateDict", "sourceMode"]:
                     try:
                         kwargs[kwarg] = getattr(self, kwarg)
@@ -581,42 +585,38 @@ def id_string(self):
         return self._id_string
 
     def __str__(self):
-        return f"{self.name}"
+        return "{}".format(self.name)
 
     def __repr__(self):
-        return f"<{self.__class__.__name__} name={self.name}, address={self.address}, id={id(self)}>"
+        return "<{} name={}, address={}, id={}>".format(self.__class__.__name__,
+                                                        self.name, self.address, id(self))
 
     def display(self):
         """ Displays the instrument's info table in a nice format."""
-        lines = [
-            f"{self}",
-            f"Bench: {self.bench}",
-            f"Host: {self.host}",
-            f"address: {self.address}",
-            f"id_string: {self.id_string}",
-        ]
-
+        lines = ["{}".format(self)]
+        lines.append("Bench: {}".format(self.bench))
+        lines.append("Host: {}".format(self.host))
+        lines.append("address: {}".format(self.address))
+        lines.append("id_string: {}".format(self.id_string))
         if not self.id_string:
             lines.append("The id_string should match the value returned by"
                          " self.driver.instrID(), and is checked by the command"
                          " self.isLive() in order to authenticate that the intrument"
                          " in that address is the intended one.")
-        lines.extend(
-            (
-                f"driver_class: {self.driver_class.__name__}",
-                "=====",
-                "Ports",
-                "=====",
-            )
-        )
-
+        lines.append("driver_class: {}".format(self.driver_class.__name__))
+        lines.append("=====")
+        lines.append("Ports")
+        lines.append("=====")
         if len(self.ports) > 0:
-            lines.extend([f"   {str(port)}" for port in self.ports])
+            lines.extend(["   {}".format(str(port)) for port in self.ports])
         else:
             lines.append("   No ports.")
         if hasattr(self, 'driver_kwargs'):
-            lines.extend(("=====", "Driver kwargs", "====="))
-            lines.extend(f"   {str(k)} = {str(v)}" for k, v in self.driver_kwargs.items())
+            lines.append("=====")
+            lines.append("Driver kwargs")
+            lines.append("=====")
+            for k, v in self.driver_kwargs.items():
+                lines.append("   {} = {}".format(str(k), str(v)))
         lines.append("***")
         print("\n".join(lines))
 
@@ -676,9 +676,7 @@ class MockInstrument(Instrument):
 
     def __getattr__(self, attrName):
         def noop(*args, **kwargs):
-            raise AttributeError(
-                f"Attempted to call method ('{attrName}') of a mock Instrument."
-            )
+            raise AttributeError("Attempted to call method ('{}') of a mock Instrument.".format(attrName))
         return noop
 
 
@@ -709,13 +707,17 @@ def __init__(self, name, **kwargs):
     bench = typed_property(Bench, '_bench')
 
     def __str__(self):
-        return f"Device {self.name}"
+        return "Device {}".format(self.name)
 
     def display(self):
         """ Displays the device's info table in a nice format."""
-        lines = [f"{self}", f"Bench: {self.bench}", "=====", "Ports", "====="]
+        lines = ["{}".format(self)]
+        lines.append("Bench: {}".format(self.bench))
+        lines.append("=====")
+        lines.append("Ports")
+        lines.append("=====")
         if len(self.ports) > 0:
-            lines.extend([f"   {str(port)}" for port in self.ports])
+            lines.extend(["   {}".format(str(port)) for port in self.ports])
         else:
             lines.append("   No ports.")
         lines.append("***")
diff --git a/lightlab/laboratory/state.py b/lightlab/laboratory/state.py
index a6c188e5..9b259dde 100644
--- a/lightlab/laboratory/state.py
+++ b/lightlab/laboratory/state.py
@@ -113,7 +113,7 @@ def instruments_dict(self):  # TODO DEPRECATE
     def __init__(self, filename=None):
         self.hosts = TypedList(Host)
         self.benches = TypedList(Bench)
-        self.connections = []
+        self.connections = list()
         self.devices = TypedList(Device)
         self.instruments = TypedList(Instrument)
         if filename is None:
@@ -370,12 +370,8 @@ def loadState(cls, filename=None, validateHash=True):
         sha256 = json_state.pop("__sha256__")
         jsonpickle.set_encoder_options('json', sort_keys=True, indent=4)
         if validateHash and sha256 != hash_sha256(json.encode(json_state)):
-            raise JSONDecodeError(
-                f"Labstate is corrupted. expected: {sha256} vs actual: {hash_sha256(json.encode(json_state))}.",
-                str(filename),
-                0,
-            )
-
+            raise JSONDecodeError("Labstate is corrupted. expected: {} vs actual: {}.".format(
+                sha256, hash_sha256(json.encode(json_state))), str(filename), 0)
 
         # Compare versions of file vs. class
         version = json_state.pop("__version__")
@@ -430,7 +426,10 @@ def _toJSON(self):
     @property
     def filename(self):
         """ Filename used to serialize labstate."""
-        return _filename if self.__filename__ is None else self.__filename__
+        if self.__filename__ is None:
+            return _filename
+        else:
+            return self.__filename__
 
     @filename.setter
     def filename(self, fname):
@@ -461,12 +460,13 @@ def saveState(self, fname=None, save_backup=True):
             self._saveState(fname, save_backup=False)
             return
         except JSONDecodeError:
-            if os.stat(fname).st_size != 0:
+            if os.stat(fname).st_size == 0:
+                logger.warning("%s is empty. Saving for the first time.", _filename)
+                self._saveState(fname, save_backup=False)
+                return
+            else:
                 raise
 
-            logger.warning("%s is empty. Saving for the first time.", _filename)
-            self._saveState(fname, save_backup=False)
-            return
         if not self.__sha256__:
             logger.debug("Attempting to compare fabricated labstate vs. preloaded one.")
             self.__sha256__ = self.__toJSON()["__sha256__"]
@@ -489,14 +489,13 @@ def _saveState(self, fname=None, save_backup=True):
         filepath = Path(fname).resolve()
 
         # it is good to backup this file in caseit exists
-        if save_backup and filepath.exists():
+        if save_backup:
+            if filepath.exists():  # pylint: disable=no-member
                 # gets folder/filename.* and transforms into folder/filename_{timestamp}.json
-            filepath_backup = Path(filepath).with_name(
-                f"{filepath.stem}_{timestamp_string()}.json"
-            )
-
-            logger.debug("Backup %s to %s", filepath, filepath_backup)
-            shutil.copy2(filepath, filepath_backup)
+                filepath_backup = Path(filepath).with_name(
+                    "{}_{}.json".format(filepath.stem, timestamp_string()))
+                logger.debug("Backup %s to %s", filepath, filepath_backup)
+                shutil.copy2(filepath, filepath_backup)
 
         # save to filepath, overwriting
         filepath.touch()  # pylint: disable=no-member
@@ -515,6 +514,13 @@ def init_module(module):
     except (OSError) as e:
         logger.error("%s: %s.", e.__class__.__name__, e)
         empty_lab = True
+    except JSONDecodeError as e:
+        if os.stat(_filename).st_size == 0:
+            logger.warning("%s is empty.", _filename)
+        else:
+            logger.error("%s: %s is corrupted. %s.", e.__class__.__name__, _filename, e)
+        empty_lab = True
+
     if empty_lab:
         logger.warning("Starting fresh new LabState(). "
                        "Save for the first time with lab._saveState()")
diff --git a/lightlab/laboratory/virtualization.py b/lightlab/laboratory/virtualization.py
index c3a3e927..7b487847 100644
--- a/lightlab/laboratory/virtualization.py
+++ b/lightlab/laboratory/virtualization.py
@@ -32,7 +32,7 @@ class Virtualizable(object):
     synced = None
 
     def __init__(self):
-        self.synced = []
+        self.synced = list()
 
     def synchronize(self, *newVirtualizables):
         r''' Adds another object that this one will put in the same virtual
@@ -56,7 +56,7 @@ def __setAll(self, toVirtual):
             Returns:
                 (list): the previous virtual states
         '''
-        old_values = []
+        old_values = list()
         for sub in ([self] + self.synced):
             old_values.append(sub._virtual)  # pylint: disable=protected-access
             sub._virtual = toVirtual  # pylint: disable=protected-access
@@ -202,21 +202,18 @@ def __init__(self, real_obj=None, virt_obj=None):
         self.real_obj = real_obj
         self.virt_obj = virt_obj
         if real_obj is not None and virt_obj is not None:
+            violated = []
             allowed = real_obj.essentialMethods + \
                 real_obj.essentialProperties + dir(VirtualInstrument)
-            if violated := [
-                attr
-                for attr in dir(type(virt_obj))
-                if attr not in allowed and '__' not in attr
-            ]:
-                logger.warning(
-                    (
-                        f'Virtual instrument ({type(virt_obj).__name__}) violates '
-                        + f'interface of the real one ({type(real_obj).__name__})'
-                    )
-                )
-
+            for attr in dir(type(virt_obj)):
+                if attr not in allowed \
+                        and '__' not in attr:
+                    violated.append(attr)
+            if len(violated) > 0:
+                logger.warning('Virtual instrument ({}) violates '.format(type(virt_obj).__name__) +
+                               'interface of the real one ({})'.format(type(real_obj).__name__))
                 logger.warning('Got: ' + ', '.join(violated))  # pylint: disable=logging-not-lazy
+                # logger.warning('Allowed: ' + ', '.join(filter(lambda x: '__' not in x, allowed)))
         self.synced = []
         super().__init__()
 
diff --git a/lightlab/util/characterize.py b/lightlab/util/characterize.py
index 384baf3c..e8542df1 100644
--- a/lightlab/util/characterize.py
+++ b/lightlab/util/characterize.py
@@ -11,7 +11,7 @@
 from .data import FunctionBundle
 
 
-def strobeTest(fActuate, fSense, fReset=None, nPts=10, maxDelay=1, visualize=True):    # pylint: disable=W0613
+def strobeTest(fActuate, fSense, fReset=None, nPts=10, maxDelay=1, visualize=True):  # pylint: disable=W0613
     ''' Looks at a sense variable at different delays after calling an actuate function.
         Good for determining the time needed to wait for settling.
         Calls each function once per delay point to construct a picture like the strobe experiment, or a sampling scope
@@ -29,7 +29,10 @@ def strobeTest(fActuate, fSense, fReset=None, nPts=10, maxDelay=1, visualize=Tru
 
     # Figure out if sense is scalar or array; wrap accordingly
     testV = fSense()
-    w = 1 if np.isscalar(testV) else len(testV)
+    if np.isscalar(testV):
+        w = 1
+    else:
+        w = len(testV)
     fWrapped = lambda: np.reshape(np.array(fSense()), (1, w))
 
     t = np.zeros((nPts, 1))
@@ -142,7 +145,10 @@ def monitorVariable(fValue, sleepSec=0, nReps=100, plotEvery=1):
     curves = None
 
     testV = fValue()
-    w = 1 if np.isscalar(testV) else len(testV)
+    if np.isscalar(testV):
+        w = 1
+    else:
+        w = len(testV)
     fWrapped = lambda: np.reshape(np.array(fValue()), (1, w))
     t0 = time.time()
     timeFun = lambda: time.time() - t0
diff --git a/lightlab/util/config.py b/lightlab/util/config.py
index 03eb65f4..2fe726dc 100644
--- a/lightlab/util/config.py
+++ b/lightlab/util/config.py
@@ -3,6 +3,12 @@
 from configparser import ConfigParser
 from pathlib import Path
 import argparse
+try:
+    from os import getuid
+except ImportError:
+    # User is on windows
+    def getuid():
+        return 1
 
 user_config_path = os.path.expanduser("~") + "/.lightlab" + "/config.conf"
 user_config_path = Path(user_config_path).resolve()
@@ -26,7 +32,7 @@ def get_config():
         read_files.append(system_config_path)
     if os.path.isfile(user_config_path):
         read_files.append(user_config_path)
-    if read_files:
+    if len(read_files) > 0:
         config.read(read_files)
     return config
 
@@ -37,9 +43,9 @@ def parse_param(param):
     split_param = param.split(".")
     section, item = None, None
     if len(split_param) > 0:
-        section = split_param[0] or None
+        section = split_param[0] if split_param[0] else None
     if len(split_param) > 1:
-        item = split_param[1] or None
+        item = split_param[1] if split_param[1] else None
     return (section, item)
 
 
@@ -80,7 +86,7 @@ def print_config_param(param):
     if section is not None and item is not None:
         gotten_param = get_config_param(param)
         print(f"{section}.{item}: {gotten_param}")
-    elif section is not None:
+    elif section is not None and item is None:
         for key, value in config[section].items():
             print(f"{section}.{key}: {value}")
     else:
@@ -108,7 +114,7 @@ def reset_config_param(param):
     if section is not None and item is not None:
         config.remove_option(section, item)
         print(f"{section}.{item} reset.")
-    elif section is not None:
+    elif section is not None and item is None:
         config.remove_section(section)
         print(f"{section}.* reset.")
     config_save(config)
@@ -122,24 +128,18 @@ def config_save(config, omit_default=True):
         unset_items = []
         for section in config.sections():
             if section in default_config.keys():
-                unset_items.extend(
-                    (section, option)
-                    for option in config[section].keys()
-                    if option in default_config[section].keys()
-                    and config[section][option]
-                    == default_config[section][option]
-                )
-
+                for option in config[section].keys():
+                    if option in default_config[section].keys():
+                        if config[section][option] == default_config[section][option]:
+                            unset_items.append((section, option))
         for section, item in unset_items:
             config.remove_option(section, item)
 
         # remove all sections that are default
-        unset_sections = [
-            section
-            for section in config.sections()
-            if len(config[section].keys()) == 0
-        ]
-
+        unset_sections = []
+        for section in config.sections():
+            if len(config[section].keys()) == 0:
+                unset_sections.append(section)
         for section in unset_sections:
             config.remove_section(section)
 
@@ -154,7 +154,7 @@ def config_save(config, omit_default=True):
         config.write(user_config_file)
         print(f'----saving {user_config_path}----', file=sys.stderr)
         config.write(sys.stderr)
-        print(f'----{"-" * len(f"saving {user_config_path}")}----', file=sys.stderr)
+        print('----{}----'.format("-" * len(f"saving {user_config_path}")), file=sys.stderr)
     return True
 
 
@@ -174,11 +174,14 @@ def config_save(config, omit_default=True):
 def config_main(args):
     config_args = config_cmd_parser.parse_args(args)
 
+    def is_root():
+        return getuid() == 0
+
     # If --system is set, change system_config_path
     if config_args.system:
         global user_config_path  # pylint: disable=W0603
         user_config_path = system_config_path
-    elif os.getuid() == 0 and not os.environ.get('DOCKER'):
+    elif is_root() and not os.environ.get('DOCKER'):
         raise SystemExit("Do not run as root except with --system flag.")
 
     params = config_args.params
@@ -192,16 +195,17 @@ def config_main(args):
         else:
             print_config_param(None)
     elif config_args.action == 'set':
-        if len(params) != 2:
-            raise SystemExit("Invalid syntax. Use lightlab config set section.item value.")
-        param = params[0]
-        set_value = params[1]
-        set_config_param(param, set_value)
+        if len(params) == 2:
+            param = params[0]
+            set_value = params[1]
+            set_config_param(param, set_value)
+        else:
+            raise SystemExit(f"Invalid syntax. Use lightlab config set section.item value.")
     elif config_args.action == 'reset':
         if len(params) == 1:
             param = params[0]
             reset_config_param(param)
         else:
-            raise SystemExit("Invalid syntax. Use lightlab config reset section[.item]")
+            raise SystemExit(f"Invalid syntax. Use lightlab config reset section[.item]")
     else:
         config_cmd_parser.print_help()
diff --git a/lightlab/util/data/basic.py b/lightlab/util/data/basic.py
index 01be8997..9284a62b 100644
--- a/lightlab/util/data/basic.py
+++ b/lightlab/util/data/basic.py
@@ -16,11 +16,8 @@ def verifyListOfType(arg, checkType):
     if isinstance(arg, (list, tuple)):
         for a in arg:
             if not isinstance(a, checkType):
-                raise Exception(
-                    (f'Incorrect type, expecting {str(checkType)}' + '. Got ')
-                    + str(type(a))
-                )
-
+                raise Exception('Incorrect type, expecting ' + str(checkType) +
+                                '. Got ' + str(type(a)))
     return arg
 
 
@@ -82,7 +79,7 @@ def mangle(name, klass):
     try:
         i = 0
         while klass[i] == '_':
-            i += 1
+            i = i + 1
     except IndexError:
         return name
     klass = klass[i:]
@@ -91,7 +88,7 @@ def mangle(name, klass):
     if tlen > MANGLE_LEN:
         klass = klass[:MANGLE_LEN - tlen]
 
-    return f"_{klass}{name}"
+    return "_%s%s" % (klass, name)
 
 
 # Simple common array operations
diff --git a/lightlab/util/data/one_dim.py b/lightlab/util/data/one_dim.py
index 00f029ce..bf988019 100644
--- a/lightlab/util/data/one_dim.py
+++ b/lightlab/util/data/one_dim.py
@@ -116,9 +116,7 @@ def __init__(self, abscissaPoints, ordinatePoints, unsafe=False):
                 if isinstance(arr, np.ndarray):
                     if arr.ndim > 1:
                         raise ValueError(
-                            f'Must be a one dimensional array. Got shape {str(arr.shape)}'
-                        )
-
+                            'Must be a one dimensional array. Got shape ' + str(arr.shape))
                     if arr.ndim == 0:
                         arr = np.array([arr])
                     checkVals[iv] = arr.copy()
@@ -127,13 +125,8 @@ def __init__(self, abscissaPoints, ordinatePoints, unsafe=False):
                 elif np.isscalar(arr):
                     checkVals[iv] = np.array([arr])
                 else:
-                    raise TypeError(
-                        (
-                            f'Unsupported type: {str(type(arr))}'
-                            + '. Need np.ndarray, scalar, list, or tuple'
-                        )
-                    )
-
+                    raise TypeError('Unsupported type: ' + str(type(arr)) +
+                                    '. Need np.ndarray, scalar, list, or tuple')
             self.absc, self.ordi = tuple(checkVals)
             if self.absc.shape != self.ordi.shape:
                 raise ValueError('Shapes do not match. Got ' +
@@ -158,7 +151,7 @@ def __len__(self):
         return len(self.absc)
 
     def __iter__(self):
-        raise TypeError(f"{self.__class__.__qualname__} is not iterable")
+        raise TypeError("{} is not iterable".format(self.__class__.__qualname__))
 
     def __getitem__(self, sl):
         ''' Slice this function.
@@ -410,7 +403,10 @@ def uniformlySample(self):
                 MeasuredFunction: new object
         '''
         dxes = np.diff(self.absc)
-        return self if all(dxes == dxes[0]) else self.resample(len(self))
+        if all(dxes == dxes[0]):
+            return self
+        else:
+            return self.resample(len(self))
 
     def addPoint(self, xyPoint):
         ''' Adds the (x, y) point to the stored absc and ordi
@@ -480,22 +476,13 @@ def movingAverage(self, windowWidth=None, mode='valid'):
         '''
         if windowWidth is None:
             windowWidth = (max(self.absc) - min(self.absc)) / 10
-        dx = abs(np.diff(self.absc[:2])[0])
+        dx = abs(np.diff(self.absc[0:2])[0])
         windPts = np.int(windowWidth / dx)
         if windPts % 2 == 0:  # Make sure windPts is odd so that basis doesn't shift
             windPts += 1
         if windPts >= np.size(self.ordi):
-            raise Exception(
-                (
-                    (
-                        f'windowWidth is {str(windPts)}'
-                        + ' wide, which is bigger than the data itself ('
-                    )
-                    + str(np.size(self.ordi))
-                    + ')'
-                )
-            )
-
+            raise Exception('windowWidth is ' + str(windPts) +
+                            ' wide, which is bigger than the data itself (' + str(np.size(self.ordi)) + ')')
 
         filt = np.ones(windPts) / windPts
         invalidIndeces = int((windPts - 1) / 2)
@@ -644,21 +631,31 @@ def invert(self, yVals, directionToDescend=None):
         maxInd = np.argmax(self.ordi)
         minInd = np.argmin(self.ordi)
         if directionToDescend is None:
-            directionToDescend = 'left' if minInd < maxInd else 'right'
-        yValArr = np.array([yVals]) if np.isscalar(yVals) else yVals
+            if minInd < maxInd:
+                directionToDescend = 'left'
+            else:
+                directionToDescend = 'right'
+        if np.isscalar(yVals):
+            yValArr = np.array([yVals])
+        else:
+            yValArr = yVals
         xValArr = np.zeros(yValArr.shape)
         for iVal, y in enumerate(yValArr):
             xValArr[iVal] = interpInverse(*self.getData(),
                                           startIndex=maxInd,
                                           direction=directionToDescend,
                                           threshVal=y)
-        return xValArr[0] if np.isscalar(yVals) else xValArr
+        if np.isscalar(yVals):
+            return xValArr[0]
+        else:
+            return xValArr
 
     def centerOfMass(self):
         ''' Returns abscissa point where mass is centered '''
         deb = self.debias().clip(0, None)
         weighted = np.multiply(*deb.getData())
-        return np.sum(weighted) / np.sum(deb.ordi)
+        com = np.sum(weighted) / np.sum(deb.ordi)
+        return com
 
     def moment(self, order=2, relativeGauss=False):
         ''' The order'th moment of the function
@@ -781,7 +778,8 @@ def _minAbsc(fa, fb):
         dxb = np.mean(np.abs(np.diff(np.sort(fb.absc))))
         new_dx = min(dxa, dxb)
 
-        return np.arange(min_absc, max_absc + new_dx, new_dx)
+        newAbsc = np.arange(min_absc, max_absc + new_dx, new_dx)
+        return newAbsc
 
     @staticmethod
     def _maxAbsc(fa, fb):
@@ -798,7 +796,8 @@ def _maxAbsc(fa, fb):
         dxb = np.mean(np.abs(np.diff(np.sort(fb.absc))))
         new_dx = min(dxa, dxb)
 
-        return np.arange(min_absc, max_absc + new_dx, new_dx)
+        newAbsc = np.arange(min_absc, max_absc + new_dx, new_dx)
+        return newAbsc
 
     def __sub__(self, other):
         ''' Returns the subtraction of the two functions, in the domain of the shortest abscissa object.
@@ -841,7 +840,7 @@ def __pow__(self, power):
         try:
             new_ordi = ordi ** power  # uses numpy's power overload
         except ValueError as err:
-            raise ValueError(f"Invalid power {power} (not a number)") from err
+            raise ValueError("Invalid power {} (not a number)".format(power)) from err
 
         return self.__newOfSameSubclass(absc, new_ordi)
 
@@ -899,13 +898,10 @@ def lin(self):
             Returns:
                 Spectrum: new object
         '''
-        return (
-            type(self)(
-                self.absc.copy(), 10 ** (self.ordi.copy() / 10), inDbm=False
-            )
-            if self.inDbm
-            else type(self)(self.absc.copy(), self.ordi.copy(), inDbm=False)
-        )
+        if not self.inDbm:
+            return type(self)(self.absc.copy(), self.ordi.copy(), inDbm=False)
+        else:
+            return type(self)(self.absc.copy(), 10 ** (self.ordi.copy() / 10), inDbm=False)
 
     def db(self):
         ''' The spectrum in decibel units
@@ -915,8 +911,9 @@ def db(self):
         '''
         if self.inDbm:
             return type(self)(self.absc.copy(), self.ordi.copy(), inDbm=True)
-        clippedOrdi = np.clip(self.ordi, 1e-12, None)
-        return type(self)(self.absc.copy(), 10 * np.log10(clippedOrdi), inDbm=True)
+        else:
+            clippedOrdi = np.clip(self.ordi, 1e-12, None)
+            return type(self)(self.absc.copy(), 10 * np.log10(clippedOrdi), inDbm=True)
 
     def __binMathHelper(self, other):
         ''' Adds a check to make sure lin/db is in the same state '''
@@ -929,7 +926,7 @@ def simplePlot(self, *args, livePlot=False, **kwargs):
         '''
         super().simplePlot(*args, livePlot=livePlot, **kwargs)
         plt.xlabel('Wavelength (nm)')
-        plt.ylabel(f"Transmission ({'dB' if self.inDbm else 'lin'})")
+        plt.ylabel('Transmission ({})'.format('dB' if self.inDbm else 'lin'))
 
     # Peak and trough related
 
@@ -962,12 +959,18 @@ def refineResonanceWavelengths(self, filtShapes, seedRes=None, isPeak=None):
         useFilts = filtShapes.copy()
         if type(self) == Spectrum:
             # For Spectrum objects only
-            spectFun = self.lin() if isPeak else 1 - self.lin()
+            if isPeak:
+                spectFun = self.lin()
+            else:
+                spectFun = 1 - self.lin()
             for i in range(len(filtShapes)):
                 if type(filtShapes[i]).__name__ != 'Spectrum':
                     raise Exception(
                         'If calling object is Spectrum, the filter shapes must also be Spectrum types')
-                useFilts[i] = filtShapes[i].lin() if isPeak else 1 - filtShapes[i].lin()
+                if isPeak:
+                    useFilts[i] = filtShapes[i].lin()
+                else:
+                    useFilts[i] = 1 - filtShapes[i].lin()
         else:
             spectFun = self
 
@@ -1025,7 +1028,7 @@ def simplePlot(self, *args, livePlot=False, **kwargs):
         '''
         super().simplePlot(*args, livePlot=livePlot, **kwargs)
         plt.xlabel('Frequency (GHz)')
-        plt.ylabel(f"Transmission ({'dB' if self.inDbm else 'lin'})")
+        plt.ylabel('Transmission ({})'.format('dB' if self.inDbm else 'lin'))
 
     def nm(self):
         ''' Convert to Spectrum'''
diff --git a/lightlab/util/data/two_dim.py b/lightlab/util/data/two_dim.py
index fd9ba084..86d4f8bc 100644
--- a/lightlab/util/data/two_dim.py
+++ b/lightlab/util/data/two_dim.py
@@ -224,17 +224,8 @@ def _putInTimebase(self, testFun):
                 TypeError: if the type of ``testFun`` is different than the others in this FunctionalBasis
         '''
         if type(testFun).__name__ is not self.memberType.__name__:
-            raise TypeError(
-                (
-                    (
-                        f'This FunctionalBasis expects {str(self.memberType)}'
-                        + ', but was given '
-                    )
-                    + str(type(testFun))
-                    + '.'
-                )
-            )
-
+            raise TypeError('This FunctionalBasis expects ' + str(self.memberType) +
+                            ', but was given ' + str(type(testFun)) + '.')
         # Check time base
         if np.any(testFun.absc != self.absc):
             # logger.warning('Warning: Basis signal time abscissa are different. Interpolating...')
@@ -256,7 +247,8 @@ def multiAxisPlot(self, *args, axList=None, titleRoot=None, **kwargs):
         if axList is None:
             _, axList = plt.subplots(nrows=len(self), figsize=(14, 14))
         if len(axList) != len(self):
-            raise ValueError(f'Wrong number of axes. Got {len(axList)}, need {len(self)}.')
+            raise ValueError('Wrong number of axes. Got {}, need {}.'.format(
+                len(axList), len(self)))
         for i, ax in enumerate(axList):
             plt.sca(ax)
             self[i].simplePlot(*args, **kwargs)
@@ -316,7 +308,10 @@ def moment(self, order=2, allDims=True, relativeGauss=False):
         byDim = np.zeros(len(self))
         for iDim in range(len(self)):
             byDim[iDim] = self[iDim].moment(order, relativeGauss=relativeGauss)
-        return np.mean(byDim) if allDims else byDim
+        if allDims:
+            return np.mean(byDim)
+        else:
+            return byDim
 
     def componentAnalysis(self, *args, pcaIca=True, lNorm=2, expectedComponents=None, **kwargs):
         ''' Gives the waveform representing the principal component of the order
@@ -360,7 +355,10 @@ def correctSigns(self, otherBundle, maintainOrder=True):
         for iDim, oSig in enumerate(otherBundle):
             if maintainOrder:
                 sSig = self[iDim]
-                newWfm = sSig if (sSig * oSig).getMean() > 0 else -1 * sSig
+                if (sSig * oSig).getMean() > 0:
+                    newWfm = sSig
+                else:
+                    newWfm = -1 * sSig
             else:
                 permCorrs = np.zeros(len(self))
                 for jDim, sSig in enumerate(self):
@@ -397,7 +395,8 @@ def innerProds(self, trial):
         '''
         tvec = self._putInTimebase(trial)
         tmat = np.tile(tvec, (self.nDims, 1))
-        return np.sum(np.multiply(tmat, self.ordiMat), axis=1).A1
+        products = np.sum(np.multiply(tmat, self.ordiMat), axis=1).A1
+        return products
 
     def magnitudes(self):
         ''' The inner product of the basis with itself
@@ -504,7 +503,10 @@ def item(self, index, dim=None):
         if np.isscalar(index):
             assert(dim is not None)
             newAbsc = self.absc[dim]
-            newOrdi = self.ordi[index, :] if dim == 0 else self.ordi[:, index]
+            if dim == 0:
+                newOrdi = self.ordi[index, :]
+            else:
+                newOrdi = self.ordi[:, index]
             return MeasuredFunction(newAbsc, newOrdi)
         else:
             assert(len(index) == 2)
@@ -561,7 +563,8 @@ def errorAt(self, testVec=None):
     def invert(self, desiredVec):
         reflectedVec = desiredVec - self.errorAt(desiredVec)
         avgErr = (self.errorAt(desiredVec) + self.errorAt(reflectedVec)) / 2
-        return desiredVec - avgErr
+        commandVec = desiredVec - avgErr
+        return commandVec
 
     def zeroCenteredSquareSize(self):
         ''' Very stupid, just look at corner points
@@ -577,7 +580,8 @@ def cornerInd(grid, minOrMax):
 
         def zcSz(grid, corner):
             cornerVec = grid[(corner + (slice(None), ))]
-            return np.min(np.abs(cornerVec))
+            sqSide = np.min(np.abs(cornerVec))
+            return sqSide
 
         nomiCornerInds = [cornerInd(self.nomiGrid, m) for m in [True, False]]
         nomiSq = np.min([zcSz(self.nomiGrid, nomiCornerInds[i]) for i in range(2)])
diff --git a/lightlab/util/io/jsonpickleable.py b/lightlab/util/io/jsonpickleable.py
index 146f9440..4727692d 100644
--- a/lightlab/util/io/jsonpickleable.py
+++ b/lightlab/util/io/jsonpickleable.py
@@ -77,17 +77,19 @@ def __getstate__(self):
                 if key in allNotPickled:
                     keys_to_delete.add(key)
 
+                # 2. hardware placeholders
                 elif (val.__class__.__name__ == 'VISAObject' or
                       any(base.__name__ == 'VISAObject' for base in val.__class__.mro())):
                     klassname = val.__class__.__name__
                     logger.warning('Not pickling %s = %s.', key, klassname)
-                    state[key] = HardwareReference(f'Reference to a {klassname}')
+                    state[key] = HardwareReference('Reference to a ' + klassname)
 
+                # 3. functions that are not available in modules - saves the code text
                 elif jsonpickle.util.is_function(val) and not jsonpickle.util.is_module_function(val):
-                    state[f'{key}_dilled'] = dill.dumps(val)
+                    state[key + '_dilled'] = dill.dumps(val)
                     keys_to_delete.add(key)
 
-                        # 4. double underscore attributes have already been removed
+                # 4. double underscore attributes have already been removed
         for key in keys_to_delete:
             del state[key]
         return state
@@ -121,17 +123,10 @@ def _fromJSONcheck(cls, json_string):
             err.args = (newm,) + err.args[1:]
             raise
 
-        if (
-            not isinstance(restored_object, cls)
-            and type(restored_object).__name__ != cls.__name__
-        ):
-            raise TypeError(
-                (
-                    'Loaded class is different than intended.\n'
-                    + f'Got {type(restored_object).__name__}, needed {cls.__name__}.'
-                )
-            )
-
+        if not isinstance(restored_object, cls):  # This is likely to happen if lightlab has been reloaded
+            if type(restored_object).__name__ != cls.__name__:  # This is not ok
+                raise TypeError('Loaded class is different than intended.\n' +
+                                'Got {}, needed {}.'.format(type(restored_object).__name__, cls.__name__))
 
         for a in cls.notPickled:
             setattr(restored_object, a, None)
diff --git a/lightlab/util/io/progress.py b/lightlab/util/io/progress.py
index 5ddeeb15..9bd59997 100644
--- a/lightlab/util/io/progress.py
+++ b/lightlab/util/io/progress.py
@@ -21,8 +21,10 @@ def printWait(*args):
         Args:
             \*args (Tuple(str)): Strings that will be written
     '''
-    msg = ''.join(str(a) for a in args)
-    print(f"{msg}... ", end='')
+    msg = ''
+    for a in args:
+        msg += str(a)
+    print(msg + "... ", end='')
     sys.stdout.flush()
 
 
@@ -34,7 +36,9 @@ def printProgress(*args):
     Args:
         *args (str, Tuple(str)): Arguments that will be written
     '''
-    msg = ''.join(str(a) for a in args)
+    msg = ''
+    for a in args:
+        msg += str(a)
     sys.stdout.write('\b' * 1000)
     sys.stdout.flush()
     sys.stdout.write(msg)
@@ -91,7 +95,7 @@ def __init__(self, name, swpSize, runServer=True, stdoutPrint=False, **kwargs):
         def print_string():
             prntStr = self.name + "\n"
             for iterDim, _ in enumerate(self.size):
-                prntStr += f'Dim-{str(iterDim)}...'
+                prntStr += 'Dim-' + str(iterDim) + '...'
             return prntStr
 
         if True:
@@ -126,12 +130,13 @@ def getUrl():
                 port = int(fx.readline())
         except FileNotFoundError:
             port = 'null'
-        return prefix + host + ':' + port
+        return prefix + host + ':' + str(port)
 
     def __tag(self, bodytext, autorefresh=False):
         ''' Do the HTML tags '''
         if not hasattr(self, '__tagHead') or self.__tagHead is None:
-            t = '<!DOCTYPE html>\n' + '<html>\n'
+            t = '<!DOCTYPE html>\n'
+            t += '<html>\n'
             t += '<head>\n'
             t += '<title>'
             t += 'Sweep Progress Monitor'
@@ -139,11 +144,12 @@ def __tag(self, bodytext, autorefresh=False):
             if autorefresh:
                 t += '<meta http-equiv="refresh" content="5" />\n'  # Autorefresh every 5 seconds
             t += '<body>\n'
-            t += f'<h1>{self.name}' + '</h1>\n'
+            t += '<h1>' + self.name + '</h1>\n'
             t += r'<hr \>\\n'
             self.__tagHead = t
         if not hasattr(self, '__tagFoot') or self.__tagFoot is None:
-            t = '</body>\n' + '</html>\n'
+            t = '</body>\n'
+            t += '</html>\n'
             self.__tagFoot = t
         return self.__tagHead + bodytext + self.__tagFoot
 
@@ -155,7 +161,7 @@ def __writeHtmlEnd(self):
         self.__tagHead = None
         self.__tagFoot = None
         body = '<h2>Sweep completed!</h2>\n'
-        body += ptag(f'At {ProgressWriter.tims(time.time())}')
+        body += ptag('At ' + ProgressWriter.tims(time.time()))
         htmlText = self.__tag(body, autorefresh=False)
         with self.filePath.open('w') as fx:  # pylint: disable=no-member
             fx.write(htmlText)
@@ -175,7 +181,7 @@ def __writeHtml(self):
         body = ''
         for i, p in enumerate(self.currentPnt):
             dimStr = i * 'sub-' + 'dimension[' + str(i) + '] : '
-            dimStr += f'{str(p + 1)} of {str(self.size[i])}'
+            dimStr += str(p + 1) + ' of ' + str(self.size[i])
             body += ptag(dimStr)
         body += r'<hr \>\\n'
 
@@ -189,7 +195,7 @@ def __writeHtml(self):
                      ProgressWriter.tims(self.startTime))
         body += ptag('(Latest Update)        ' +
                      ProgressWriter.tims(currentTime))
-        body += ptag(f'(Expected Completion)  {ProgressWriter.tims(endTime)}')
+        body += ptag('(Expected Completion)  ' + ProgressWriter.tims(endTime))
 
         # Say where the files are hosted
         body += ptag('This monitor service is hosted in the directory:')
@@ -212,14 +218,17 @@ def update(self, steps=1):
                 self.__writeHtml()
             if self.printing:
                 self.__writeStdio()
-            self.tempfile.write(self._get_std_string() + "\n")
+            if True:
+                self.tempfile.write(self._get_std_string() + "\n")
+                self.tempfile.flush()
         else:
             if self.serving:
                 self.__writeHtmlEnd()
             if self.printing:
                 self.__writeStdioEnd()
-            self.tempfile.write("End." + "\n")
-        self.tempfile.flush()
+            if True:
+                self.tempfile.write("End." + "\n")
+                self.tempfile.flush()
 
     def __updateOneInternal(self):
         for i in range(len(self.size)):
@@ -238,4 +247,4 @@ def tims(cls, epochTime):
 
 
 def ptag(s):
-    return f'<p>{s}' + '</p>\n'
+    return '<p>' + s + '</p>\n'
diff --git a/lightlab/util/io/saveload.py b/lightlab/util/io/saveload.py
index e0e091c7..a73b9215 100644
--- a/lightlab/util/io/saveload.py
+++ b/lightlab/util/io/saveload.py
@@ -19,7 +19,7 @@ def _makeFileExist(filename):
     rp = _getFileDir(filename)
     os.makedirs(_getFileDir(), mode=0o775, exist_ok=True)
     rp.touch()
-    print(f"Saving to file: {rp}")
+    print("Saving to file: {}".format(rp))
     return rp
 
 
@@ -54,7 +54,7 @@ def pprintFileDir(*, generate=False):
     childNames = list(map(lambda x: x.name, childrenFiles))
     sortedChildren = [x for _, x in sorted(zip(childNames, childrenFiles))]
     for child in sortedChildren:
-        justified = f'{child.name.rjust(maxStrLen)}   '
+        justified = child.name.rjust(maxStrLen) + '   '
         if child.name.endswith('.pkl'):
             print(justified, f'io.loadPickle(\'{child.stem}\')')
         elif child.name.endswith('.gz'):
@@ -71,7 +71,10 @@ def _endingWith(filerootname, suffix):
     froot = str(filerootname)
     if suffix[0] != '.':
         suffix = f'.{suffix}'
-    return froot if froot.endswith(suffix) else froot + suffix
+    if froot.endswith(suffix):
+        return froot
+    else:
+        return froot + suffix
 
 
 def savePickle(filename, dataTuple):
diff --git a/lightlab/util/measprocessing.py b/lightlab/util/measprocessing.py
index 212b4df6..2b901fa1 100644
--- a/lightlab/util/measprocessing.py
+++ b/lightlab/util/measprocessing.py
@@ -46,7 +46,8 @@ def fgSpect(self, avgCnt=1, raw=None, bgType=None):
         bg = self.getBgSpect(bgType)
         if bgType is None and type(bg) is float and bg == 0:
             self.setBgSmoothed(raw)
-        return raw - bg
+        unbiased = raw - bg
+        return unbiased
 
     def resonances(self, spect=None, avgCnt=1):
         ''' Returns the current wavelengths of detected peaks in order sorted by wavelength.
@@ -158,7 +159,5 @@ def getBgSpect(self, bgType=None):
             except KeyError:
                 raise KeyError('Background of type \'' + bgType + '\' has not been taken yet.')
         else:
-            raise ValueError(
-                (f'Invalid background token: {bgType}' + '. Need ')
-                + ', '.join(preferredOrder)
-            )
+            raise ValueError('Invalid background token: ' + bgType +
+                             '. Need ' + str(', '.join(preferredOrder)))
diff --git a/lightlab/util/plot.py b/lightlab/util/plot.py
index 5273f676..3a8dd83c 100644
--- a/lightlab/util/plot.py
+++ b/lightlab/util/plot.py
@@ -8,7 +8,7 @@ class DynamicLine(object):
     ''' A line that can refresh when called
     '''
 
-    def __init__(self, formatStr='b-', existing=None, geometry=[(0, 0), (4, 4)]):    # pylint: disable=dangerous-default-value
+    def __init__(self, formatStr='b-', existing=None, geometry=[(0, 0), (4, 4)]):  # pylint: disable=dangerous-default-value
         '''
             Args:
                 formatStr (str): plotting line format
@@ -28,9 +28,7 @@ def __init__(self, formatStr='b-', existing=None, geometry=[(0, 0), (4, 4)]):
             # Geometry is ignored here
         self.lines, = self.ax.plot([], [], formatStr)
         plt.get_current_fig_manager().window.wm_geometry(
-            f'+{str(geometry[0][0])}+{str(geometry[0][1])}'
-        )
-
+            '+' + str(geometry[0][0]) + '+' + str(geometry[0][1]))
         # Autoscale on unknown axis and known lims on the other
         self.ax.set_autoscaley_on(True)
         # Other stuff
diff --git a/lightlab/util/search.py b/lightlab/util/search.py
index 2b1c4f59..2bde69b9 100644
--- a/lightlab/util/search.py
+++ b/lightlab/util/search.py
@@ -99,11 +99,12 @@ def shrinkAround(arr, bestInd, shrinkage=.6):
 def doesMFbracket(targetY, twoPointMF):
     yRange = twoPointMF.getRange()
     if targetY < yRange[0]:
-        return 'low'
+        outOfRangeDirection = 'low'
     elif targetY > yRange[1]:
-        return 'high'
+        outOfRangeDirection = 'high'
     else:
-        return 'in-range'
+        outOfRangeDirection = 'in-range'
+    return outOfRangeDirection
 
 
 def bracketSearch(evalPointFun, targetY, startBounds, xTol, hardConstrain=False, livePlot=False):
diff --git a/lightlab/util/sweep.py b/lightlab/util/sweep.py
index 202ba9f3..686cc67d 100644
--- a/lightlab/util/sweep.py
+++ b/lightlab/util/sweep.py
@@ -36,8 +36,8 @@ class Sweeper(object):
     def __init__(self):
         self.data = None
         self.savefile = None
-        self.plotOptions = {}
-        self.monitorOptions = {}
+        self.plotOptions = dict()
+        self.monitorOptions = dict()
 
     def gather(self):
         print('gather method must be overloaded in subclass')
@@ -184,7 +184,7 @@ def repeater(cls, nTrials):
         new.addActuation('trial', lambda a: None, np.arange(nTrials))
         return new
 
-    def gather(self, soakTime=None, autoSave=False, returnToStart=False):    # pylint: disable=arguments-differ
+    def gather(self, soakTime=None, autoSave=False, returnToStart=False):  # pylint: disable=arguments-differ
         ''' Perform the sweep
 
             Args:
@@ -235,7 +235,7 @@ def gather(self, soakTime=None, autoSave=False, returnToStart=False):    # pylin
                     if iDim == self.actuDims - 1 or index[iDim + 1] == 0 or actuObj.doOnEveryPoint:
                         y = actuObj.function(x)  # The actual function call occurs here
                         if y is not None:
-                            pointData[f'{actuKey}-return'] = y
+                            pointData[actuKey + '-return'] = y
 
                 # Do the measurement, store return values
                 for measKey, measFun in self.measure.items():
@@ -274,7 +274,7 @@ def gather(self, soakTime=None, autoSave=False, returnToStart=False):    # pylin
                         display.clear_output(wait=True)
                 # Progress report
                 prog.update()
-                # End of the main loop
+            # End of the main loop
 
         except Exception as err:
             logger.error('Error while sweeping. Keeping data. %s', err)
@@ -403,18 +403,9 @@ def addStaticData(self, name, contents):
         if np.isscalar(contents):
             contents *= np.ones(self.swpShape)
         if np.any(contents.shape != self.swpShape):
-            raise ValueError(
-                (
-                    (
-                        (f'Static data {name} is wrong shape for sweep.' + 'Need ')
-                        + str(self.swpShape)
-                        + '. Got '
-                    )
-                    + str(contents.shape)
-                    + 'The order that actuations and static data are added matter.'
-                )
-            )
-
+            raise ValueError('Static data ' + name + ' is wrong shape for sweep.' +
+                             'Need ' + str(self.swpShape) + '. Got ' + str(contents.shape) +
+                             'The order that actuations and static data are added matter.')
         self.static[name] = contents
 
     def subsume(self, other, useMinorOptions=False):
diff --git a/notebooks/Examples/ParameterExtraction/myProcedures.py b/notebooks/Examples/ParameterExtraction/myProcedures.py
index cc286695..957471f4 100644
--- a/notebooks/Examples/ParameterExtraction/myProcedures.py
+++ b/notebooks/Examples/ParameterExtraction/myProcedures.py
@@ -32,7 +32,8 @@ def dither(centerVolt):
         for iDv, dv in enumerate(dvArr):
             source_meter_instr.setVoltage(centerVolt + dv)
             diArr[iDv] = source_meter_instr.measCurrent()
-        return diArr[0] - 2 * diArr[1] + diArr[2]
+        d2idv2 = diArr[0] - 2 * diArr[1] + diArr[2]
+        return d2idv2
 
     foundThresh, _ = peakSearch(dither, [-1, 3], livePlot=True, nSwarm=5)
     return foundThresh
diff --git a/pylintrc b/pylintrc
index 1b5e5ae6..bfd35ff8 100644
--- a/pylintrc
+++ b/pylintrc
@@ -769,4 +769,4 @@ min-public-methods=2
 
 # Exceptions that will emit a warning when being caught. Defaults to
 # "Exception"
-overgeneral-exceptions=Exception
+overgeneral-exceptions=builtins.Exception
diff --git a/pylintrc-errors b/pylintrc-errors
index fdc588e3..e26127f8 100644
--- a/pylintrc-errors
+++ b/pylintrc-errors
@@ -769,4 +769,4 @@ min-public-methods=2
 
 # Exceptions that will emit a warning when being caught. Defaults to
 # "Exception"
-overgeneral-exceptions=Exception
+overgeneral-exceptions=builtins.Exception
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..a18cb1af
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,48 @@
+[build-system]
+requires = ["setuptools", "setuptools-scm"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "lightlab"
+authors = [
+    {name = "Alex Tait", email = "atait@ieee.org"},
+    {name = "Thomas Ferreira de Lima", email = "github@tlima.me"},
+]
+maintainers = [
+    {name = "Thomas Ferreira de Lima", email = "github@tlima.me"}
+]
+description = "Lightwave Lab instrument automation tools"
+readme = "README.rst"
+requires-python = ">=3.7"
+keywords = ["gpib", "visa", "instrument control"]
+license = {file = "LICENSE"}
+classifiers = [
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+    "Topic :: Scientific/Engineering",
+    "Topic :: System :: Hardware :: Hardware Drivers",
+    "Framework :: Jupyter",
+]
+dependencies = [
+    'dpath',
+    'jsonpickle>=1.4.1',
+    'matplotlib',
+    'IPython',
+    'PyVISA',
+    'scipy',
+    'scikit-learn',
+    'dill',
+]
+dynamic = ["version"]
+
+[project.urls]
+repository = "https://github.com/lightwave-lab/lightlab"
+
+[project.scripts]
+lightlab = "lightlab.command_line:main"
+
+[tool.setuptools.dynamic]
+version = {attr = "version.version"}
+
+[tool.setuptools]
+packages = ["lightlab"]
\ No newline at end of file
diff --git a/readthedocs.yml b/readthedocs.yml
index c8b8b04c..465b81ba 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -2,5 +2,5 @@ build:
     image: latest
 
 python:
-    version: 3.6
+    version: 3.10
     pip_install: true
diff --git a/setup.cfg b/setup.cfg
index d4ebd5bc..d9e895c9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,2 @@
 [tool:pytest]
-flake8-ignore = E731 E402 E501 W504
-
-[metadata]
-description-file = README.rst
-license-file = LICENSE
+flake8-ignore = E731 E402 E501 W504
\ No newline at end of file
diff --git a/setup.py b/setup.py
index ec0e0960..3be5be94 100644
--- a/setup.py
+++ b/setup.py
@@ -1,71 +1,11 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-import os
-import sys
-from setuptools import setup, find_packages
-
-
-LABSTATE_FILENAME = "labstate.json"
-JUPYTER_GROUP = "jupyter"
-
-# assert sys.version_info >= (3, 6), "Use python >= 3.6 - We are living in the __future__!"
-
-
-def touch(fname, times=None):
-    with open(fname, 'a'):
-        os.utime(fname, times)
-
+from setuptools import setup
 
 def main():
-    with open('README.rst') as f:
-        readme = f.read()
-
-    with open('LICENSE') as f:
-        license_text = f.read()
-
-    with open("version.py") as f:
-        code = compile(f.read(), "version.py", 'exec')
-        version_dict = {}
-        exec(code, {}, version_dict)  # pylint: disable=exec-used
-        release = version_dict['release']
-
-    metadata = dict(
-        name='lightlab',
-        version=release,
-        description='Lightwave Lab instrument automation tools',
-        long_description=readme,
-        license=license_text.split('\n')[0],
-        python_requires='>=3.6',
-        packages=find_packages(exclude=('tests', 'docs', 'data')),
-        url="https://github.com/lightwave-lab/lightlab",
-        author="Alex Tait <atait@ieee.org>, Thomas Ferreira de Lima <tlima@princeton.edu>",
-        author_email="tlima@princeton.edu",
-        classifiers=(
-            "Programming Language :: Python :: 3.6",
-            "Programming Language :: Python :: 3.7",
-            "License :: OSI Approved :: MIT License",
-            "Operating System :: OS Independent",
-            "Topic :: Scientific/Engineering",
-            "Topic :: System :: Hardware :: Hardware Drivers",
-            "Framework :: Jupyter",
-        ),
-        install_requires=[
-            'dpath',
-            'jsonpickle>=1.4.1',
-            'matplotlib',
-            'IPython',
-            'PyVISA',
-            'scipy',
-            'sklearn',
-            'dill',
-        ],
-        entry_points={
-            'console_scripts': ['lightlab=lightlab.command_line:main'],
-        }
-    )
-
-    setup(**metadata)
+    # All metadata was moved to pyproject.toml
+    setup()
 
 
 if __name__ == '__main__':
diff --git a/version.py b/version.py
index e232bb6f..dfd2db4c 100644
--- a/version.py
+++ b/version.py
@@ -1,9 +1,9 @@
 # version.py
 
 # The short X.Y version.
-version = '1.1.0'
+version = '1.1.1'
 # The full version, including alpha/beta/rc tags.
-release = f"{version}"
+release = version + ""
 
 if __name__ == '__main__':
-    print(f'lightlab v{version}')
+    print('lightlab v' + version)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..faf4ef78
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,37 @@
+name: Python package
+
+on: [push, pull_request]
+
+jobs:
+  test_code:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      max-parallel: 12
+      matrix:
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        os: [ubuntu-latest, windows-latest, macos-latest]
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install build
+        pip install -r test-requirements.txt
+    - name: Install package and dependencies
+      run: |
+        pip install .
+    - name: Build package from source
+      run: |
+        python -m build
+    - name: Run tests
+      run: |
+        py.test --capture=sys --cov=lightlab --cov-config .coveragerc
+    - name: Run linting
+      run: |
+        py.test --pylint --flake8 --pylint-rcfile=pylintrc lightlab
+      continue-on-error: true
\ No newline at end of file
diff --git a/lightlab/equipment/lab_instruments/Advantest_Q8221_PM.py b/lightlab/equipment/lab_instruments/Advantest_Q8221_PM.py
index 4e150067..d329f8d9 100644
--- a/lightlab/equipment/lab_instruments/Advantest_Q8221_PM.py
+++ b/lightlab/equipment/lab_instruments/Advantest_Q8221_PM.py
@@ -44,6 +44,6 @@ def powerDbm(self, channel=1):
                 (double): Power in dB or dBm
         '''
         self.validateChannel(channel)
-        self.write(f'CH{str(channel)}')
-        powStr = self.query(f'CH{str(channel)}')
+        self.write('CH' + str(channel))
+        powStr = self.query('CH' + str(channel))
         return float(powStr[3:])
diff --git a/test-requirements.txt b/test-requirements.txt
index f31d286c..311c5f6c 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,63 +1,6 @@
-appnope==0.1.0
-astroid==2.2.5
-atomicwrites==1.3.0
-attrs==19.1.0
-backcall==0.1.0
-coverage==4.5.3
-cycler==0.10.0
-decorator==4.4.0
-dill==0.2.9
-dpath==2.0.1
-flake8==3.6.0
-importlib-metadata==0.17
-ipykernel==5.1.1
-ipython==7.16.3
-ipython-genutils==0.2.0
-isort==4.3.20
-jedi==0.13.3
-joblib==0.13.2
-jsonpickle==1.2
-jsonschema==3.0.1
-jupyter-client==5.2.4
-jupyter-core==4.4.0
-kiwisolver==1.1.0
-lazy-object-proxy==1.4.1
-matplotlib==3.1.0
-mccabe==0.6.1
-mock==2.0.0
-more-itertools==7.0.0
-nbformat==4.4.0
-nbval==0.9.1
-numpy==1.22.0
-packaging==19.0
-parso==0.4.0
-pbr==5.2.1
-pexpect==4.7.0
-pickleshare==0.7.5
-pluggy==0.12.0
-prompt-toolkit==2.0.9
-ptyprocess==0.6.0
-py==1.10.0
-pycodestyle==2.4.0
-pyflakes==2.0.0
-Pygments==2.7.4
-pylint==2.3.1
-pyparsing==2.4.0
-pyrsistent==0.15.2
-pytest==4.6.2
-pytest-cov==2.5.1
-pytest-flake8==1.0.2
-pytest-pylint==0.12.1
-python-dateutil==2.8.0
-PyVISA==1.9.1
-pyzmq==18.0.1
-scikit-learn==0.21.2
-scipy==1.3.0
-six==1.12.0
-sklearn==0.0
-tornado==6.0.2
-traitlets==4.3.2
-typed-ast==1.4.0
-wcwidth==0.1.7
-wrapt==1.11.1
-zipp==0.5.1
+pytest
+pytest-flake8
+pytest-cov
+pytest-pylint
+mock
+freezegun
\ No newline at end of file
diff --git a/tests/test_JSONpickleable.py b/tests/test_JSONpickleable.py
index 42291698..37e22816 100644
--- a/tests/test_JSONpickleable.py
+++ b/tests/test_JSONpickleable.py
@@ -137,7 +137,9 @@ def expJSONfile():
 
 def test_JSONpickleableWithFile(expJSONfile):
     global INSTANTIATION_COUNTER
+    INSTANTIATION_COUNTER = 0
     loadedExp = SomeVirtualizedExperiment.load(expJSONfile)
+    assert INSTANTIATION_COUNTER == 0  # __init__ is not called again
     validate(loadedExp)
 
 
diff --git a/tests/test_config.py b/tests/test_config.py
index 2d13dae1..7cba1966 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -6,7 +6,7 @@
 from lightlab import config
 from configparser import ConfigParser
 
-filename = f'testconfig_{int(time.time())}.conf'
+filename = 'testconfig_{}.conf'.format(int(time.time()))
 user_config_path = Path(filename).resolve()
 
 default_config = config.default_config
diff --git a/tests/test_imports.py b/tests/test_imports.py
index b96789df..899e2e8d 100644
--- a/tests/test_imports.py
+++ b/tests/test_imports.py
@@ -1,19 +1,15 @@
 '''Import all instrument module by name'''
-
 import pkgutil
 import lightlab
 import pytest
 import importlib
 
 package = lightlab
-modules = [
-    modname
-    for _, modname, _ in pkgutil.walk_packages(
-        path=package.__path__,
-        prefix=f'{package.__name__}.',
-        onerror=lambda x: None,
-    )
-]
+modules = list()
+for _, modname, _ in pkgutil.walk_packages(path=package.__path__,
+                                           prefix=package.__name__ + '.',
+                                           onerror=lambda x: None):
+    modules.append(modname)
 
 
 @pytest.mark.parametrize("modname", modules)
diff --git a/tests/test_labstate.py b/tests/test_labstate.py
index 7351a345..63f2d496 100644
--- a/tests/test_labstate.py
+++ b/tests/test_labstate.py
@@ -1,6 +1,5 @@
 '''Tests whether the functionality of the laboratory module is working properly.'''
 
-
 import pytest
 import lightlab.laboratory.state as labstate
 from lightlab.laboratory.instruments import LocalHost, Host, Bench, Instrument, Keithley
@@ -12,9 +11,10 @@
 import time
 import os
 import logging
+from freezegun import freeze_time
 
 logging.disable(logging.CRITICAL)
-filename = f'test_{int(time.time())}.json'
+filename = 'test_{}.json'.format(int(time.time()))
 labstate._filename = filename
 
 # Shared objects
@@ -179,7 +179,9 @@ def test_bench_iteration(lab):
         benches_items.append(bench)
         benches_names.append(bench_name)
 
-    benches_values = list(lab.benches.values())
+    benches_values = []
+    for bench in lab.benches.values():
+        benches_values.append(bench)
     assert benches_items == benches_values
 
     assert benches_names == [bench.name for bench in benches_values]
@@ -207,7 +209,7 @@ def test_savestate(lab):
     lab.updateHost(h1)
     lab.saveState(filename, save_backup=False)
 
-
+@freeze_time("2023-02-21")
 def test_reloadlabstate(lab):
     ''' Saves and reloads LabState and asserts equality '''
     lab2 = labstate.LabState.loadState(filename=filename)
diff --git a/tests/test_virtualization.py b/tests/test_virtualization.py
index 66abb9f2..5c958226 100644
--- a/tests/test_virtualization.py
+++ b/tests/test_virtualization.py
@@ -59,9 +59,11 @@ def checkHits(N=1):
         and that the hammer was put down afterwards
     '''
     global NAIL, IN_HAND
-    yield  # user code runs
     NAIL = 0
+    IN_HAND = False
+    yield  # user code runs
     assert NAIL == N
+    assert not IN_HAND
 
 
 ''' INSTRUMENT INTERFACES
@@ -287,4 +289,3 @@ def test_dualWeilding():
 
     with pytest.raises(VirtualizationError):
         complicatedProcedure_resultingIn3hits(dual1, dual2)
-
diff --git a/tests/test_visa_drivers.py b/tests/test_visa_drivers.py
index ad531aff..9ae6fde2 100644
--- a/tests/test_visa_drivers.py
+++ b/tests/test_visa_drivers.py
@@ -1,17 +1,16 @@
 ''' Tests whether all the visa drivers included in lightlab are properly
 coded. All tests should be safe to run locally.'''
 
-
 import pytest
 from mock import patch
 from lightlab.equipment import lab_instruments
-from lightlab.equipment.lab_instruments import VISAInstrumentDriver
+from lightlab.equipment.lab_instruments import VISAInstrumentDriver, experimental_instruments
 import inspect
 
 classes = [
     obj
     for name, obj in inspect.getmembers(lab_instruments)
-    if inspect.isclass(obj) and issubclass(obj, VISAInstrumentDriver)
+    if inspect.isclass(obj) and issubclass(obj, VISAInstrumentDriver) and name not in experimental_instruments
 ]
 
 class OpenError(RuntimeError):