Up to this point, every piece of data in our programs has been a stand-alone value: a price, a list of items, a dictionary of customer details. Classes let you bundle related data and the functions that work on it into a single named type, so product.price and product.apply_discount() live together instead of floating around your file. This lesson covers what a class is, how to define one with the class keyword, how to create instances, and how methods reach the instance through self. The deeper mechanics of __init__, the different method types, and inheritance each get their own chapter; here, we stay on the foundation.
A shopping cart isn't really three separate things. It's one thing with three parts: a customer ID, a list of items, and a running total. If you keep them in separate variables, your code has to remember which variables belong together and pass them around as a group:
This works, but it leaks. The function needs both items and total because they're really one piece of state that got split apart. The caller has to keep them in sync. If you forget to reassign cart_total, the next call sees a stale number. The fix is to glue the pieces together so they travel as one unit.
That glue is a class. A class is a blueprint that says "a cart has items, a total, and a customer ID, and here's how you add an item to it". Once that blueprint exists, you can stamp out as many carts as you want, and each one carries its own data without you having to track it by hand. The shape of the data and the operations on it live in one place, and the rest of your program asks the cart to do its own work instead of poking at its parts.
Mental model: A class is a recipe. An object is a cake baked from that recipe. The recipe doesn't taste like cake, and you can bake many cakes from the same recipe without them sharing icing.
You define a class with the class keyword, a name (PascalCase by convention), and a colon, followed by an indented block:
That's a complete, legal class. It does nothing useful, but Python accepts it. The pass is a placeholder body; Python requires at least one statement in the indented block, and pass is the smallest statement that satisfies the rule. The class itself is now a value you can use:
Two things to notice. First, Product prints as <class '__main__.Product'>, which tells you it's a class named Product defined in the module currently running (__main__). Second, type(Product) is <class 'type'>, because in Python every class is itself an object of type type. Classes are first-class values; you can pass them to functions, store them in lists, and return them from other functions. We won't lean on that yet, but it's worth knowing it isn't magic, the class is just an object like any other.
A more interesting class attaches some data and behavior. Here's a Product with a description and a method that prints it:
The line description = "A generic product" is a class attribute: a name attached to the class itself. The def show(self): is a method: a function defined inside the class body that takes a special first parameter named self. We'll come back to what self actually is in a moment. For now, treat both lines as things the class "owns".
Convention: Class names use PascalCase (Product, ShoppingCart, CustomerReview). Methods and attributes use snake_case (add_item, total_price, is_in_stock). This matches PEP 8 and is what every Python library you'll read does.
A class is a blueprint. To get something useful out of it, you call the class like a function. That call produces an instance, an actual object built from the blueprint:
Two calls to Product() produce two separate objects. They print at different memory addresses, and product_a is product_b is False because they are not the same object. Each call to the class allocates a new instance.
Notice we wrote Product() with empty parentheses, even though we didn't define any constructor parameters. That call goes through Python's machinery for building a new instance, which in the absence of any custom setup gives you a bare object with no instance-specific data. For this lesson, every instance we create starts empty and gets its data added afterward.
After the assignments, each instance carries its own name and price. The attribute description we put on the class doesn't show up in those prints because we didn't ask for it, but it's still reachable:
Both instances read the same value, because both instances are looking up description on the class they came from. The takeaway is that attaching attributes after the fact works, and each instance keeps its own copy of whatever you attach to it directly.
Setting attributes from outside the class like this is fine for demonstration but not how real code works. In practice, the __init__ method takes the data once at creation time.
It helps to picture the link between the class and the instances it produces. The class lives in one place. Each instance is a separate object that points back to its class:
The cyan node is the class. It holds the shared definition: the description value, the show method, anything else written inside the class block. The orange nodes are three instances. Each instance has its own per-object data (name and price), and each one knows which class it came from. When you read product_a.show, Python first looks at product_a itself for a show attribute and, not finding one, follows the arrow up to the class and finds it there.
This is the lookup rule in two sentences. Attribute access on an instance checks the instance first, then the class. That's how every instance can call the same show method without each instance carrying its own copy of the function.
Python gives you type() to ask any object what its class is:
The first print confirms the instance's class is Product. The second confirms the link is the literal class object, not just a string that looks like its name. type(some_instance) is the official way to ask "what kind of thing is this?".
You can also ask whether an object is an instance of a class with isinstance:
isinstance is the preferred check in most code because it also understands inheritance. For now, type(x) is C and isinstance(x, C) give the same answer for simple classes that don't inherit from anything custom.
A method is a function defined inside the class body. It looks almost like a regular function, with one twist: the first parameter is conventionally named self, and Python passes the instance automatically when you call the method through the dot.
Walk through that call carefully. product_a.describe() is the dotted method call. Python finds describe on the class (the instance doesn't have its own copy), and calls it with product_a bound to the first parameter. Inside the method, self is product_a. So self.name reads product_a.name, which is "Wireless Mouse", and self.price reads product_a.price, which is 29.99.
The same method works for every instance:
Two instances, one method, two different outputs. The method body never refers to product_a or product_b by name. It always goes through self, which is whichever instance is being acted on. That's the whole trick: one function defined once, reused for every instance, with self filling in which instance it should look at.
Methods can take other parameters too. self is just the first one:
When you call product_a.apply_discount(10), Python passes product_a as self and 10 as percent. Inside the method, self.price = self.price * (1 - percent / 100) reads the current price off the instance, computes the discounted value, and writes it back. After the call, product_a.price is 26.991, and describe sees the new value the next time it's called.
Python has three kinds of methods: instance methods, class methods, and static methods. Everything we've written here is an instance method, the most common kind. It takes self as the first parameter and operates on per-instance data. That's enough to keep moving.
About `self`: The name self is a convention, not a keyword. Python passes the instance as the first argument no matter what you call the parameter. You could write def describe(this): and it would work. But every Python programmer expects self, and every linter and IDE assumes it. Don't fight the convention.
self Actually Worksself is the parameter that connects a method back to the instance it was called on. The mechanism is simple enough that it stops feeling magical once you see it laid out.
When you write product_a.describe(), Python does this:
describe on product_a. Not there, so look on the class Product. Found.describe that takes one parameter self.self = product_a, then runs the function body.That last step is what the dot does. It's a small piece of glue: "the thing before the dot becomes the first argument of the function after the dot". You can see it in slow motion by calling the method through the class instead of the instance:
Product.describe(product_a) is what product_a.describe() actually does under the hood. You almost never write it this way (the dotted form is shorter and clearer), but seeing it spelled out makes self less mysterious. It's just the first argument. The dot is just sugar for passing the instance in automatically.
Forgetting self is one of the most common beginner mistakes. What's wrong with this code?
The method describe was defined to take zero parameters, but the call product_a.describe() passes product_a as the first argument automatically. Python complains because the count doesn't match. Fix: add self as the first parameter, even if the method doesn't use it.
Every regular method needs self as the first parameter. The interpreter doesn't care about the name, only the position. Naming it anything other than self works mechanically but throws off every reader.
Putting the pieces together, here's a Cart with a small surface area: a few attributes set after construction, two methods that operate on them, and self doing the wiring.
The class defines one shared piece of data (store_name, on the class itself), two methods, and nothing else. Each instance gets its own customer, items, and total after creation. The add method reaches the instance's own list and total through self, mutates them, and the change sticks because self is the actual instance, not a copy. The summary method does the same to print the current state.
Having to write cart.items = [] and cart.total = 0.0 from outside the class is ugly and easy to forget. That's exactly the problem __init__ solves. The shape of the class above is what you'll write hundreds of times once you've learned that one extra method.
type() and the object Base ClassTwo builtin pieces tie classes into the rest of Python and are worth knowing on day one, even before inheritance enters the picture.
The first is type(). Pass it any object and it gives you the object's class. Pass it a class and it gives you type, the class of classes:
Every value in Python is an object, and every object has a class. Integers, strings, lists, your own classes, even classes themselves all fit the same rule. type() is the lens that lets you see which class anything belongs to. It's also the first thing you reach for when you're debugging "wait, what kind of thing is this actually?".
The second is the object base class. Every class in Python implicitly inherits from a builtin class called object if you don't say otherwise. Writing class Product: is the same as writing class Product(object):. The practical effect is that every class you write picks up a small set of default behaviors and dunder methods from object for free.
__bases__ is a tuple of the classes a class inherits from directly. For a plain class Product:, that tuple contains just object. Every class in Python 3 traces back to object eventually, which is what makes generic things like isinstance(x, object) always True and what lets every object respond to dunder methods like __repr__ even when you haven't defined one yourself.
You don't have to write (object) after the class name. It's automatic. Some legacy Python 2 code does write it explicitly, and you'll occasionally see it in older projects, but in modern Python it's redundant.
10 quizzes