Python for Engineers
The Python an engineer needs: data types, functions, NumPy and Matplotlib, instruments over serial, units, speed and virtual environments.
On this page
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.
| Type | Example | Use |
|---|---|---|
| int, float | 42, 3.14, 1e-3 | Numbers: Python integers never overflow; floats are 64-bit doubles |
| str | "motor 3", f"T = {t:.1f} C" | Text; f-strings format values |
| bool, None | True, None | Flags 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?” |
| bytes | b"\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); slicestemps[1:3]stop before the second number. - Division:
7 / 2is 3.5,7 // 2is 3,7 % 2is 1 (unlike C, where7 / 2is 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.pybecomes a module:import filtersorfrom filters import ema. The standard library already hasmath,statistics,csv,json,datetime,pathlib,struct,argparseandlogging. - Exceptions: catch the specific error you expect (
except ValueError) rather than everything; a bareexcepthides bugs. - Command-line tools:
argparseturns a script into a tool with options and a help message, andif __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
.shapewhen you get one. - Memory. A million
float64values occupy 8 MB; asfloat32only 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) + 10wraps to 4), and mixing integers and floats upcasts. See Numbers and Floating Point. - Missing data:
np.nanpropagates through math; usenp.nanmeanand 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:
pintstores 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; usedatetimewith a time zone, andtime.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
| Step | What to do | Typical gain |
|---|---|---|
| 1. Measure | python -m cProfile -s cumtime script.py, or %timeit in a notebook | Know where the time goes: it is rarely where you think |
| 2. Better algorithm | A dictionary or set lookup instead of searching a list; sort once; avoid a loop inside a loop | Orders of magnitude for large data |
| 3. Vectorize | Use NumPy and pandas operations on whole arrays | 20 to 100 times |
| 4. Compile the hot loop | Numba (@njit), Cython, or a C extension | 10 to 200 times on numeric loops |
| 5. Use all the cores | multiprocessing or concurrent.futures for independent jobs | Up to the number of cores |
| 6. Switch language | Only 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, thenpip freeze > requirements.txtso 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
.pyfiles 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, therequirements.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
| Pitfall | What happens | Fix |
|---|---|---|
| Mutable default argument | def f(x, items=[]) shares one list between calls | Use None and create the list inside |
| Aliasing | b = a makes b the same list; changing b changes a | b = a.copy() or list(a); for nested data copy.deepcopy |
| Float equality | 0.1 + 0.2 == 0.3 is False | math.isclose, np.isclose; see Numbers and Floating Point |
| Integer arrays overflow | NumPy uint8 and int16 arrays wrap silently | Convert to a wider type before arithmetic |
| Modifying a list while looping | Items are skipped | Build a new list or loop over a copy |
Silent except | A bug is swallowed and the program carries on wrongly | Catch specific errors and log them |
| Slow string building | Adding strings in a loop is quadratic | "".join(parts) |
| Notebook state | A cell’s result depends on cells run earlier | Restart the kernel and run all |
Related
- Programming Languages: the overview of this guide.
- Numerical Methods and Filters, PID and Signal Code: what SciPy’s routines do and how to use them well.
- Data, Files and Parsing: files, parsing and time stamps.
- Software Practice, Testing and Debugging: testing and profiling Python code.
- Data Logging and Machine Learning: where Python code often leads.
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.
