Search⌘ K
AI Features

Defining Classes and Creating Instances

Explore how to create custom Python classes and instantiate objects to model complex data. Understand attributes, methods, and object state management using self, enabling you to design and work with distinct instances effectively.

So far, we have worked exclusively with Python’s built-in objects. We know that an int handles arithmetic, a str represents text, and a list organizes sequences of values. However, real-world software often needs to model complex Python data types that go beyond simple integers or strings.

Many programs need to model domain-specific concepts that do not map cleanly to a single built-in type. For example, consider a flight simulator. A single primitive type, such as a number or list, does not represent a Spaceship. It has attributes such as a name, fuel level, and speed, along with defined behavior. For example, it may support operations such as acceleration, deceleration, and fuel consumption during flight. It should also prevent flight when the fuel level reaches zero.

This information could be represented using dictionaries and standalone functions, but that approach becomes difficult to manage and prone to misuse as programs grow. Object-oriented programming (OOP) allows developers to define custom types that combine data and behavior. Instead of relying only on built-in types, developers can define their own types to model program behavior.

The class keyword

A class is a blueprint. It defines the structure and behavior of an object, but it is not an object itself. Just as a blueprint for a house is not something you can live in, a class is a template used to create actual objects.

In Python, we define a class using the class keyword. If we are learning how to create a class in Python, think of it as creating a custom blueprint that defines the structure and behavior for our future objects. By convention, Python class names use PascalCase (also known as CapWords), where every word is capitalized without underscores (e.g., Spaceship, UserProfile, GameLevel). This distinguishes them from functions and variables, which use snake_case.

Here is the simplest possible class definition:

Python
# Defining a minimal class
class Spaceship:
# 'pass' allows us to define an empty block without errors
pass
print(Spaceship)
  • Line 2: The class keyword registers a new type named Spaceship. Unlike a variable assignment, this definition doesn't run code immediately; it establishes a structure we can use later.

  • Line 4: We use pass here because Python classes must have a body. In a real application, this is where we would define the attributes and methods shared by all spaceships.

  • Line 6: Printing the class name confirms that Spaceship is now a recognized type in the program's namespace (__main__), just like built-in types such as int or list.

Instantiation

Defining a class is only the first step. To actually use it, we must create an instance. An instance is a concrete object created from the class blueprint. From a single class definition, we can create many distinct instances, each with its own data.

In Python, an instance is created by calling the class name as if it were a function, using the following syntax:

Python
class Spaceship:
pass
# Creating two separate instances (instantiation)
ship_a = Spaceship()
ship_b = Spaceship()
# Checking what they are
print(f"ship_a is: {ship_a}")
print(f"ship_b is: {ship_b}")
# Verifying they are different objects
print(f"Are they the same object? {ship_a is ship_b}")
  • Lines 5–6: Calling Spaceship() triggers the instantiation process. Python allocates a fresh block of memory for each call, meaning ship_a and ship_b are completely independent entities despite being made from the same mold.

  • Lines 9–10: The output reveals the memory address (the hex code like 0x7f...) for each object. Notice that while the type is the same (Spaceship), the addresses differ, proving they live in different places in memory.

  • Line 13: The is operator confirms identity. Even though these two ships look identical right now, they are not the same object, just like two identical cars coming off an assembly line are still two separate cars.

State and attributes

Objects are useful because they can store data. This data represents an object’s state, which is simply the information the object currently holds. In Python, this information is stored in attributes. Unlike global Python variables, these attributes are 'owned' by a specific object, representing that object's unique state. Each attribute has a value, and together, these values make up the object’s state at a given moment. Let's see them in practice below:

Python
class Spaceship:
pass
ship_a = Spaceship()
ship_b = Spaceship()
# Assigning attributes manually using dot notation
ship_a.name = "Voyager"
ship_a.speed = 100
ship_b.name = "Enterprise"
ship_b.speed = 200
# Accessing the data
print(f"{ship_a.name} is traveling at {ship_a.speed} km/s")
print(f"{ship_b.name} is traveling at {ship_b.speed} km/s")
  • Lines 8–9: Python objects are dynamic; we can attach new data to them at any time. By assigning ship_a.name, we create a specific variable that exists only inside ship_a.

  • Lines 11–12: We create the same attributes for ship_b but assign them different values. This demonstrates encapsulation of state: modifying the data in ship_b has absolutely no effect on ship_a.

  • Lines 15–16: We retrieve the data using the same dot notation. The code looks the same (.name), but the result depends on which specific object (instance) we are asking.

Because ship_a and ship_b are separate instances, each object maintains its own state. Changing an attribute on one object does not affect the other. We can assign attributes directly using dot notation, written as object.attribute = value.

Note: In the next lesson, we will learn how to initialize an object’s state automatically when it is created. For now, we assign attributes manually to make the idea of object state explicit and easier to observe.

Defining behavior (Methods)

Data alone does not fully describe an object. Classes also define the actions an object can perform. Behavior is defined by writing functions inside the class body. These functions are known as methods. When defining an instance method, the first parameter is conventionally named self.

So, what is self? The name self allows the method to know which specific instance it is operating on. This is a special form of variable scope where the function can 'see' the data belonging to the instance it was called from. Without it, the method would have no way to determine whether it should act on the ship_a or ship_b. For example, when we call a method such as ship_a.report_status(), Python automatically passes the object ship_a as the first argument to the method. Inside the method, that argument is received as self.

Python
class Spaceship:
# A method to display status
def report_status(self):
# We access attributes using 'self.attribute_name'
print(f"Status Report: {self.name} is ready.")
ship_a = Spaceship()
ship_a.name = "Voyager"
ship_b = Spaceship()
ship_b.name = "Enterprise"
# Calling the method
ship_a.report_status()
ship_b.report_status()
  • Line 3: We define report_status inside the class, making it a method shared by all spaceships. However, it requires the self parameter to function.

  • Line 5: The magic happens here: self.name is a placeholder. It means "get the name attribute of whichever object called this method." This allows one piece of code to handle data for infinite distinct objects.

  • Lines 14–15: When we call ship_a.report_status(), Python implicitly passes ship_a into the self slot. We don't write ship_a inside the parentheses, but it is being sent behind the scenes.

Classes are objects too

In the last module, we learned that everything in Python is an object, and this includes class definitions themselves. When we define a class such as Spaceship, Python creates an object in memory whose type is type.

This relationship means that classes are first-class objects. We can inspect them, pass them around, and reason about them in the same way we do with built-in types. Our custom classes follow the same rules as Python’s standard ones, which is what makes the language both consistent and flexible.

Python
class Spaceship:
pass
ship = Spaceship()
# Checking the type of the instance
print(type(ship))
# Checking the type of the class itself
print(type(Spaceship))
# Using isinstance to verify type
print(f"Is ship a Spaceship? {isinstance(ship, Spaceship)}")
  • Line 7: type(ship) confirms that our object isn't just a generic blob of data—it is specifically defined as a Spaceship instance.

  • Line 10: This line reveals that the class Spaceship is itself an object, created by Python's built-in type metaclass. This is why we can pass classes around in our code just like variables.

  • Line 13: The isinstance() function checks the lineage of an object. This is crucial for verifying that an object adheres to the blueprint we expect before we try to use it.

We have successfully defined a new data type, Spaceship, and created multiple independent instances of it. We learned that each instance maintains its own separate namespace for attributes, and that methods use self to access that specific instance's data.

However, manually assigning attributes like ship.name = "..." every time we create an object is repetitive and error-prone. If we forget to set a name, our report_status() method would crash. In the next lesson, we will solve this by learning how to initialize objects automatically when they are created.