AlgoMaster Logo

OOP Basics

High Priority15 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Object-oriented programming is a way of organizing code around the data your program works with, instead of around the operations it performs. A Product, a Cart, an Order, each becomes a named type that bundles its own data with the functions that act on it. This lesson covers what OOP is, why it exists, the four ideas it's usually summarized by, and how Python in particular handles classes compared to languages with more ceremony.

Before Classes: The Procedural Style

One way to write a program: pick some data, define a set of functions that operate on it, and call those functions in order. That style is called procedural programming. It works for short scripts, and it's how most of the code we've written so far has been structured.

A small shopping cart written procedurally:

Nothing is wrong with this code. It runs, it works, and a small program could be built this way. The data is a dictionary, the functions take that dictionary as their first argument, and the program is the sequence of calls at the bottom.

The trouble starts when the program grows. The functions add_item, remove_item, show_cart, and a dozen others (apply_discount, merge_carts, clear_cart, count_items) all need to live somewhere. They all take a cart as their first argument. They all know the shape of the cart dictionary (which keys it has, what types those values are). And nothing in the language ties them together. The make_cart function is defined on line 1, show_cart on line 80, and clear_cart in a different file. The only link is that the programmer remembers they all operate on the same kind of dict.

If a teammate changes the cart dict to use "total_amount" instead of "total", every function has to be updated to match. The compiler can't help, because as far as Python is concerned, it's all just dictionaries.

Bundling Data and Behavior

Object-oriented programming says: take the data and the functions that operate on it, and group them into a single named type. A cart isn't a dictionary plus a pile of loose functions; it's one thing called a Cart, and the functions that work on it are part of that thing.

The same example, written with a class:

The output is identical. The structure is not. The data (customer, items, total) and the operations (add_item, remove_item, show) are now defined together inside the Cart class. The call site reads as cart.add_item(...) instead of add_item(cart, ...). The cart and its operations are one unit, not a dict plus a folder of related functions.

The mechanics are the same: there's still data, there are still functions, and the functions still take the data as their first argument (now called self instead of cart). What's different is the grouping. Reading the class shows, in one place, everything a cart is and everything a cart can do. The procedural version requires searching for every function that touches a cart dict.

The next few sections stay at the conceptual level: why this grouping matters, what problems it solves, and the four ideas common to OOP.

State and Behavior, Together

The term commonly used in OOP is state. State is the data an object carries: the customer name, the list of items, the running total. Each cart in your program has its own state, separate from every other cart's. Two Cart objects can have the same items by coincidence, but they don't share storage; mutating one doesn't touch the other.

Behavior is the set of operations that act on that state: adding an item, removing an item, computing a discount, showing the cart. In a class, behavior is expressed as methods, which are functions that live on the class and receive the instance as their first argument.

Combining state and behavior into one named type gives several practical wins:

  • Code colocates. Everything a cart does is in the Cart class. No need to search across files for all the cart-related functions.
  • Names get cleaner. cart.add_item("Mouse", 29.99) reads better than add_item_to_cart(cart, "Mouse", 29.99). The cart is in the dotted call, so the function name doesn't have to spell it out.
  • The shape of the data is documented. Reading the __init__ of Cart shows, in one glance, which attributes a cart has. No need to infer them by reading every function that pokes at the dict.
  • Mistakes get harder. A function that expects a Cart and gets a Product raises an AttributeError the moment it tries to call something the Product doesn't have. Dictionaries are duck-shaped; classes have a clearer signal of "this is the wrong thing".

These are small ergonomic wins that compound as the codebase grows. A 50-line script doesn't need any of them. A 5,000-line app does.

The Four Pillars (At a Glance)

Most OOP introductions list four ideas that show up in nearly every object-oriented language: encapsulation, inheritance, polymorphism, and abstraction. This section sketches each at a high level to introduce the vocabulary.

The diagram shows the four ideas as branches off the OOP trunk. They aren't a checklist to "complete" when writing a class; they're lenses for thinking about how code is organized. A class can use all four, or mostly one, depending on what it's modeling.

Encapsulation

Encapsulation is the bundling described above: data and the functions that act on it live together in a single type. A Cart adds items to itself because the add_item method is part of the Cart class, not a separate function. The state (items, total) and the behavior (add_item, remove_item) are encapsulated inside the class.

There's a second piece to encapsulation: hiding internal details. Code outside the class should call methods like cart.add_item("Mouse", 29.99) rather than reach in to mutate cart.total by hand. The internals are the class's business; the external surface is the methods it offers. Python is relaxed about enforcing this (there are no truly private attributes), but the convention of accessing state through methods is the standard way to write Python classes.

Inheritance

Inheritance lets one class build on another. Given a Product class with name, price, and apply_discount, and a need for a DigitalProduct with all of those plus a download_url, the solution is not to copy the Product code; DigitalProduct inherits from Product. The subclass gets everything the parent has, plus whatever extra it adds.

A sketch:

DigitalProduct inherits from Product, so every DigitalProduct instance has a name and a price automatically. Inheritance fits genuine "is-a" relationships (a digital product is a product) but is easy to overuse for "kind of similar to" relationships.

Polymorphism

Polymorphism is the idea that different types can be used through the same interface. If a Cart calls .price on whatever it's holding, it doesn't care whether each item is a PhysicalProduct, a DigitalProduct, or a Subscription, as long as each one has a price attribute. The cart works with all three through the same code path.

In Python, polymorphism is often informal. There's no declaration that two classes "implement the same interface"; give them the same method names and attributes, and code that uses those names works with either type. That style has a name (duck typing) and a saying that goes with it: "if it walks like a duck and quacks like a duck, it's a duck."

Abstraction

Abstraction is closely tied to encapsulation, but it's about what the class exposes, not just what it bundles. A good class hides the details of how something works and gives callers a clean, intent-revealing interface. The caller knows what the class does, not how.

A Cart with a method apply_discount(percent) is abstracting away the math. The caller says "apply 10%" and the method works out the rest. The caller doesn't need to know that the implementation iterates the items list and multiplies each price (or adjusts the running total once). That implementation could change tomorrow and the caller wouldn't notice, as long as apply_discount(percent) still does what its name promises.

That's the list. Encapsulation, inheritance, polymorphism, abstraction. The names are what to recognize.

Why Use Classes at All?

Not every program needs classes. Plenty of useful Python code (scripts, small utilities, single-file tools) gets along with functions and built-in types. Using a class because "real Python has classes" is the wrong reason. So when is a class the right choice?

Signals that a class will help:

  • Multiple values move together. If a customer is always represented by name plus email plus address plus a list of past orders, those four pieces are really one thing, and a class makes that explicit. Passing them as separate arguments to every function is tedious and error-prone.
  • State changes over time. A cart accumulates items, an order moves through statuses (placed, shipped, delivered), a session tracks the current user. Anything with a lifecycle benefits from an object that holds the state and provides methods to transition it.
  • The same operations apply to many similar things. When creating thousands of products, each with the same shape and the same operations (apply_discount, is_in_stock, format_price), a Product class lets those operations be written once and applied to every instance.
  • Enforcing rules. If a Review rating must be between 1 and 5, putting the validation in __init__ means no Review instance can exist with an invalid rating. The class is a gatekeeper. Achieving the same thing with a dict means scattering checks across every function that creates or modifies a review.
  • Extension later. With a Product today and a possible DigitalProduct or SubscriptionProduct tomorrow, starting with a class makes the extension obvious. Starting with a dict makes it a rewrite.

And when functions and built-ins are enough:

  • Stateless operations. A function that takes a string and returns a slugified version doesn't need to be a method on a class. Wrapping it in a Slugifier class with a single method is ceremony for ceremony's sake.
  • One-off scripts. A 30-line script that reads a CSV, computes a sum, and prints it doesn't need a class. Use what fits.
  • Functional pipelines. If the work is mostly transforming data through a chain of pure functions (map, filter, reduce style), functions and built-in types often read more clearly than classes.

The rule of thumb: use a class when the alternative is passing the same three or four values to every function in a group, or when something has its own lifecycle that needs tracking. If neither is true, functions are usually fine. Python is unusual among "OOP languages" in not forcing classes for everything; use that flexibility.

A class has a small runtime overhead compared to a plain dictionary: attribute lookups go through more layers, and instances carry a reference to their class. For most code, this doesn't matter. When storing millions of small records, libraries like dataclasses (with slots=True) or typing.NamedTuple give class-like syntax with leaner storage.

Python's Take on OOP

Different languages do OOP differently, and Python's flavor has a particular character. Coming from another language, some of Python's choices may seem unusual; for Python as a first language, knowing these choices helps when reading about OOP elsewhere where other languages are stricter.

Everything Is an Object

In Python, every value is an object. Integers, strings, lists, functions, classes themselves: all of them are objects with a class, attributes, and methods. There is no distinction between "primitive types" (which can't have methods) and "object types" (which can) the way there is in some languages.

Every one of those values has a class, and methods can be called on all of them. Functions are objects too: they can be passed as arguments, stored in lists, and have attributes attached to them. That uniformity makes Python's object model consistent. User-defined classes sit alongside the built-in types, and operators like +, comparison, indexing, and iteration all work through the same mechanism (dunder methods).

Duck Typing Over Strict Interfaces

In languages like Java or C#, polymorphism is usually expressed through interfaces or abstract base classes that types must explicitly declare they implement. Python prefers the lighter approach: if an object has the right methods and attributes, it works in any place that calls those methods, regardless of its class.

cart_total doesn't know or care that the list mixes two different classes. It reads .price off each item. As long as every item has a .price attribute, the function works. That's duck typing, and it's how most polymorphism is expressed in Python.

There's a trade-off. The flexibility is genuine: a new "cart-able" type can be added without inheriting from a common base or formally implementing an interface. The downside is that the connection between cart_total and the types it works with isn't declared anywhere. A reader has to work it out from the code. Python's type hints (item: Product, or Protocol from typing) help fill that gap when the implicit contract is worth making explicit.

Less Ceremony Than Java or C++

Python's class syntax is shorter than most. There are no field declarations at the top of the class, no separate header file, no required getters and setters, no mandatory access modifiers, no curly braces. A useful Python class can be three lines:

The same class in Java is several times as long once the field declarations, the constructor, the getters and setters, and the package boilerplate are written. None of that extra code does anything Python's three lines don't already cover.

This is a deliberate language choice. Python optimizes for readability and short programs. The trade-off is that some things other languages enforce at compile time (private fields, type safety, abstract methods that subclasses must implement) Python enforces by convention, by runtime checks, or not at all. Python's tools for those include the _ prefix convention for "private", @property for controlled access, and ABC for required methods.

Multiple Inheritance Exists, Use Sparingly

Python allows a class to inherit from more than one parent, a feature some other languages restrict or forbid. The flexibility is occasionally useful but easy to abuse. The method resolution order determines which parent's method wins when more than one defines the same name. The headline: Python allows it, but most Python code uses single inheritance, and many designs that look like they "need" multiple inheritance are cleaner with composition (one class holding instances of others as attributes).

The diagram is a rough placement, not a strict ranking. Different languages occupy different points on the trade-off curve between "the compiler catches everything" and "the programmer has the flexibility to do whatever." Python sits closer to the flexible end than Java or C++ but not as far as JavaScript. Knowing where Python sits helps when reading code in another language and noticing it looks heavier; that's not Python being lax, it's the other language enforcing things at a different point in the design.

A Real Python Class

A class that combines the mechanics: defining a class, creating instances, setting them up with __init__, attributes (instance vs class), methods, and special "dunder" methods that integrate classes with operators and built-ins.

There's a class attribute, an __init__, two regular methods, and one dunder method (__repr__) that hooks into print. This is what a "real" Python class looks like in Pythonic style.

Quiz

OOP Basics Quiz

10 quizzes