In the domain of control engineering, the PID (Proportional-Integral-Derivative) controller is the place to start. In this post we will see how to implement a PID in Python from scratch.
The example will show you how to implement the Controller in Python to control the speed of a car.
The following picture shows how the theoretical diagram looks like.

For more information on how a PID Controller works, see PID Controller: The Magic Behind Everyday Systems.
Plant model
The model is a one-dimensional car of mass
, pushed by a force
and subject to aerodynamic drag
. Where:
: drag coefficient
: reference area
: air density
: speed
The dynamic equation is:
![]()
The parameters used are taken from the Porsche Taycan Turbo (Wikipedia):
For convenience
is called
in the model.
Porsche Taycan Turbo
For the Porsche Taycan Turbo Wikipedia reports:
- Max speed: 260 km/h
- 0 – 100 km/h: 3.2 s
Based on this info we can assume (approximation) that the maximum force that the electric powertrain can develop depends on the speed (intuitive).
We assume that the dependence is linear as shown in the figure below:

From the max speed we have:
![]()
where ![]()
Then we can simulate the model in Python to find
that gives 0-100 km/h in 3.2 s. This results in
.
Implementation in Python
The implementation in Python consists of 3 main elements:
- PID Class
- Car Class
- Main Function
The code is available on GitHub: https://github.com/simorxb/PID-Controller-Python.
PID Controller Class
The PID Controller class in Python creates the PID controller object. It is based on several parameters:
,
,
: These are the proportional, integral, and derivative gains, respectively. They determine the response characteristics of the controller.
: This is the anti-windup gain, which helps in preventing the integral term from getting too large when the actuator is in saturation.
: The time constant for derivative filtering, which smooths out the derivative term.
: The time step, which is the time interval between each step of the controller.- max, min: These set the maximum and minimum bounds for the controller output.
- max_rate: The maximum rate of change (per second) of the controller command.
The __init__ method initializes these parameters, setting the stage for the controller to operate.
The Step function of the PID class is where the action happens. This function takes the current measurement of the process variable and the desired setpoint as inputs.
By calculating the error, updating the integral term with anti-windup correction, and filtering the derivative term, the Step function computes the PID command.
This command is then constrained within set limits and rate of change, ensuring that the output is smooth and controlled.
class PID:
""" This class implements a PID controller.
"""
def __init__(self, Kp, Ki, Kd, Kaw, T_C, T, max, min, max_rate):
self.Kp = Kp # Proportional gain
self.Ki = Ki # Integral gain
self.Kd = Kd # Derivative gain
self.Kaw = Kaw # Anti-windup gain
self.T_C = T_C # Time constant for derivative filtering
self.T = T # Time step
self.max = max # Maximum command
self.min = min # Minimum command
self.max_rate = max_rate # Maximum rate of change of the command
self.integral = 0 # Integral term
self.err_prev = 0 # Previous error
self.deriv_prev = 0 # Previous derivative
self.command_sat_prev = 0 # Previous saturated command
self.command_prev = 0 # Previous command
self.command_sat = 0 # Current saturated command
self.command = 0 # Current command
def Step(self, measurement, setpoint):
""" Execute a step of the PID controller.
Inputs:
measurement: current measurement of the process variable
setpoint: desired value of the process variable
"""
# Calculate error
err = setpoint - measurement
# Update integral term with anti-windup
self.integral += self.Ki*err*self.T + self.Kaw*(self.command_sat_prev - self.command_prev)*self.T
# Calculate filtered derivative
deriv_filt = (err - self.err_prev + self.T_C*self.deriv_prev)/(self.T + self.T_C)
self.err_prev = err
self.deriv_prev = deriv_filt
# Calculate command using PID equation
self.command = self.Kp*err + self.integral + self.Kd*deriv_filt
# Store previous command
self.command_prev = self.command
# Saturate command
if self.command > self.max:
self.command_sat = self.max
elif self.command < self.min:
self.command_sat = self.min
else:
self.command_sat = self.command
# Apply rate limiter
if self.command_sat > self.command_sat_prev + self.max_rate*self.T:
self.command_sat = self.command_sat_prev + self.max_rate*self.T
elif self.command_sat < self.command_sat_prev - self.max_rate*self.T:
self.command_sat = self.command_sat_prev - self.max_rate*self.T
# Store previous saturated command
self.command_sat_prev = self.command_sat
Car Class
The Car class models a one-dimensional vehicle (as described above). This class has its own set of parameters:
: Mass of the car.
: Aerodynamic drag coefficient.- F_max_0, F_max_max: These parameters define the maximum force generated by the powertrain at zero and maximum speed.
: The maximum speed of the car.
: The time step, which, as in the PID class, is the time interval between each step.
The Step function in the Car class takes an applied force as an input and updates the car’s speed.
It first determines the maximum force generated at the current speed and then saturates the input force if necessary. Following this, the function calculates the rate of change of speed and updates the current speed.
class Car:
""" This class represents a car moving in 1D, subject to a throttle force F, with mass m,
aerodynamic drag coefficient b, F_max/F_min forces, and time step T.
"""
def __init__(self, m, b, F_max_0, F_max_max, v_max, T):
self.m = m # Mass of the car
self.b = b # Aerodynamic drag coefficient
self.F_max_0 = F_max_0 # Max force applied to the car by the powertrain at 0 speed
self.F_max_max = F_max_max # Max force applied to the car by the powertrain at max speed
self.v_max = v_max # Max speed (m/s)
self.T = T # Time step
self.v = 0 # Speed of the car
def Step(self, F):
""" Update the speed of the car based on the applied force F.
"""
# Max force applied by the powertrain depends on the speed
v_to_F_max_x_axis = [0, self.v_max]
F_max_y_axis = [self.F_max_0, self.F_max_max]
if self.v < v_to_F_max_x_axis[0]:
F_max = F_max_y_axis[0]
elif self.v > v_to_F_max_x_axis[-1]:
F_max = F_max_y_axis[-1]
else:
F_max = np.interp(self.v, v_to_F_max_x_axis, F_max_y_axis)
# Saturate input force
if F > F_max:
F_sat = F_max
elif F < 0:
F_sat = 0
else:
F_sat = F
# Calculate the derivative dv/dt using the input force and the car's speed and properties
dv_dt = (F_sat - self.b*self.v*self.v)/self.m
# Update the speed by integrating the derivative using the time step T
self.v += dv_dt*self.T
Implementing the PID Control Loop
In the main function, we simulate the car’s speed control using the PID controller implemented.
We initialize the PID controller and Car objects with specific parameters:
- The car’s parameters are initialized with the values corresponding to the Porsche Taycan Turbo, as calculated previously.
- The PID Controller is initialized with an educated manual guess.
We then enter a loop that represents our simulation. At each step, we calculate the car’s current speed and use the PID controller to compute the force needed to reach a predefined setpoint speed (42 m/s). This force is then applied to the car, and the speed is updated accordingly.
Lastly, we plot the car’s speed response and the force command over time to visualize the control system’s performance:

import numpy as np
from lib import Car, PID
import matplotlib.pyplot as plt
def main():
# -------- Configuration --------
# Simulation parameters
time_step = 0.1
end_time = 25
length = round(end_time/time_step)
t = np.zeros(length)
stp = np.zeros(length)
v = np.zeros(length)
command = np.zeros(length)
# Car parameters
m = 2140
b = 0.33
F_max_0 = 22000
F_max_max = 1710
v_max = 72
# PID parameters
Kp = 800.0
Ki = 70.0
Kaw = 1.0
Kd = 20.0
T_C = 1.0
# Initialize PID controller
pid = PID(Kp, Ki, Kd, Kaw, T_C, time_step, F_max_0, 0, 30000)
# Initialize car with given parameters
car = Car(m, b, F_max_0, F_max_max, v_max, time_step)
# Iterate through time steps
for idx in range(0, length):
t[idx] = idx*time_step
# Set setpoint
stp[idx] = 42
# Execute the control loop
v[idx] = car.v
pid.Step(v[idx], stp[idx])
command[idx] = pid.command_sat
car.Step(command[idx])
# Plot speed response
plt.subplot(2, 1, 1)
plt.plot(t, v, label="Response")
plt.plot(t, stp, '--', label="Setpoint")
plt.xlabel("Time [s]")
plt.ylabel("Speed [m/s]")
plt.legend()
plt.grid()
# Plot command force
plt.subplot(2, 1, 2)
plt.plot(t, command, label="Command")
plt.xlabel("Time [s]")
plt.ylabel("Force [N]")
plt.legend()
plt.grid()
# Display the plots
plt.show()
main()
Conclusion
Implementing a PID controller in Python can be useful whether you need to run a simulation or you are dealing with the real-world using a microcontroller based on Python. With this Python-based PID controller, you can control not just virtual cars but potentially any process.
Remember, the PID controller is a versatile tool, and while this example is automotive-focused, the principles remain the same across a multitude of applications.
Happy control engineering!
Feel free to ask any questions in the comments, and if you want to dive deeper into the world of PID Controllers here’s my video course:





Hello Simone,
Can you give me coupon to enroll in your PID Control course? I click the link but it is not applying the 25% off. Also, I read the pre-req’s for the course. I have been out of school for a while now. Do not remember Laplace transform. Do you have some reading materials prior that I can work towards prior to starting the course? Thank you
Hi quoit1,
Apologies for the discount, my bad, if you try again the 25% off will be applied. Please let me know if that’s not the case.
Regarding the Laplace transform I don’t have anything to suggest on the spot, but I can look around.
If you have any book on control engineering that should give a good enough intro to the Laplace transform.
Hy can I use this work to implement in research paper? how can I add reference
Hi Waleed, of course, you can use it. For the reference you can simply mention: “PID Controller in Python – How to implement it”, published by Simone Bertoni on simonebertonilab.com. Is that okay?
Hi Simone,
I am now working on the PID controller system. But I have no idea how to run the PID python code. Do you have any tutorial how to use it? I try copy the codes on to the python Idle, but it is not working.
Thanks so much
Hi David, thanks for your interest.
I think you want to use a more advanced IDE than IDLE, for example VS Code. You could follow this tutorial to create a folder with a virtual environment:
https://code.visualstudio.com/docs/python/python-tutorial
Then install matplotlib and numpy in the venv using “pip install” and copy the two files main.py and lib.py from https://github.com/simorxb/PID-Controller-Python
At this point you should be able to run main.py