Classes and Objects in Python (Part 1)
Table of Contents
- Introduction: Templates
- Procedural vs. Object-Oriented Programming
- Procedural Programming
- Object-Oriented Programming
- Objects and Classes
- Objects
- Classes
- Features of Object-Oriented Programming: Encapsulation, Inheritance, Polymorphism
- Encapsulation
- Inheritance
- Polymorphism
1. Introduction: Templates
Imagine we're designing a game where dogs and people fight. We first need to describe a dog and a person. For instance, a dog has a name, breed, attack power, etc. We can represent its attributes using a dictionary:
dog1 = {
"name": "zhaopeng",
"breed": "Husky",
"attack_val": 30,
"life_val": 50,
}
print(dog1)
# {'name': 'zhaopeng', 'breed': 'Husky', 'attack_val': 30, 'life_val': 50}
To reuse this structure, we can wrap it in a function. Each call with different parameters creates a new dog:
def create_dog(name, breed, attack_val):
data = {
"name": name,
"breed": breed,
"attack_val": attack_val,
"life_val": 100, # default value
}
return data
dog1 = create_dog("zhaopeng", "Husky", 30)
dog2 = create_dog("zhangsan", "Mastiff", 50)
print(dog1, dog2)
# {'name': 'zhaopeng', 'breed': 'Husky', 'attack_val': 30, 'life_val': 100}
# {'name': 'zhangsan', 'breed': 'Mastiff', 'attack_val': 50, 'life_val': 100}
Similarly, we can create a template for a person. We can also map breed to attack value and age to attack value:
# Mapping breed to attack value
breed_attack = {
"Husky": 20,
"Mastiff": 40,
"Puppy": 5
}
def create_dog(name, breed):
data = {
"name": name,
"breed": breed,
"life_val": 100,
}
if breed in breed_attack:
data["attack_val"] = breed_attack[breed]
else:
data["attack_val"] = 15
return data
def create_person(name, age):
data = {
"name": name,
"age": age,
"life_val": 100,
}
if age > 18:
data["attack_val"] = 50
else:
data["attack_val"] = 30
return data
dog1 = create_dog("zhaopeng", "Husky")
dog2 = create_dog("zhangsan", "Mastiff")
person1 = create_person("lisi", 30)
person2 = create_person("wangwu", 15)
print(dog1, dog2)
# {'name': 'zhaopeng', 'breed': 'Husky', 'life_val': 100, 'attack_val': 20}
# {'name': 'zhangsan', 'breed': 'Mastiff', 'life_val': 100, 'attack_val': 40}
print(person1, person2)
# {'name': 'lisi', 'age': 30, 'life_val': 100, 'attack_val': 50}
# {'name': 'wangwu', 'age': 15, 'life_val': 100, 'attack_val': 30}
Now, let's write functions for a dog biting a person and a person hitting a dog:
def bite(dog, person):
person["life_val"] -= dog["attack_val"]
print(f"Dog [{dog['name']}] bit person [{person['name']}], remaining HP: [{person['life_val']}]")
def hit(dog, person):
dog["life_val"] -= person["attack_val"]
print(f"Person [{person['name']}] hit dog [{dog['name']}], remaining HP: [{dog['life_val']}]")
bite(dog1, person1)
# Dog [zhaopeng] bit person [lisi], remaining HP: [80]
hit(dog1, person2)
# Person [wangwu] hit dog [zhaopeng], remaining HP: [70]
This works, but if we accidentally pass the wrong arguments, like bite(person1, dog1), it fails logically. We need to ensure bite is only for dogs biting people and hit is only for people hitting dogs. We can embed these actions within the data structures:
def create_dog(name, breed):
data = {
"name": name,
"breed": breed,
"life_val": 100,
}
if breed in breed_attack:
data["attack_val"] = breed_attack[breed]
else:
data["attack_val"] = 15
def bite(person):
person["life_val"] -= data["attack_val"]
print(f"Dog [{data['name']}] bit person [{person['name']}], remaining HP: [{person['life_val']}]")
data["bite"] = bite
return data
def create_person(name, age):
data = {
"name": name,
"age": age,
"life_val": 100,
}
if age > 18:
data["attack_val"] = 50
else:
data["attack_val"] = 30
def hit(dog):
dog["life_val"] -= data["attack_val"]
print(f"Person [{data['name']}] hit dog [{dog['name']}], remaining HP: [{dog['life_val']}]")
data["hit"] = hit
return data
dog1 = create_dog("zhaopeng", "Husky")
dog2 = create_dog("zhangsan", "Mastiff")
person1 = create_person("lisi", 30)
person2 = create_person("wangwu", 15)
dog1["bite"](person1) # Dog [zhaopeng] bit person [lisi], remaining HP: [80]
person2["hit"](dog1) # Person [wangwu] hit dog [zhaopeng], remaining HP: [70]
dog2["hit"](person1) # KeyError: 'hit'
This restricts actions to the correct entities. This is essentially object-oriented programming in disguise.
2. Procedural vs. Object-Oriented Programming
Before discussing these paradigms, let's define a programming paradigm: It is a way of classifying programming languages based on their features. Programming is the process of writing code using specific syntax, data structures, and algorithms to instruct a computer. Different paradigms represent different approaches to solving problems.
Procedural Programming
Procedural programming uses a list of instructions to tell the computer what to do step-by-step. It relies on procedures, which are sets of computational steps. This is also called top-down programming; the program executes from top to bottom, solving a problem sequentially.
The basic idea is to break a large problem into smaller subproblems or procedures, each of which is further decomposed until its simple enough to solve.
For example, putting an elephant in a refrigerator:

- Open the refrigerator door:
open() - Put the elephant inside:
push() - Close the door:
close()
This is straightforward, but modifying the program can be difficult because changes in one part may affect dependent parts. For instance, if we later need to:
- Put a tiger in the refrigerator
- Put an elephant in a suitcase
- Put a tiger in the refrigerator without closing the door
If a variable is changed that other procedures depend on, a cascade of changes may be needed. As projects grow, maintenance becomes harder. Procedural programming is great for simple scripts, but for complex, evolving tasks, object-oriented programming is more suitable.
Object-Oriented Programming
From the same problem, we abstract two classes:
- Refrigerator class with
open()andclose()methods - Elephant class with an
enter()method

Now, changing requirements is easier. For example:
refrigerator.open()elephant.enter()refrigerator.close()
To put a tiger inside, just replace the second line with tiger.enter(). This is the essence of object-oriented design. Object-oriented programming (OOP) is a mature paradigm suitable for large software projects, promoting flexibility and code reuse.
An object in OOP represents an entity in the real world, with unique identity and characteristics. Objects interact with each other. They can also be abstract, like a "simple shape" abstracted from circles, squares, etc. OOP simulates the real world in terms of objects with attributes and behaviors.
3. Objects and Classes
Objects
An object is an abstract concept representing anything that exists. Everything in the world can be considered an object. Objects have two parts: static part (attributes) and dynamic part (behavior). Attributes describe the object's state, and behavior describes what the object can do. In our dog example, attributes are name, breed, health, attack power; behavior is biting.
In Python, everything is an object—numbers, strings, functions, etc. Python is inherently object-oriented.
Classes
A class is a blueprint that encapsulates the attributes and behaviors of objects. In other words, a class is a category of entities that share common attributes and behaviors. For instance, the class "Goose" would have attributes like beak, wings, claws, and behaviors like foraging, flying, sleeping. A specific goose migrating from north to south is an object of the Goose class.

In the dog-human fight game, the Dog class has attributes (name, breed, health, attack) and behavior (bite). A specific dog {'name': 'zhaopeng', 'breed': 'Husky', 'life_val': 100, 'attack_val': 20} is an object of the Dog class.
4. Features of Object-Oriented Programming: Encapsulation, Inheritance, Polymorphism
Encapsulation
Encapsulation is the core idea of OOP. It hides the internal state and implementation details of an object from the outside, exposing only what is necessary. The class is the carrier of encapsulation. For example, a computer user presses keys without knowing the internal circuitry. Encapsulation protects the integrity of data, making the code more maintainable and preventing external interference.
Inheritance
Shapes like rectangles, rhombuses, parallelograms, and trapezoids are all quadrilaterals. They share the property of having four sides. By extending the quadrilateral class, we get more specific shapes. For instance, a paralelogram extends quadrilateral, inheriting its attributes and behaviors, and adding its own (e.g., opposite sides parallel and equal).
The more specific class is called a subclass (or child class), and the more general class is the superclass (or parent class). A parallelogram is a special kind of quadrilateral, but not vice versa. Similarly, instances of a subclass are instances of the superclass, but not the other way around. Inheritance promotes code reuse by allowing subclasses to inherit the superclass's features and add new ones.
Polymorphism
Polymorphism means that a superclass reference can be used to point to a subclass object. For example, consider a Screw class with attributes (diameter and thread density). We can create two subclasses: LongScrew and ShortScrew, both inheriting from Screw. They share the same diameter and thread density but differ in length. Thus, the same superclass type can represent different subclass behaviors. Polymorphism allows treating objects of different subclasses uniformly through the superclass interface.