Object-oriented programming in Python is a style of writing code around objects that combine data (attributes) and behavior (methods). Objects are created from classes, and the four core Python OOP concepts are encapsulation, abstraction, inheritance and polymorphism. Together they make code reusable, modular and easier to maintain as programs grow.
Picture a Python script that reads one temperature sensor. Easy. Now add a humidity sensor, a light sensor and a motor driver. A few weeks later the file is full of copy-pasted functions and global variables, and one small change breaks three other things.
That is the moment object oriented programming in Python starts to make sense. Instead of juggling loose variables and functions, you group related data and behavior into small, reusable units called objects.
This guide explains the Python OOP concepts step by step, using sensor and board examples that embedded and IoT engineers will recognize. Every code example was run on Python 3.12 before publishing, so you can copy it, run it and experiment.
What you will find in this guide:
Who this guide is for: engineering undergraduates, fresh graduates and early-career developers who know basic Python (variables, loops, functions) and want to write cleaner, more scalable programs. If lists and dictionaries still feel shaky, brush up on Python data types first.
Object-oriented programming (OOP) in Python is a programming paradigm that organizes code into classes and objects, so data and the functions that work on that data stay together. Python supports OOP fully, and everything in Python, from numbers to functions, is an object.
Four building blocks appear in every OOP program:
Sensor.temp = Sensor('Temperature', 4).temp.pin.temp.describe().Think of a circuit schematic. The schematic (class) defines the parts and connections once. Every board you assemble from it (object) is a separate physical unit with its own state.
One class produces many objects, and four pillars guide how that class is designed. Look at the big picture first, then each pillar turns into working code in the next section.
OOP IN PYTHON AT A GLANCE
+-------------------------------+
| CLASS: Sensor (blueprint) |
| attributes + methods |
+---------------+---------------+
| creates
+-----------------------+-----------------------+
| | |
v v v
+-----------+ +-----------+ +-----------+
| temp | | light | | humidity |
| own data | | own data | | own data |
+-----------+ +-----------+ +-----------+
THE FOUR PILLARS THAT SHAPE EVERY CLASS
+----------------+----------------+----------------+----------------+
| Encapsulation | Abstraction | Inheritance | Polymorphism |
+----------------+----------------+----------------+----------------+
| Bundle data + | Expose only | A child class | Same method |
| methods; hide | what matters; | reuses and | name, many |
| the internals | hide the how | extends parent| behaviors |
+----------------+----------------+----------------+----------------+| OOP Concept | Core Idea | Main Python Tools |
|---|---|---|
| Encapsulation | Keep data and the methods that use it together, and control access to it. | _protected, __private, @property |
| Abstraction | Expose what an object does and hide how it does it. | abc.ABC, @abstractmethod |
| Inheritance | Let a child class reuse and extend a parent class. | class Child(Parent), super() |
| Polymorphism | Use one method name with different behavior in different classes. | Method overriding, duck typing, operator overloading |
Keep this table handy. The next section turns each row into code.
Each example is short, complete and runnable. Copy it into a file, run it, then change a value and watch the output change.
A class defines the structure. An object is a real instance of that structure, holding its own data.
class Sensor:
sensor_count = 0 # class attribute: shared by every object
def __init__(self, name, pin): # initializer (often called the constructor)
self.name = name # instance attributes: unique to each object
self.pin = pin
Sensor.sensor_count += 1
def describe(self): # instance method
return f'{self.name} sensor on pin {self.pin}'
temp = Sensor('Temperature', 4) # object 1
light = Sensor('Light', 7) # object 2
print(temp.describe()) # Temperature sensor on pin 4
print(light.describe()) # Light sensor on pin 7
print(Sensor.sensor_count) # 2class Sensor: defines the blueprint.__init__ is the initializer, commonly called the constructor. Python runs it automatically every time you create an object.self refers to the object being created or used. Python passes it for you, so you never type it when calling a method.name and pin are instance attributes: each object gets its own copy.sensor_count is a class attribute: one copy shared by all objects.Encapsulation means bundling data with the methods that operate on it, and controlling how outside code can touch that data. It protects an object from accidental misuse.
Python has three access levels, all based on naming:
voltage): Accessible from anywhere._voltage): A convention that tells other developers “internal use only”. Python does not enforce it.__voltage): Python renames it internally through name mangling, which makes accidental access harder.class BatteryMonitor:
def __init__(self, voltage):
self.voltage = voltage # goes through the setter, so validation runs here too
@property
def voltage(self): # getter
return self.__voltage
@voltage.setter
def voltage(self, value): # setter with a safety check
if not 0 <= value <= 5:
raise ValueError('Voltage must be between 0 and 5 V')
self.__voltage = value # double underscore: private (name-mangled)
battery = BatteryMonitor(3.7)
battery.voltage = 4.2
print(battery.voltage) # 4.2
try:
battery.voltage = 12 # rejected by the setter
except ValueError as error:
print(error) # Voltage must be between 0 and 5 VA bad value written straight into a sensor object, such as 12 V on a 5 V channel, can travel through your whole program. The setter blocks it at the door.
Good to know: Name mangling prevents accidents, not determined access. battery._BatteryMonitor__voltage still works. Treat private attributes as a signal of intent, not a security feature.
Abstraction means showing only what an object does and hiding how it does it. You call read() on a sensor without caring whether the value comes from an ADC, an I2C bus or a simulator.
Python implements abstraction with the abc module (Abstract Base Classes).
from abc import ABC, abstractmethod
class Device(ABC): # abstract base class
@abstractmethod
def read(self):
"""Every device must define how it returns a reading."""
class LightSensor(Device):
def read(self):
return 512 # imagine real ADC code here
# device = Device() # TypeError: abstract classes cannot be instantiated
sensor = LightSensor()
print(sensor.read()) # 512Device defines a contract: every device must implement read().TypeError.read() fails the same way at object creation, so the mistake surfaces early instead of halfway through a run.Inheritance lets a child class reuse the attributes and methods of a parent class, and add or change only what it needs.
class Sensor:
def __init__(self, name, pin):
self.name = name
self.pin = pin
def describe(self):
return f'{self.name} sensor on pin {self.pin}'
class TemperatureSensor(Sensor): # child class: inherits from Sensor
def __init__(self, pin, unit='C'):
super().__init__('Temperature', pin) # reuse the parent initializer
self.unit = unit
def read(self): # behavior only the child has
return f'27.5 {self.unit}'
sensor = TemperatureSensor(4)
print(sensor.describe()) # Temperature sensor on pin 4 (inherited)
print(sensor.read()) # 27.5 C (child's own)
print(TemperatureSensor.__mro__) # the lookup order Python followsTip: With multiple inheritance, Python decides which parent’s method wins using the Method Resolution Order (MRO). Print ClassName.__mro__ to see the exact lookup order.
Polymorphism means one method name can behave differently depending on the object that calls it. It lets you write one loop that works with many kinds of objects.
class Sensor:
def read(self):
raise NotImplementedError('Child classes must override read()')
class TemperatureSensor(Sensor):
def read(self): # overrides the parent method
return '27.5 C'
class HumiditySensor(Sensor):
def read(self): # same name, different behavior
return '61 %'
for sensor in (TemperatureSensor(), HumiditySensor()):
print(sensor.read()) # 27.5 C, then 61 %read() method, Python calls it, even when the class shares no parent. If it walks like a duck and quacks like a duck, it is treated as a duck.__add__ let + work on your own objects.One thing to remember: Python does not support classic method overloading (same name, different parameter lists). If you define a method twice, the last definition wins. Use default arguments instead.
This example models two sensors that share one abstract design. Read it from top to bottom and match each comment to a pillar.
from abc import ABC, abstractmethod
class Sensor(ABC): # Abstraction: defines the contract
def __init__(self, name, pin):
self.name = name
self.__pin = pin # Encapsulation: hidden from outside code
@property
def pin(self): # read-only access to the pin
return self.__pin
@abstractmethod
def read(self):
pass
def report(self): # shared behavior, written once
return f'{self.name} (pin {self.pin}): {self.read()}'
class TemperatureSensor(Sensor): # Inheritance
def __init__(self, pin):
super().__init__('Temperature', pin)
def read(self): # Polymorphism: its own read()
return '27.5 C'
class HumiditySensor(Sensor):
def __init__(self, pin):
super().__init__('Humidity', pin)
def read(self): # Polymorphism: a different read()
return '61 %'
sensors = [TemperatureSensor(4), HumiditySensor(5)]
for sensor in sensors:
print(sensor.report())Temperature (pin 4): 27.5 C
Humidity (pin 5): 61 %Sensor(ABC) with the abstract read() method.__pin is private and exposed through a read-only pin property.TemperatureSensor and HumiditySensor reuse the initializer and report() from Sensor.sensor.report() calls a different read() for each object inside a single loop.Now add a LightSensor class with its own read() and append it to the list. The loop works without a single change. That is the real payoff of object-oriented programming in Python.
The advantages of OOP show up most when a project grows beyond a single file. Here is what you gain:
OOP is a tool, not a rule, and knowing its costs helps you decide when to use it. Watch out for these:
self, inheritance chains, abstract classes and the MRO all take practice.__slots__ can reduce per-object memory. Our guide to Python in embedded systems covers these trade-offs in more depth.Object-oriented programming organizes a program around objects that hold both data and behavior, while procedural programming organizes it around functions that run step by step on separate data. The table below shows how the two styles compare.
| Feature | Object-Oriented Programming (OOP) | Procedural Oriented Programming (POP) |
|---|---|---|
| Basic approach | Bottom-up: design objects first, then combine them. | Top-down: start with the main task and split it into functions. |
| Program structure | Classes and objects. | Functions and a sequence of steps. |
| Data and functions | Bundled together inside objects. | Kept separate: data is passed to functions. |
| Data security | Data can be hidden and validated through encapsulation. | Data is often global or exposed, so it is easier to change by accident. |
| Code reuse | Inheritance, composition and polymorphism. | Mostly by calling the same functions again. |
| Best suited for | Large, growing projects with many similar components. | Short scripts and small, linear tasks. |
| Adding features | Add a new class and leave existing code alone. | Often means editing existing functions. |
| Real-world modeling | Natural: sensors, motors and packets become objects. | Less natural: the focus stays on actions. |
| Typical languages | Python, Java, C++, C#. | C, Pascal, Fortran, BASIC. |
| In Python | class, objects and methods. | Plain functions and modules. |
Python and C++ support both styles, so the choice is about design, not the language.
Here is a small job: switch two LEDs on and off. First, the procedural version.
def toggle(led):
led['on'] = not led['on']
red = {'pin': 12, 'on': False} # data lives here...
green = {'pin': 13, 'on': False}
toggle(red) # ...functions live elsewhere
print(red) # {'pin': 12, 'on': True}class Led:
def __init__(self, pin):
self.pin = pin
self.on = False
def toggle(self): # data and behavior live together
self.on = not self.on
red = Led(12)
green = Led(13)
red.toggle()
print(red.on) # Truered['on'] = 'banana'.toggle() belongs to it.self as its first parameter, and attributes need the self. prefix to belong to the object.def __init__(self, readings=[]) shares one list across all objects. Use None as the default and create the list inside the method.obj.sensor_count = 5) creates a new instance attribute instead of changing the shared class attribute.
Object-oriented programming in Python comes down to one idea: keep related data and behavior together, and hide whatever the rest of the program does not need to see.
Is this conversation helpful so far?
The four pillars of OOP in Python are encapsulation, abstraction, inheritance and polymorphism. Encapsulation bundles data with methods and controls access, abstraction hides implementation details, inheritance lets a child class reuse a parent class, and polymorphism lets one method name behave differently across classes.
A class is a blueprint that defines attributes and methods, while an object is a specific instance created from that blueprint with its own data. One class can create any number of objects. For example, temp and light are two separate objects created from the same Sensor class.
self is the first parameter of an instance method, and it refers to the object that is calling the method. Python passes it automatically, which is why you write temp.describe() with no arguments even though the method is defined as describe(self).
No. Python is a multi-paradigm language. Everything in Python is an object, including numbers, strings and functions, but the language does not force you to write classes. You can write procedural, object-oriented or functional code, and mix them in one project.
Yes. A class can inherit from more than one parent, for example class C(A, B). When both parents define the same method, Python picks one using the Method Resolution Order (MRO), which you can inspect with C.__mro__.
Yes, when you manage several similar components such as sensors, motors or communication modules, because a class keeps each component’s state and behavior together. For very small scripts on memory-limited boards, plain functions are often lighter and simpler.
Indian Institute of Embedded Systems – IIES