Object-oriented programming (OOP) is the design philosophy behind a large portion of Python code in the wild. Once you understand how to write a class, read a class, and think in terms of objects, the entire Python ecosystem — from Django to data science libraries — starts to make sense at a structural level.
This tutorial walks through OOP from the very beginning. You will learn what a class is, how to create objects from it, what self and __init__ actually do, and how encapsulation, inheritance, and polymorphism work in practice. Every concept is demonstrated with working Python code you can run immediately.
What Is Object-Oriented Programming?
Procedural Python code is organized as a sequence of instructions: assign a variable, call a function, loop over a list. That approach works well for small scripts. As a program grows, though, managing state becomes harder — functions need more parameters, data and the logic that operates on it drift apart, and reuse becomes difficult.
Object-oriented programming solves this by grouping related data and behavior into a single unit called an object. An object knows things (its attributes) and can do things (its methods). The template that defines what every object of a given type knows and can do is called a class.
Python does not force you to use OOP. Procedural and functional styles are equally valid. OOP becomes useful when your program models real-world entities, when you need reusable components, or when a team shares a codebase and consistent structure matters.
Think of a class as a cookie cutter. The cutter defines the shape. Each cookie you press out is an object — an independent instance that has the same shape but can have different decorations (attribute values). You can press out as many cookies as you need from one cutter.
Classes and Objects: The Core Pair
Defining a class
The class keyword opens a class definition. By convention, class names use PascalCase (each word capitalized, no underscores). The simplest possible class has a name and a placeholder body:
class Dog:
pass # placeholder — a valid but empty class body
That is a complete class. It has no attributes or methods yet, but Python accepts it. pass is required because Python needs at least one statement inside any indented block.
Creating objects (instances)
You create an object by calling the class like a function. Each call returns a new, independent instance:
dog_one = Dog() # first Dog object
dog_two = Dog() # second Dog object, completely independent
print(type(dog_one)) # <class '__main__.Dog'>
print(dog_one is dog_two) # False — they are separate objects
The __init__ method and self
A class with no data is rarely useful. The special method __init__ runs automatically every time a new object is created. It is where you set up the object's initial attributes. The first parameter, self, is a reference to the object being created — Python passes it automatically, so you never provide it yourself when calling the class.
class Dog:
def __init__(self, name, breed):
self.name = name # instance attribute
self.breed = breed # instance attribute
rex = Dog("Rex", "German Shepherd")
bella = Dog("Bella", "Labrador")
print(rex.name) # Rex
print(bella.breed) # Labrador
self is a convention, not a keyword — you could technically name it anything. Every experienced Python developer uses self, so deviating from it makes your code harder to read. Stick with self.
Build the correct __init__ signature for a Car class that accepts make and year as parameters.
def, followed by the method name, then the parameter list in parentheses ending with a colon. __init__ must include self as the first parameter. The assignment statements follow on indented lines below.
Instance Attributes, Class Attributes, and Methods
Instance attributes vs. class attributes
An instance attribute is set on an individual object — each object has its own copy. A class attribute is defined directly inside the class body (outside any method) and is shared by every instance unless explicitly overridden on an individual object.
class Dog:
species = "Canis familiaris" # class attribute — shared by all Dogs
def __init__(self, name, breed):
self.name = name # instance attribute — unique per object
self.breed = breed # instance attribute — unique per object
rex = Dog("Rex", "German Shepherd")
bella = Dog("Bella", "Labrador")
print(rex.species) # Canis familiaris
print(bella.species) # Canis familiaris — same value from class
print(rex.name) # Rex
print(bella.name) # Bella — different per instance
Avoid using mutable class attributes (like lists or dicts) to hold per-object data. All instances share the same object, so modifying it from one instance modifies it for all of them. Keep mutable state in __init__ as instance attributes.
Instance methods
A method is just a function defined inside a class. The convention requires self as the first parameter so the method can access the calling object's attributes. You call it with dot notation on the object — Python fills in self automatically.
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
return f"{self.name} says: Woof!"
def description(self):
return f"{self.name} is a {self.breed}."
rex = Dog("Rex", "German Shepherd")
print(rex.bark()) # Rex says: Woof!
print(rex.description()) # Rex is a German Shepherd.
This Dog class has one bug that will cause an AttributeError when bark() is called. Find it.
self.breed = breed. Without self., the assignment creates a plain local variable inside __init__ that disappears when the method returns. The self.breed instance attribute is never created, so accessing it in bark() raises an AttributeError.
The Four Pillars of OOP in Python
Encapsulation
Encapsulation means keeping an object's internal data protected and only exposing it through controlled methods. This prevents outside code from accidentally corrupting an object's state. In Python, you signal a private attribute by prefixing its name with a single underscore:
class BankAccount:
def __init__(self, owner, initial_balance=0):
self.owner = owner
self._balance = initial_balance # convention: treat as private
def deposit(self, amount):
if amount > 0:
self._balance += amount
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
else:
print("Insufficient funds or invalid amount.")
def get_balance(self):
return self._balance
account = BankAccount("Alice", 500)
account.deposit(200)
account.withdraw(100)
print(account.get_balance()) # 600
Outside code uses deposit(), withdraw(), and get_balance() rather than touching _balance directly. The class controls every change to its own data.
Inheritance
Inheritance lets one class (the child or subclass) reuse the code of another class (the parent or superclass). You declare it by placing the parent name in parentheses after the child class name. The child automatically gets all the parent's methods and attributes and can extend or override them.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal): # Dog inherits from Animal
def speak(self): # override the parent method
return f"{self.name} says: Woof!"
class Cat(Animal): # Cat inherits from Animal
def speak(self): # override the parent method
return f"{self.name} says: Meow!"
animals = [Dog("Rex"), Cat("Whiskers"), Animal("Generic")]
for a in animals:
print(a.speak())
"Classes provide a means of bundling data and functionality together." — Python Software Foundation Documentation
Polymorphism
Polymorphism is what allows the loop above to call speak() on every object without caring what type each one is. Each class provides its own version of the method, and Python calls the right version based on the actual type of the object at runtime. This is called method overriding.
Polymorphism also works through shared interfaces — two classes that are unrelated by inheritance can both define a method with the same name, and code that calls that method can work with either class interchangeably.
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
print(f"Area: {shape.area():.2f}")
# Area: 78.54
# Area: 24.00
Abstraction
Abstraction means hiding complex implementation details and exposing only what a caller needs to know. You have already seen this in action: callers use deposit() and withdraw() on BankAccount without knowing or caring how the internal balance is tracked. The internal logic is encapsulated; the public interface is the abstraction.
| Pillar | What it does | Python mechanism |
|---|---|---|
| Encapsulation | Bundles data and logic; controls access | _attribute convention, getter/setter methods |
| Inheritance | Child class reuses parent class code | class Child(Parent): |
| Polymorphism | One interface, many implementations | Method overriding, duck typing |
| Abstraction | Hides complexity, exposes clean interface | Public methods hiding private logic |
Build the correct class declaration line for a GuideDog that inherits from Dog.
class, then the new class name in PascalCase. To inherit from a parent, place the parent class name in parentheses directly after the child name, then add a colon to open the class body.
How to Write a Python Class from Scratch
The following five steps take you from an empty file to a working class with attributes, methods, and objects.
-
Declare the class
Use the
classkeyword followed by a PascalCase name and a colon. Everything inside the class is indented one level. Start withpassif you are not ready to add any content yet. -
Add the __init__ method
Define
def __init__(self, ...):inside the class body. List all the data you want each object to carry as additional parameters. Assign them toself.attribute_nameinside the method body. These become the object's instance attributes. -
Write instance methods
Add functions that operate on the object's data. Each must accept
selfas its first parameter. Inside the method, refer to the object's attributes asself.attribute_nameand call other methods asself.method_name(). -
Create objects
Call the class by name and pass the arguments that match
__init__'s parameters (excludingself). Assign the result to a variable. Each call produces an independent object with its own copy of the instance attributes. -
Call methods on your objects
Use dot notation —
object.method()— to call methods. Read attributes directly withobject.attribute. You can also reassign attributes:object.name = "new value"updates that instance without affecting any other object.
# Step 1 — Declare the class
class Book:
# Step 2 — Define __init__
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
self._current_page = 0 # encapsulated — treated as private
# Step 3 — Write instance methods
def read(self, pages):
self._current_page = min(self._current_page + pages, self.pages)
def progress(self):
pct = (self._current_page / self.pages) * 100
return f"{pct:.1f}% read ({self._current_page}/{self.pages} pages)"
def __repr__(self):
return f"Book('{self.title}' by {self.author})"
# Step 4 — Create objects
book_one = Book("Clean Code", "Robert C. Martin", 431)
book_two = Book("Fluent Python", "Luciano Ramalho", 790)
# Step 5 — Call methods
book_one.read(50)
book_one.read(75)
print(book_one.progress()) # 29.0% read (125/431 pages)
print(book_two.progress()) # 0.0% read (0/790 pages)
print(book_one) # Book('Clean Code' by Robert C. Martin)
Python Learning Summary Points
- A class is a blueprint. An object is an instance created from that blueprint. You can create as many independent objects as you need from one class.
__init__runs automatically when an object is created and sets up instance attributes.selfis a reference to the object being created — Python supplies it automatically.- Instance attributes belong to a single object; class attributes are shared across all instances. Keep mutable per-object state in
__init__. - The four pillars — encapsulation, inheritance, polymorphism, and abstraction — are not separate features. They work together to produce code that is organized, reusable, and maintainable.
- OOP does not replace procedural or functional Python. Choose it when your program models entities with shared structure, when reuse matters, or when the codebase will be maintained by a team.
The patterns shown here apply directly throughout the Python standard library and in every major framework. Once you can read and write classes confidently, the behavior of Python's built-in types — lists, dicts, file objects — starts to make mechanical sense, because they are all objects built on these same principles.
Frequently Asked Questions
Object-oriented programming (OOP) in Python is a style of writing code that organizes data and behavior into reusable units called classes. Each class acts as a blueprint, and objects are the actual instances created from it. OOP makes large programs easier to structure, test, and maintain.
A class in Python is a template that defines the attributes (data) and methods (functions) that its objects will have. You define a class with the class keyword followed by a PascalCase name and a colon. Everything inside the indented block belongs to that class.
__init__ is Python's initializer method, called automatically when you create a new object from a class. It sets up the initial state of the object by assigning values to its attributes using self.attribute = value. It is not a constructor in the C++ sense — the object already exists when __init__ runs.
self is a reference to the current instance of the class. Python passes it automatically as the first argument to every instance method, giving the method access to the object's attributes and other methods. You write it explicitly in the method signature but never supply it when calling the method.
A class is the blueprint — it describes what attributes and methods instances will have. An object is a specific instance created from that blueprint. You can create many objects from a single class, each with its own attribute values and independent state.
The four pillars are encapsulation (bundling data and methods together and restricting direct access), inheritance (letting a child class reuse code from a parent class), polymorphism (allowing different classes to share a method name that behaves differently per class), and abstraction (hiding complex internal details and exposing only what callers need).
Encapsulation means keeping an object's internal data protected from direct outside access. In Python, you signal private attributes by prefixing the name with an underscore (e.g., _balance), then expose controlled access through public methods. Python does not enforce access restrictions at the language level, but the underscore convention is widely respected.
Inheritance allows one class (the child) to acquire the attributes and methods of another class (the parent). You declare it by passing the parent class name in parentheses after the child class name: class Dog(Animal):. The child can override any parent method by defining its own version with the same name.
Polymorphism lets different classes define the same method name so that code can call that method without knowing which class it is working with. The correct implementation runs automatically depending on the object's type. Python supports this through method overriding and duck typing.
No. Python supports multiple paradigms. Procedural code using plain functions works well for small scripts and automation tasks. OOP becomes valuable when your program grows, when you need to model real-world entities with shared structure, or when multiple developers share a codebase that needs consistent organization.