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.
We'll cover the following...
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:
Line 2: The
classkeyword registers a new type namedSpaceship. Unlike a variable assignment, this definition doesn't run code immediately; it establishes a structure we can use later.Line 4: We use
passhere 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
Spaceshipis now a recognized type in the program's namespace (__main__), just like built-in types such asintorlist.
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:
Lines 5–6: Calling
Spaceship()triggers the instantiation process. Python allocates a fresh block of memory for each call, meaningship_aandship_bare 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
isoperator 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:
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 insideship_a.Lines 11–12: We create the same attributes for
ship_bbut assign them different values. This demonstrates encapsulation of state: modifying the data inship_bhas absolutely no effect onship_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.
Line 3: We define
report_statusinside the class, making it a method shared by all spaceships. However, it requires theselfparameter to function.Line 5: The magic happens here:
self.nameis 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 passesship_ainto theselfslot. We don't writeship_ainside 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.
Line 7:
type(ship)confirms that our object isn't just a generic blob of data—it is specifically defined as aSpaceshipinstance.Line 10: This line reveals that the class
Spaceshipis itself an object, created by Python's built-intypemetaclass. 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.