Python for Engineers

The Python an engineer needs: data types, functions, NumPy and Matplotlib, instruments over serial, units, speed and virtual environments.

The parts of Python you use every day

Python reads like pseudo-code: indentation marks blocks, there are no braces or type declarations, and a handful of built-in types do most of the work.

TypeExampleUse
int, float42, 3.14, 1e-3Numbers: Python integers never overflow; floats are 64-bit doubles
str"motor 3", f"T = {t:.1f} C"Text; f-strings format values
bool, NoneTrue, NoneFlags and “no value”
list[1.2, 3.4, 5.6]An ordered, changeable sequence
tuple(12.0, 30.5)A fixed group of values (a coordinate)
dict{"kp": 2.0, "ki": 0.5}Values looked up by key: settings, records
set{1, 2, 3}Unique items; fast “is it in there?”
bytesb"\x01\x03\x00"Raw data from a port or file (see Bits, Bytes and Data Formats)
temps = [20.1, 20.3, 19.9, 20.2]            # a list
limits = {"min": 18.0, "max": 25.0}          # a dictionary

for i, t in enumerate(temps):                # loop with an index
    if t > limits["max"]:
        print(f"sample {i}: {t:.1f} C is too hot")

mean = sum(temps) / len(temps)               # built-in sum and len
hot = [t for t in temps if t > 20.0]         # list comprehension: a filtered copy
  • Indexing starts at 0; negative indexes count from the end (temps[-1] is the last); slices temps[1:3] stop before the second number.
  • Division: 7 / 2 is 3.5, 7 // 2 is 3, 7 % 2 is 1 (unlike C, where 7 / 2 is 3).
  • Everything is an object, and assignment copies a reference, not the data (see the pitfalls below).
  • Blocks by indentation: use four spaces, never tabs mixed with spaces.

Functions, modules and errors

def rms(values):
    """Root-mean-square of a sequence of numbers."""
    if len(values) == 0:
        raise ValueError("rms of an empty list")
    return (sum(v * v for v in values) / len(values)) ** 0.5

def read_sensor(port, timeout_s=0.5):          # a default argument
    try:
        return port.read_value(timeout_s)
    except TimeoutError:
        return None                            # the caller must handle None
    finally:
        port.flush()                           # always runs
  • Docstrings and names with units (speed_mm_s, temp_c) make a function readable months later.
  • Modules: a file filters.py becomes a module: import filters or from filters import ema. The standard library already has math, statistics, csv, json, datetime, pathlib, struct, argparse and logging.
  • Exceptions: catch the specific error you expect (except ValueError) rather than everything; a bare except hides bugs.
  • Command-line tools: argparse turns a script into a tool with options and a help message, and if __name__ == "__main__": lets the file be both a module and a script.

NumPy: arrays and vectorizing

NumPy provides a fixed-type, fixed-size N-dimensional array, with math on whole arrays at once. The loop happens in compiled code, so it is short to write and fast to run. Almost all scientific Python (SciPy, pandas, Matplotlib, scikit-learn, TensorFlow) is built on it.

import numpy as np

t = np.linspace(0, 1, 1000, endpoint=False)    # 1000 time points, 1 kHz
x = 5.0 * np.sin(2 * np.pi * 50 * t)           # a 50 Hz sine of amplitude 5

rms  = np.sqrt(np.mean(x**2))                  # whole-array maths, no loop
peak = np.max(np.abs(x))

grid = np.arange(12).reshape(3, 4)             # a 3 x 4 matrix
col_means = grid.mean(axis=0)                  # one mean per column
scaled = grid * np.array([1, 10, 100, 1000])   # broadcasting: the row is applied to every row

Worked example: RMS and broadcasting

  • RMS of a sine. A sine of amplitude 5 has an RMS of A ÷ √2 = 5 ÷ 1.4142 = 3.536. Averaging x² over exactly whole cycles gives this; the code above on 1 000 samples (50 whole cycles) returns 3.536. Sampling only part of a cycle gives a different answer, which is why you measure a whole number of cycles.
  • With only 8 samples per cycle the same calculation gives 3.5355 for the samples 0.00, 3.54, 5.00, 3.54, 0.00, -3.54, -5.00, -3.54: still exact here because the samples cover a full cycle evenly.
  • Shapes. A 3 × 4 array times a length-4 vector works because NumPy broadcasts the vector across the 3 rows; a length-3 vector would raise an error (the shapes (3, 4) and (3,) do not match). Check .shape when you get one.
  • Memory. A million float64 values occupy 8 MB; as float32 only 4 MB.
  • Prefer array operations to loops: np.sum(a * b) instead of a Python loop over the indexes (typically 20 to 100 times faster).
  • Boolean masks select data: x[x > 3.0] returns the elements above 3.0, np.where(cond, a, b) chooses between two arrays.
  • Views and copies: a slice of an array is a view of the same data; changing it changes the original. Use .copy() when you need independence.
  • dtype matters: integer arrays overflow silently (np.uint8(250) + 10 wraps to 4), and mixing integers and floats upcasts. See Numbers and Floating Point.
  • Missing data: np.nan propagates through math; use np.nanmean and friends, or mask it first.

Plotting with Matplotlib

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(7, 3.5))
ax.plot(t * 1000, x, label="sensor")                 # time in ms
ax.axhline(rms, color="orange", linestyle="--", label=f"RMS = {rms:.2f}")
ax.set_xlabel("time (ms)")                            # always label the axes, with units
ax.set_ylabel("voltage (V)")
ax.grid(True)
ax.legend()
fig.savefig("signal.png", dpi=150, bbox_inches="tight")
  • Label axes with units, use a legend only when there is more than one line, and keep the data range honest (a truncated axis exaggerates changes).
  • Plot the data, not only a summary: a scatter of raw points shows outliers and steps that a mean hides.
  • Save figures as files from the script (PNG for the web, SVG or PDF for documents) so that the plot can be regenerated.
  • Other libraries: Plotly and Bokeh for interactive plots, Seaborn for statistical plots, PyQtGraph for real-time displays.

Statistics and curve fitting with SciPy

Worked example: how sure is a measurement?

Eight readings of a temperature: 20.1, 20.3, 19.9, 20.2, 20.0, 20.4, 19.8, 20.3 °C. The mean is 20.13 °C. The sample standard deviation (dividing by n − 1) is s = 0.212 °C, and the standard error of the mean is s ÷ √n = 0.212 ÷ √8 = 0.075 °C. Quote the result as 20.13 ± 0.08 °C (one standard error). In NumPy: np.mean(x), np.std(x, ddof=1). The ddof=1 matters: the default divides by n.

A calibration line. A sensor reads 0.02, 2.55, 5.01 V at 0, 50, 100 units. A least-squares line gives slope 0.0499 V per unit and intercept 0.0317 V, so a reading of 3.80 V corresponds to (3.80 − 0.0317) ÷ 0.0499 = 75.52 units. In code: m, c = np.polyfit(x, v, 1).

from scipy import optimize, integrate, signal, interpolate

popt, pcov = optimize.curve_fit(lambda t, a, tau: a * np.exp(-t / tau), t_data, y_data, p0=[1.0, 1.0])
area  = integrate.trapezoid(power_w, dx=0.1)           # energy from a power log sampled every 0.1 s
b, a  = signal.butter(2, 10, fs=1000)                   # 2nd-order 10 Hz low-pass filter at 1 kHz
smooth = signal.filtfilt(b, a, noisy)                   # zero-phase (offline) filtering
f = interpolate.interp1d(x_table, y_table, kind="linear")

See Numerical Methods for what these routines do, and Filters, PID and Signal Code for filters and spectra.

Talking to instruments and serial ports

pyserial opens a serial (COM) port; PyVISA talks to bench instruments over USB, GPIB and Ethernet with SCPI commands; pymodbus speaks Modbus; sockets and paho-mqtt cover networks. All follow the same pattern: open, send, read with a timeout, parse, close.

import serial, time

with serial.Serial("COM3", 9600, timeout=0.5) as port:        # closes the port automatically
    port.write(b"READ?\n")
    line = port.readline().decode("ascii", errors="replace").strip()   # waits up to the timeout
    if not line:
        raise TimeoutError("no reply from the instrument")
    value = float(line)                                        # may raise ValueError: handle it

Worked example: choosing the timeout

At 9 600 baud a byte takes 10 ÷ 9 600 = 1.04 ms, so a 12-byte reply takes 12.50 ms on the wire. The instrument also needs time to measure; a timeout of 0.5 s leaves plenty of room. Too short a timeout drops good replies; none at all hangs the program when a cable is unplugged. Always set one, and log the failures.

  • Parse defensively: a reply can be empty, partial or contain an error code; check before converting to a number.
  • Flush and sync: clear old data before a new request, and match replies to requests, so that a late reply from the last command is not read as the answer to this one.
  • Log the raw lines to a file with a time stamp, and process them later (see Data, Files and Parsing).
  • Threads or asyncio keep a user interface responsive while a slow instrument is read; for a simple logger a loop with a timer is enough.
  • Raspberry Pi and MicroPython: the same language reaches GPIO pins on a single-board computer or a microcontroller (see Embedded Systems).

Units and time

  • Put the unit in the name: length_mm, speed_mps. A bare number cannot warn you that meters met millimeters, as happened to the Mars Climate Orbiter (pound-force seconds against newton seconds).
  • Use a units library for calculations that cross systems: pint stores quantities with units and raises an error on nonsense: (5 * ureg.mm + 2 * ureg.s) fails.
  • Time stamps: store UTC in ISO 8601 (2026-10-31T14:05:09Z), convert to local time only for display; use datetime with a time zone, and time.monotonic() (which never jumps back) to measure durations.
  • Do not use time.sleep() as a clock: it sleeps at least the time you ask, so loops drift; schedule from a fixed start time: next += period; time.sleep(max(0, next - time.monotonic())).

Making Python fast enough

StepWhat to doTypical gain
1. Measurepython -m cProfile -s cumtime script.py, or %timeit in a notebookKnow where the time goes: it is rarely where you think
2. Better algorithmA dictionary or set lookup instead of searching a list; sort once; avoid a loop inside a loopOrders of magnitude for large data
3. VectorizeUse NumPy and pandas operations on whole arrays20 to 100 times
4. Compile the hot loopNumba (@njit), Cython, or a C extension10 to 200 times on numeric loops
5. Use all the coresmultiprocessing or concurrent.futures for independent jobsUp to the number of cores
6. Switch languageOnly for the part that remains slow: C, C++, Rust or Julia (see Choosing a Language)Last resort

Environments, notebooks and sharing

  • Virtual environments keep each project’s libraries separate: python -m venv .venv, activate it, pip install numpy, then pip freeze > requirements.txt so that the project can be rebuilt. Conda and uv do the same job.
  • Jupyter notebooks mix code, results and notes: ideal for exploring data and reporting; move stable code into .py files and import it, and restart and run all before you trust a notebook (cells run out of order hide bugs).
  • Pin the versions of NumPy and friends for a result you must reproduce; libraries change behavior between versions.
  • Share a script with a README, the requirements.txt, a small sample of the data and the command that runs it.
  • Style: follow PEP 8, and let a formatter (black, ruff) and a linter (ruff, flake8) check it automatically.

Python pitfalls

PitfallWhat happensFix
Mutable default argumentdef f(x, items=[]) shares one list between callsUse None and create the list inside
Aliasingb = a makes b the same list; changing b changes ab = a.copy() or list(a); for nested data copy.deepcopy
Float equality0.1 + 0.2 == 0.3 is Falsemath.isclose, np.isclose; see Numbers and Floating Point
Integer arrays overflowNumPy uint8 and int16 arrays wrap silentlyConvert to a wider type before arithmetic
Modifying a list while loopingItems are skippedBuild a new list or loop over a copy
Silent exceptA bug is swallowed and the program carries on wronglyCatch specific errors and log them
Slow string buildingAdding strings in a loop is quadratic"".join(parts)
Notebook stateA cell’s result depends on cells run earlierRestart the kernel and run all

Related

FAQ

What is the difference between a list and a NumPy array?

A list holds anything and is flexible but slow for math; a NumPy array holds one number type in a block of memory and does math on the whole array at C speed. Use arrays for numbers.

Why does my loop change the original data?

Because Python assignment shares a reference and NumPy slices are views. Copy explicitly (.copy()) when you need an independent copy.

Do I need classes?

Not for scripts. Functions and dictionaries do most things. Classes help when a thing has state and behavior that belong together (an instrument connection, a controller), and you will use them in every library.

How do I run a Python script on a schedule or on boot?

Use cron or a systemd timer on Linux or a Raspberry Pi, the Task Scheduler on Windows, or a service. Log to a file so that you can see what happened while you were away.

References

  • Python Software Foundation, The Python Tutorial; NumPy, SciPy and Matplotlib documentation.
  • VanderPlas, J., Python Data Science Handbook; McKinney, W., Python for Data Analysis.
  • PEP 8 (style guide), PEP 20 (the Zen of Python); PyVISA, pyserial and pint documentation.

Last reviewed: October 2026. Library versions and function names change: check the current documentation.