Learn How to Make Code Object-Oriented in Python: Absolute Beginners Tutorial

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.

Note

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:

python
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:

python
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.

python
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
Pro Tip

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.

code builder click tokens to place them in order

Build the correct __init__ signature for a Car class that accepts make and year as parameters.

click tokens below to place them here…
self.year = year ( self , make , year ): __init__ self.make = make def class Car:
Hint: A method definition always starts with 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.

python
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
Watch Out

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.

python
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.
spot the bug click the line that contains the error

This Dog class has one bug that will cause an AttributeError when bark() is called. Find it.

1 class Dog:
2 def __init__(self, name, breed):
3 self.name = name
4 breed = breed
5 def bark(self):
6 return f"{self.name} the {self.breed} says: Woof!"
Fix: Line 4 should be 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:

python
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.

python
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.

python
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
EncapsulationBundles data and logic; controls access_attribute convention, getter/setter methods
InheritanceChild class reuses parent class codeclass Child(Parent):
PolymorphismOne interface, many implementationsMethod overriding, duck typing
AbstractionHides complexity, exposes clean interfacePublic methods hiding private logic
code builder click tokens to place them in order

Build the correct class declaration line for a GuideDog that inherits from Dog.

click tokens below to place them here…
( Dog ): class def GuideDog ( Animal ):
Hint: A class declaration starts with 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.

  1. Declare the class

    Use the class keyword followed by a PascalCase name and a colon. Everything inside the class is indented one level. Start with pass if you are not ready to add any content yet.

  2. 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 to self.attribute_name inside the method body. These become the object's instance attributes.

  3. Write instance methods

    Add functions that operate on the object's data. Each must accept self as its first parameter. Inside the method, refer to the object's attributes as self.attribute_name and call other methods as self.method_name().

  4. Create objects

    Call the class by name and pass the arguments that match __init__'s parameters (excluding self). Assign the result to a variable. Each call produces an independent object with its own copy of the instance attributes.

  5. Call methods on your objects

    Use dot notation — object.method() — to call methods. Read attributes directly with object.attribute. You can also reassign attributes: object.name = "new value" updates that instance without affecting any other object.

python
# 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

  1. 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.
  2. __init__ runs automatically when an object is created and sets up instance attributes. self is a reference to the object being created — Python supplies it automatically.
  3. Instance attributes belong to a single object; class attributes are shared across all instances. Keep mutable per-object state in __init__.
  4. The four pillars — encapsulation, inheritance, polymorphism, and abstraction — are not separate features. They work together to produce code that is organized, reusable, and maintainable.
  5. 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.

check your understanding question 1 of 5

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.